存储/加载/移动后浮点数相等

Equality of floating point numbers after storing/loading/moving

提问人:Kai Schmidt 提问时间:5/31/2023 更新时间:6/2/2023 访问量:157

问:

我和一位同事在比较两个未经数学运算的浮点数时会发生什么存在分歧。也就是说,这些数字可能已经在内存和/或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;

有没有情况不会?equal1

浮点 CPU 架构 浮点精度

评论

3赞 Erik Eidt 5/31/2023
我不知道任何架构的位模式可能会改变,不过,你有没有考虑过 NaN?一般来说,当 a = NaN 时,那么 a != a,我认为,因为浮点的性质相等。如果比较位模式,即使 NaN 也应该看起来相等。
2赞 Peter Cordes 5/31/2023
1.0具体来说,是最整数,所有尾数位都清晰,所以任何四舍五入都无法改变它。FPU 不会随机破坏数字,它们只是有时会引入舍入以低于临时精度(或者实际上,保持比 C 规则要求的精度更高的精度,除非您使用 或类似的东西来代替默认值。另请参阅 randomascii.wordpress.com/2012/03/21/... 和 randomascii.wordpress.com/2012/02/25/...gcc -ffloat-storegcc -std=c11gnu11
1赞 Peter Cordes 5/31/2023
此外,据我所知,唯一以令人惊讶的方式处理 FP 数据的主流 FPU 架构是 x87,它不是现代的。现代 x86 CPU 都支持 SSE2,x86-64 将其用于 FP 数学运算,但支持 80 位类型的 ISA 除外。大多数现代编译器也被配置为在 32 位版本中使用 SSE2 进行标量数学运算,假设它们不需要支持 2000 年代初之前的计算机。 x87 通常是 (en.cppreference.com/w/cpp/types/climits/FLT_EVAL_METHOD) 或草率的版本,而大多数其他 ISA 是long doubleFLT_EVAL_METHOD == 20
2赞 chux - Reinstate Monica 5/31/2023
“有没有任何情况,相等不会是1?” --> 没有。然而,您使用的是一个简单的案例。
1赞 chtz 5/31/2023
相关(不是真正的重复):stackoverflow.com/questions/59710531/...

答:

1赞 Sam Mason 6/1/2023 #1

我假设你指的是IEEE754二进制浮点数,因为现在大多数传统 CPU 都会使用这些浮点数。

在这种情况下,是的,只是移动这些值不会导致值发生变化。在对值进行操作时,听起来您的直觉可能需要澄清。具体来说,任何操作的结果都应该与FPU以完全(即任意)精度执行操作,然后仅将结果四舍五入以适合适当的(例如32位二进制浮点数)相同。因此,对于实现 IEEE754 浮点数的 CPU,发生的任何舍入都是确定性的——这就是语句背后的原因。0.1 + 0.2 != 0.3

请注意,通过使用 C,您已经通过两种机制使它复杂化了;1. C 不需要IEEE754语义;2. C 将自动在 和 值之间转换(即在通用架构上IEEE754 32 位和 64 位二进制浮点数)。这两个功能可能意味着代码可能无法达到幼稚读者的期望。floatdouble

作为一个更完整的例子,我将以下代码放入 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编译器不能这么宽松。ba

可以看出,代码编译为始终返回的代码(通过设置 ),而代码仅对传递的参数进行比较,以便在少量代码中正确处理 NaN。explicit_nan1EAXimplicit_nan

评论

0赞 Peter Cordes 6/2/2023
C 指定在表达式边界处向实际 C 类型(或)进行舍入(如果有过于精确的临时 randomascii.wordpress.com/2012/03/21/...),但实际上编译器对此很草率。例如,针对 32 位 x86 和 x87 FP 数学的 GCC 在优化时会在语句之间保持额外的精度,除非您使用 or 或 or 其他(而不是默认)。所以这个答案的假设有点乐观。floatdoublegcc -ffloat-storegcc -std=c99c11gnu99
0赞 Peter Cordes 6/2/2023
但是,是的,如果数据已经可以表示为 .如果 or 还不是整数,则往返 or to 可能会更改该值。floatdoublelong doublefloat
0赞 Sam Mason 6/2/2023
@PeterCordes对不起,我已经有一段时间没有考虑 x86 FPU 的 80 位浮动了!我也将 OP 的“现代架构”评论解释为 x86-64/ARM64(也许还有 RISC-V/Power)而不是非 754 DSP,但这个问题有些模糊
0赞 Peter Cordes 6/2/2023
是的,正如问题下的评论中所讨论的那样,所有现代架构都有 FLT_EVAL_METHOD == 0,使用 SSE2 之类的东西进行标量数学,而不是 x87,这使得它几乎完全没有问题,除非在 C 源代码中混合不同类型的。但我认为 OP 和他们的同事听说过的那种影响是由 x87 的 FLT_EVAL_METHOD == 2 引起的。
2赞 Eric Postpischil 6/2/2023 #2

你的同事的信念可能起源于 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。floatd = a + b - c;doubledfloatdoubleabcfloatfloatdouble

这与以下事实没有什么不同:给定所有变量,使用算术计算表达式,并且可能产生与仅使用算术计算的结果不同的结果。例如,由于使用了算术,with = 100, = 3 将产生 100。如果使用算术,将得到 44 (300−256),除以 3 会得到 14。因此,这不仅仅是一个浮点问题;这是一个关于编程语言如何计算表达式的问题。unsigned charintunsigned charabd = 3 * a / b ;intunsigned char3 * a

C 和 C++ 标准还要求将其操作数转换为目标类型以及赋值。

根据标准,此许可证不允许实现更改仅复制的浮点值,包括通过赋值到不执行算术运算的相同类型。

评论

0赞 Peter Cordes 6/3/2023
也可以使用算术进行评估;这是 (en.cppreference.com/w/cpp/types/climits/FLT_EVAL_METHOD),如果 x87 精度控制位保留为默认的全 64 位尾数精度,而不是四舍五入到 53 位尾数,则在 32 位 x86 上得到的结果是。randomascii.wordpress.com/2012/03/21/......包含有关 x87 的历史 MSVC 行为的一些信息。幸运的是,大多数现代 ISA(包括 x86-64)都可以有效地做到这一点(eval 作为 C 类型)。long doubleFLT_EVAL_METHOD == 2doubleFLT_EVAL_METHOD == 0
3赞 vinc17 6/2/2023 #3

你写的相当于

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 中明确规定,在支持负整数零的实现中,负零在存储时可能会变成正常零)。a1.0FFLT_EVAL_METHODfloatab

要回答这个问题,首先考虑将常量(作为字符串)转换为两行。ISO C17 在 6.4.4.2p5 中说:“相同源形式的所有浮点常量都应转换为具有相同值的相同内部格式。因此,在这两种情况下,您将获得相同的值(在评估格式中)。但请注意,如果你分别有 和,你可能会得到不同的值(不太可能,特别是因为它完全可以表示并且足够简单,但 C 标准并不禁止)。1.0FF1.01.001.0

然后考虑将获得的值转换为 。ISO C17 6.3.1.5p1 说:“当实数浮动类型的值转换为实数浮动类型时,如果被转换的值可以准确地用新类型表示,则它是不变的。如果(在示例中使用)转换为 1,则值 1 就是这种情况,因此在本例中为 1。但是,如果转换为其他值,则不能在 a 中表示,我认为这可能是 0(C 标准不要求将某些值转换为某种类型总是产生相同的结果,并且特别注意,当舍入模式更改时,情况并非如此)。float1.0equal1.0floatequal

评论

0赞 Kai Schmidt 6/2/2023
谢谢,这触及了我想知道的核心。