龙空技术网

浮点数问题示例

码农世界 98

前言:

眼前我们对“c语言随机浮点数”大体比较关切,我们都想要学习一些“c语言随机浮点数”的相关知识。那么小编同时在网络上网罗了一些对于“c语言随机浮点数””的相关资讯,希望各位老铁们能喜欢,你们快快来学习一下吧!

浮点如何工作?

我不打算在这篇文章中对浮点如何工作进行冗长的解释,但这是我几年前写的一篇关于基础知识的漫画:

浮点不是“坏”或随机的

我不希望你读这篇文章并得出结论,浮点是不好的。 这是进行数值计算的绝佳工具。这么多聪明人 为使计算机上的数值计算高效和 准确!关于这一切如何不是浮点的错的两点:

在计算机上进行数值计算本质上涉及 一些近似和四舍五入,特别是如果你想这样做 有效。您不能总是存储任意数量的精度 您正在使用的每个数字。浮点是标准化的(IEEE 754),因此诸如加法之类的操作 浮点数是确定性的——我的理解是 0.1 + 0.2 将始终为您提供完全相同的结果(0.30000000000000004),即使 跨不同的架构。这可能不是您期望的结果, 但这实际上是非常可预测的。

我写这篇文章的目的只是解释可以提出什么样的问题 浮点数及其发生的原因,以便您知道何时出现浮点数 小心他们,当他们不合适时。

现在让我们进入示例。

示例 1:停止的里程表

一个人说他们正在研究一个连续的里程表 向 32 位浮点数添加少量以测量行进距离,以及 事情变得非常不对劲。

为了具体化,假设我们正在向里程表添加数字 1cm 一次。万公里后是什么样子?

下面是一个模拟它的 C 程序:

#include <stdio.h>int main() {    float meters = 0;    int iterations = 100000000;    for (int i = 0; i < iterations; i++) {        meters += 0.01;    }    printf("Expected: %f km\n", 0.01 * iterations / 1000 );    printf("Got: %f km \n", meters / 1000);}

这是输出:

Expected: 10000.000000 kmGot: 262.144012 km

这是非常糟糕的 - 这不是一个小错误,262公里比10,000公里少很多。出了什么问题?

出了什么问题:浮点数之间的差距变大

在这种情况下,问题是,对于 32 位浮点数,262144.0 + 0.01 = 262144.0。 所以不仅仅是这个数字不准确,它实际上永远不会增加 完全!如果我们再行驶 10,000 公里,里程表仍然是 停留在262144米(又名262.144公里)。

为什么会这样?好吧,浮点数随着变大而相距越远。在此示例中,对于 32 位浮点数,下面是 3 个连续的浮点数:

262144.0262144.03125262144.0625

我通过去 并增加“有效”数字几次来获得这些数字。

因此,32.262144 和 0.262144 之间没有 03125 位浮点数。为什么这是一个问题?

问题是 262144.03125 大约是 262144.0 + 0.03。因此,当我们尝试 将 0.01 添加到 262144.0,向上舍入到下一个数字是没有意义的。所以 总和仅保持在 262144.0。

此外,262144是 2 的幂(它是 2^18)并非巧合。差距 浮点数在 2 的每个幂后发生变化,在 2^18 处间隙 32 位浮点数之间的值为 0.03125,从 0.016 开始。

解决这个问题的一种方法:使用双精度

使用 64 位浮点数可以解决此问题——如果我们在上面的 C 程序中替换,一切都会好得多。下面是输出:float改成double

Expected: 10000.000000 kmGot: 9999.999825 km

这里仍然有一些小的不准确之处——我们偏离了大约 17 厘米。 这是否重要取决于上下文:稍微偏离可能会非常 好吧,如果我们在进行精确的太空机动或其他事情,那将是灾难性的,但是 对于里程表来说可能没问题。

改善这一点的另一种方法是将里程表增加更大的块 – 与其一次添加 1 厘米,也许我们可以减少更新频率, 就像每50厘米一样。

如果我们使用双倍数并递增 50 厘米而不是 1 厘米,我们会得到确切的 正确答案:

Expected: 10000.000000 kmGot: 10000.000000 km

解决这个问题的第三种方法可能是使用整数:也许我们决定 我们关心的最小单位是0.1mm,然后测量所有内容 0.1mm的整数倍。我从来没有做过里程表,所以我不能说什么 最好的方法是。

示例 2:JavaScript 中的推文 ID

Javascript 只有浮点数——它没有整数类型。 您可以在 64 位浮点数中表示的最大整数是 2^53.

但是推文 ID 是大数字,大于 2^53。Twitter API 现在返回 它们既是整数又是字符串,因此在 Javascript 中你可以只使用 字符串 ID(如“1612850010110005250”),但如果您尝试使用整数 在JS版本中,事情会变得非常错误。

你可以自己检查这一点,方法是获取推文 ID 并将其放入 Javascript 控制台,如下所示:

>> 1612850010110005250    1612850010110005200

请注意,1612850010110005200 与 1612850010110005250 的数字不同!!少了50个!

这个特殊的问题不会发生在 Python(或我的任何其他语言)中 知道),因为Python有整数。如果我们在Python REPL中输入相同的数字,会发生什么:

In [3]: 1612850010110005250Out[3]: 1612850010110005250

正如您所期望的那样,相同的数字。

例 2.1.. 损坏的 JSON 数据

这是“Javascript中的推文ID”问题的一个小变体,但即使 你实际上并没有在编写Javascript代码,JSON中的数字有时仍然是 被视为漂浮物。这对我来说很有意义,因为 JSON 有 名称中的“Javascript”,因此以这种方式解码值似乎是合理的 Javascript会。

例如,如果我们通过一些 JSON 传递,我们会看到完全相同的问题: 1612850010110005250的数字将更改为1612850010110005200。jq

$ echo '{"id": 1612850010110005250}' | jq '.'{  "id": 1612850010110005200}

但是它在所有JSON库中并不一致,Python的模块将解码为正确的整数。json1612850010110005250

有几个人提到了在 JSON 中发送浮点数的问题,无论是 他们试图在 JSON 中发送一个大整数(如指针地址),并且 它已损坏,或来回发送较小的浮点值 反复,价值随着时间的推移慢慢发散。

示例 3:差异计算出错

假设您正在做一些统计,并且想要计算方差 许多数字。也许比你可以很容易地放入内存的数字多,所以你 想一次性完成。

您可以使用一种简单(但糟糕!!!)算法来计算单次传递的方差, 来自这篇博文。下面是一些 Python 代码:

def calculate_bad_variance(nums):    sum_of_squares = 0    sum_of_nums = 0    N = len(nums)    for num in nums:        sum_of_squares += num**2        sum_of_nums += num    mean = sum_of_nums / N    variance = (sum_of_squares - N * mean**2) / N    print(f"Real variance: {np.var(nums)}")    print(f"Bad variance: {variance}")

首先,让我们使用这个糟糕的算法来计算 5 个小数的方差。一切看起来都不错:

In [2]: calculate_bad_variance([2, 7, 3, 12, 9])Real variance: 13.84Bad variance: 13.840000000000003 <- pretty close!

现在,让我们尝试一下非常接近的 100,000 个大数(分布在 100000000 和 100000000.06 之间)

In [7]: calculate_bad_variance(np.random.uniform(100000000, 100000000.06, 100000))Real variance: 0.00029959105209321173Bad variance: -138.93632 <- OH NO

这是非常糟糕的:不仅差错了,而且是负的!(方差永远不应该是负数,它总是零或更多)

出了什么问题:灾难性取消

这里的情况类似于我们的里程表数字问题:数字变得非常大(大约 10^21 或 2^69),此时, 连续浮点数之间的差距也很大——是 2**46。 所以我们只是在计算中失去了所有的精度。sum_of_squares

这个问题的术语是“灾难性取消”——我们正在减去 两个非常大的浮点数,它们都会很远 从计算的正确值,所以减法的结果是 也会错的。

我之前提到的博客文章谈到了人们用来计算方差的更好算法,称为 Welford的算法,它没有灾难性的取消问题。

当然,大多数人的解决方案是只使用科学 像 Numpy 这样的计算库来计算方差而不是尝试这样做 你自己:)

示例 4:不同的语言有时以不同的方式执行相同的浮点计算

一群人提到不同的平台也会做同样的事情 以不同的方式计算。这在实践中出现的一种方式是——也许 你有一些前端代码和一些后端代码做完全相同的事情 浮点计算。但是在Javascript中它的方式略有不同。 而在PHP中,所以你的用户最终会看到差异并感到困惑。

原则上,您可能会认为不同的实现应该工作 同样的方式,因为 IEEE 754 浮点标准,但这里有一个 提到的几个警告:

libc 中的数学运算(如 sin/log)在不同的 实现。因此,使用 glibc 的代码可能会给你不同的结果 使用 MUSL 编写代码某些 x86 指令可以使用 80 位精度进行某些双重操作 内部而不是 64 位精度。这是一个 GitHub 问题在谈论 那

我对这些观点不是很确定,也没有具体的例子可以复制。

示例5:深空海妖

坎巴拉太空计划是一款太空模拟游戏,它曾经有一个错误 被称为深空海妖,当 你移动得非常快,你的飞船会因为浮点问题而开始被摧毁。这类似于我们讨论过的其他涉及大浮点数的问题(如方差问题),但我想提到它,因为:

它有一个有趣的名字这似乎是视频游戏/天体物理学/模拟中非常常见的错误——如果你的点离原点很远,你的数学就会搞砸

另一个例子是《我的世界》中的远方之地。

示例 6:时间戳不准确

我保证这是“非常大的浮动数字会毁了你的一天”的最后一个例子。 但!再来一个!假设我们尝试以纳秒为单位表示当前的Unix时代。 (大约 1673580409000000000)作为 64 位浮点数。

这可不行!1673580409000000000大约是 2^60(至关重要的是,大于 2^53),之后的下一个 64 位浮点数是1673580409000000256。

因此,这将是最终导致时间数学不准确的好方法。之 当然,时间库实际上将时间表示为整数,所以这不是 通常是一个问题。(2038年的问题总是存在的,但事实并非如此 与浮标有关)

一般来说,这里的教训是有时最好使用整数。

示例 7:将页面拆分为列

现在我们已经讨论了大浮点数的问题,让我们做 浮点数小的问题。

假设您有一个页面宽度和一个列宽,并且您想要弄清楚:

页面上可容纳多少列剩余多少空间

您可以合理地尝试第一个 问题和第二个问题。因为 这对整数来说很好!floor(page_width / column_width)page_width % column_width

In [5]: math.floor(13.716 / 4.572)Out[5]: 3In [6]: 13.716 % 4.572Out[6]: 4.571999999999999

这是错误的!剩余空间量为 0!

计算剩余空间量的更好方法可能是,它给了我们一个非常小的负数。13.716 - 3 * 4.572

我认为这里的教训是永远不要用浮点数以 2 种不同的方式计算同样的事情。

这是一个非常基本的例子,但我可以看到这将如何创建所有 如果我使用浮点数进行页面布局,则会出现各种问题,或者 做CAD图纸。

示例 8:碰撞检查

这是一个非常愚蠢的Python程序,它从1000开始一个变量,并且 递减它,直到它与 0 发生冲突。您可以想象这是 乒乓球游戏什么的,那是一个应该与之碰撞的球。 一堵墙。a

a = 1000while a != 0:    a -= 0.001

您可能希望此程序终止。但事实并非如此! 从不为 0, 相反,它从 1.673494676862619e-08 变为 -0.0009999832650532314。a

这里的教训是,通常不是检查浮点相等性,而是 想要检查两个数字是否相差一些非常小的数量。或者在这里 我们可以写.while a > 0

这就是现在的全部内容

我什至没有达到 NaN(它们太多了!)或无穷大或 +0 / -0 或次正常,但我们已经 已经写了 2000 字,我将发表这个。

我可能会稍后再写一篇后续文章——乳齿象线程从字面上看 15字的浮点题在里面,材料很多!或者我 可能没有,谁知道:)

标签: #c语言随机浮点数 #c语言整数和浮点数运算的结果