提问人:Kai Schmidt 提问时间:5/31/2023 更新时间:6/2/2023 访问量:157
存储/加载/移动后浮点数相等
Equality of floating point numbers after storing/loading/moving
问:
我和一位同事在比较两个未经数学运算的浮点数时会发生什么存在分歧。也就是说,这些数字可能已经在内存和/或CPU寄存器周围移动,但没有对它们进行数学运算。也许它们已被放入列表中,然后被删除或其他各种操作。
我的经验使我相信,对浮点数进行非算术运算不应该改变它们,也不应该受到与算术运算相同的舍入误差的影响。我的同事认为,对于某些现代架构,CPU 的浮点处理部分被允许稍微损坏数字,从而导致相等性检查失败,即使仅存储/加载/移动值也是如此。
例如,请考虑以下 C 代码:
float* a = (float*)malloc(sizeof(float));
float* b = (float*)malloc(sizeof(float));
*a = 1.0;
*b = 1.0;
int equal = *a == *b;
有没有情况不会?equal
1
答:
我假设你指的是IEEE754二进制浮点数,因为现在大多数传统 CPU 都会使用这些浮点数。
在这种情况下,是的,只是移动这些值不会导致值发生变化。在对值进行操作时,听起来您的直觉可能需要澄清。具体来说,任何操作的结果都应该与FPU以完全(即任意)精度执行操作,然后仅将结果四舍五入以适合适当的(例如32位二进制浮点数)相同。因此,对于实现 IEEE754 浮点数的 CPU,发生的任何舍入都是确定性的——这就是语句背后的原因。0.1 + 0.2 != 0.3
请注意,通过使用 C,您已经通过两种机制使它复杂化了;1. C 不需要IEEE754语义;2. C 将自动在 和 值之间转换(即在通用架构上IEEE754 32 位和 64 位二进制浮点数)。这两个功能可能意味着代码可能无法达到幼稚读者的期望。float
double
作为一个更完整的例子,我将以下代码放入 Godbolt 的编译器资源管理器中:
#include <math.h>
int explicit_nan(float * restrict a, float * restrict b, float val) {
*a = val;
*b = val;
return isnan(val) ? 1 : *a == *b;
}
int implicit_nan(float * restrict a, float * restrict b, float val) {
*a = val;
*b = val;
return *a == *b;
}
restrict
关键字是必需的,否则 Clang 会通过假设代码可能被调用来防御性地编译代码:
char data[sizeof(float) + 1];
explicit_nan((float*)(data), (float*)(data+1), 1.f);
这会导致写入更改 的值。想必你不在乎这种情况,但是C编译器不能这么宽松。b
a
可以看出,代码编译为始终返回的代码(通过设置 ),而代码仅对传递的参数进行比较,以便在少量代码中正确处理 NaN。explicit_nan
1
EAX
implicit_nan
评论
float
double
gcc -ffloat-store
gcc -std=c99
c11
gnu99
float
double
long double
float
你的同事的信念可能起源于 C 和 C++ 规则。
C 和 C++ 允许使用比操作数的名义类型更精确地计算浮点表达式。
例如,给定所有变量,可以使用算术进行计算。规则要求,当结果存储在 中时,将其转换回标称类型。但是,中间计算可以使用 .例如,如果是 2 30、是 1 并且是 2 30,则如果使用算术计算,则此表达式将产生 0,因为将 2 30 和 1 相加将产生 2 30(因为 2 30+1 不能以常用的格式表示,因此它被舍入为可表示的值),然后减去 2 30 得到 0。但是,如果使用算术,则加法产生 230+1,减去 230 得到 1。float
d = a + b - c;
double
d
float
double
a
b
c
float
float
double
这与以下事实没有什么不同:给定所有变量,使用算术计算表达式,并且可能产生与仅使用算术计算的结果不同的结果。例如,由于使用了算术,with = 100, = 3 将产生 100。如果使用算术,将得到 44 (300−256),除以 3 会得到 14。因此,这不仅仅是一个浮点问题;这是一个关于编程语言如何计算表达式的问题。unsigned char
int
unsigned char
a
b
d = 3 * a / b ;
int
unsigned char
3 * a
C 和 C++ 标准还要求将其操作数转换为目标类型以及赋值。
根据标准,此许可证不允许实现更改仅复制的浮点值,包括通过赋值到不执行算术运算的相同类型。
评论
long double
FLT_EVAL_METHOD == 2
double
FLT_EVAL_METHOD == 0
你写的相当于
float a = 1.0;
float b = 1.0;
int equal = a == b;
(就 C 标准而言,使用指针不会改变任何内容)。因此,对于变量 ,以某种计算格式进行解释(取决于 ,请参阅 ISO C17 5.2.4.2.2p9),然后转换为 并存储在 中。同上。作为一般规则,除非明确说明,否则存储/读取值不得更改它们(例如,ISO C17 标准在 6.2.6.2p3 中明确规定,在支持负整数零的实现中,负零在存储时可能会变成正常零)。a
1.0
F
FLT_EVAL_METHOD
float
a
b
要回答这个问题,首先考虑将常量(作为字符串)转换为两行。ISO C17 在 6.4.4.2p5 中说:“相同源形式的所有浮点常量都应转换为具有相同值的相同内部格式。因此,在这两种情况下,您将获得相同的值(在评估格式中)。但请注意,如果你分别有 和,你可能会得到不同的值(不太可能,特别是因为它完全可以表示并且足够简单,但 C 标准并不禁止)。1.0
F
F
1.0
1.00
1.0
然后考虑将获得的值转换为 。ISO C17 6.3.1.5p1 说:“当实数浮动类型的值转换为实数浮动类型时,如果被转换的值可以准确地用新类型表示,则它是不变的。如果(在示例中使用)转换为 1,则值 1 就是这种情况,因此在本例中为 1。但是,如果转换为其他值,则不能在 a 中表示,我认为这可能是 0(C 标准不要求将某些值转换为某种类型总是产生相同的结果,并且特别注意,当舍入模式更改时,情况并非如此)。float
1.0
equal
1.0
float
equal
评论
1.0
具体来说,是最整数,所有尾数位都清晰,所以任何四舍五入都无法改变它。FPU 不会随机破坏数字,它们只是有时会引入舍入以低于临时精度(或者实际上,保持比 C 规则要求的精度更高的精度,除非您使用 或类似的东西来代替默认值。另请参阅 randomascii.wordpress.com/2012/03/21/... 和 randomascii.wordpress.com/2012/02/25/...gcc -ffloat-store
gcc -std=c11
gnu11
long double
FLT_EVAL_METHOD == 2
0