在我的计算机科学课程中,我们正在研究浮点数及其在内存中的表示方式。我已经了解了它们在内存中的表示方式(尾数/有效数,指数及其偏倚以及符号位),并且我了解了浮点数是如何相互加减的(非正规化和所有这些有趣的东西)。但是,在研究一些学习问题时,我注意到一些我无法解释的问题。
当将无法精确表示的浮点数加到自身上时,答案要比我们在数学上期望的要低,但是当同一浮点数乘以整数后,答案就精确地得出了正确的数字。
这是来自我们研究问题的一个示例(该示例是用Java编写的,为了简单起见,我对其进行了编辑):
float max = 10.0f; /* Defined outside the function in the original code */
float min = 1.0f; /* Defined outside the function in the original code */
int count = 10; /* Passed to the function in the original code */
float width = (max - min) / count;
float p = min + (width * count);
在此示例中,我们被告知结果精确地为10.0。但是,如果我们将此问题视为浮点数的总和,则会得到略有不同的结果:
float max = 10.0f; /* Defined outside the function in the original code */
float min = 1.0f; /* Defined outside the function in the original code */
int count = 10; /* Passed to the function in the original code */
float width = (max - min) / count;
for(float p=min; p <= max; p += width){
System.out.printf("%f%n", p);
}
我们被告知该测试中p的最终值为~9.999999,最后一个p值与max值之间的差为-9.536743E-7。从逻辑的角度(知道浮点如何工作),这个值是有意义的。
不过,我不明白的是,为什么第一个示例恰好得到10.0。从数学上讲,我们得到10.0是有意义的,但是知道浮点数如何存储在内存中,对我来说这没有意义。谁能解释为什么我们通过将不精确的浮点数乘以int来获得精确的值?
编辑:为澄清起见,在最初的研究问题中,一些值被传递给函数,而其他值则在函数外部声明。我的示例代码是学习问题示例的简化版本。由于某些值是传递给函数的,而不是显式定义为常量的,因此我认为可以排除编译时的简化/优化。
因为编译器将所有这些减少为一个恒定值。尝试使每个语句成为一个函数,然后一个接一个地调用。
@Amit,我很抱歉,我应该在问题中明确指出这一点。示例中定义的某些值将作为变量传递给计算最终结果的函数,因此似乎不太可能是编译器优化。我试图简化本文的代码,因此在示例中定义了值。不久后我将进行修改以澄清这一点。
除非您想通过编辑使我感到惊讶,否则我的评论(如果您希望我将其发布为答案)将保持不变。编译器会将所有语句优化为max值,因为所有语句都进行来回计算。
他可以通过在命令行或文件中输入数字来排除这些数字,从而使它们不是编译时间常数的变量。
我确定他们要教给您的是浮点数已损坏并且需要注意,因为您不能完全以2为基数的浮点数格式表示小数。关键是避免10次加法并进行1次乘法以提高精度。
@ Rob11311-当您想在评论中与某人联系时,请使用@符号和用户名,以便通知他们(我)。正如您现在已经意识到的那样,我的意思是准确的,并且从命令行,文件甚至心灵感应输入值都不会有所作为。
首先,挑剔:
When a float that cannot be precisely represented
没有"无法精确表示的浮点数"。所有float都可以精确地表示为float s。
is added to itself several times, the answer is lower than we would
mathematically expect,
当您多次向自己添加一个数字时,实际上可以获得比您预期更高的值。我将使用C99十六进制表示法。考虑f = 0x1.000006p+0f。然后f+f = 0x1.000006p+1f,f+f+f = 0x1.800008p+1f,f+f+f+f = 0x1.000006p+2f,f+f+f+f+f = 0x1.400008p+2f,f+f+f+f+f+f = 0x1.80000ap+2f和f+f+f+f+f+f+f = 0x1.c0000cp+2f。但是,7.0*f = 0x1.c0000a8p+2会四舍五入为0x1.c0000ap+2f,小于f+f+f+f+f+f+f。
but when that same float is multiplied by an integer, the answer,
comes out precisely to the correct number.
7 * 0x1.000006p+0f不能表示为IEEE float。因此,它被舍入。使用默认的四舍五入模式为"四舍五入",即使执行这样的单个算术运算,也可以得到与实际结果最接近的浮点数。
The thing that I do not understand, though, is why we get exactly 10.0
for the first example. Mathematically, it makes sense that we would
get 10.0, but knowing how floats are stored in memory, it does not
make sense to me. Could anyone explain why we get a precise and exact
value by multiplying an imprecise float with an int?
要回答您的问题,您将得到不同的结果,因为您执行了不同的操作。您在这里得到"正确"的答案有点a幸。
让我们来切换数字。如果我计算0x1.800002p+0f / 3,则会得到0x1.00000155555...p-1,将其舍入为0x1.000002p-1f。当我将其三倍时,得到0x1.800003p+0f,它会四舍五入(因为我们打破平局,直到平局)到0x1.800004p+0f。这与在float算术中f = 0x1.000002p-1f的情况下计算f+f+f时得到的结果相同。
由于1.0 + ((10.0 - 1.0) / 10.0) * 10.0仅使用不精确的值进行1次计算,因此产生1个舍入误差,因此它比对0.9f的float表示法进行10次加法更为准确。我认为这是本示例要教的原理。
关键问题是0.1不能精确地表示为浮点数。
因此0.9包含错误,这些错误加在函数循环中。
由于精确的输出格式化例程,因此可能显示"精确"数字。当我第一次使用计算机时,他们喜欢将这些数字以荒谬的科学固定数字格式发布,这对人类来说并不友好。
我想了解发生了什么事,我会在Koenig的Dobbs博士博客上找到关于此主题的文章,这是一篇很有启发性的文章,该系列通过展示perl,python和java之类的语言在不够精确的情况下如何使计算看起来准确来完善了该系列。
Koenig的Dobbs博士关于浮点的文章
甚至简单的浮点输出也很复杂
如果将定点算法添加到5-10年后的CPU中,不要太惊讶,财务人员喜欢精确的总和。
绝对是一个有用的答案,可以解释这里发生的情况。但是,width(~0.9的值)乘以10,而不是min(1.0的值)。但是,您链接到的博客文章使我有了一个有趣的想法。将width自身添加10次时,不会发生非规范化,因为width的指数显然是相同的。然后,将该结果添加到min时,足够大,以至于也不会发生非规范化。因此,没有任何精度损失,因此width的值"足够精确"以至于被认为是精确的。
编译器可以简化所写的表达式。你有一个除以计数,然后是一个乘法。同样,min + max-min可以减少为float p = max;。如今,编译器是如此的聪明。
正如阿米特(Amit)指出,聪明的编译器可以检测到您乘以除以相同的值后,硬件会随机调整数字以进行缩放。编译器不会在运行时进行10到10.0f的昂贵转换。因此,要检验该理论,您需要在运行时以浮点数形式输入计数。它应该比10个加法更为准确,但在编译时不能减少为float p = max;。并且感谢您为答案打勾,您必须经常急于入手,然后改善答案,或者在撰写本文时发现其他人重复。
啊,这实际上是一个很好的观点。我没有考虑过这样一个事实,当计算p时,我们实际上以min + ((max - min)count) * count结尾(正如您所指出的,它简化为p = max)。现在这似乎很明显,我不敢相信我忽略了xD,谢谢您指出这一点。
那么,您会分心,因为有经验,有时会看汇编器输出来弄清楚C编译器的真正作用,您有时会发现它非常聪明。我猜Java有某种或可读的输出选项,尽管我从未真正使用过。
它甚至在计算p时都没有,问题在于编译器为表达式构建了一棵树,对其进行分析,可以注意到在表达式除以SAME值后立即重新进行乘法运算,因此可以在知道它们已被抵消的情况下消除它们。通过将min + (max - min)重新排列为max - min + min,可以注意到相同的情况。编译器作者喜欢自由地对表达式和函数参数进行重新排序的原因之一。编译器可以使用值的名称,它们是identifiers,因此具有属性(如类型,常量和实际值)存储在符号表中。
整个讨论是我在初始评论中写的内容(首先,因为您已经提到重复项)非常冗长而冗长的重复。您的答案虽然有用,但与该问题无关。我还解释了如何在我的原始评论中对此进行验证。
@Amit,我理解您的建议"编译器将所有这些都减小到一个恒定值",但学生显然没有。您的意思马上肯定不是很清楚。其次,当我建议他使用运行时设置值进行测试时,我并没有特别针对您。第三,这不是他所要练习的重点,该问题可能是SO问题规则的结果