打破int和float之间严格混叠的实际后果

Practical consequences of breaking strict aliasing between int and float

本文关键字:后果 int float 之间 打破      更新时间:2023-10-16

我曾经在我必须维护的一个项目中遇到一个非常微妙的错误。基本上它是这样做的:

union Value {
    int64_t int64;
    int32_t int32[2];
    int16_t shorts[4];
    int8_t chars[8];
    float floats[2];
    double float64;
};
Value v;
// in one place (not sure about exact code, it could be just memcpy):
v.shorts[0] = <some short value>;
v.shorts[1] = <some other short value>;
// in another place:
float f = v.floats[0];

现在,就标准而言,这只是UB。在实践中,这可能意味着任何事情,但我很难想象一个合理的实现会导致上面的代码引发第三次世界大战或瓦解我的PC。在现实生活中,我只能想象发生两件事:

  1. 编译器可能在优化时搞砸了一些事情,没有意识到它在这里处理的是相同的内存。在这种情况下不太可能,因为写和读发生在完全不同的地方。

  2. 没有什么不好的事情发生,float值只是逐位读取。

在实践中,

几乎都是,除了一次。在大约100-150个输入文件上运行用MSVC 2010在发布模式下编译的程序后,在其中一个文件中,它生成了一个不正确的值,与根据常识应该的值相差了一个位。这也是很重要的一点,所以不是1.5,而是117.9。我能够追踪到准确的读取,并且在修改代码以遵守严格的混叠规则之后,一切都很好。

现在的问题是,纯粹从低级别的角度来看,是什么导致了这种情况?CPU处理浮点值的一些特性?硬件缓存细节?编译器怪癖吗?为什么只有一个值是错误的?

硬件是老式的2核64位英特尔CPU,运行32位Windows 7,如果这对你有帮助的话。该程序是一个单线程控制台应用程序,没什么特别的。这个问题是100%可重复的,相同的输入文件总是产生相同的输出,并且总是相同的值出错。

从标准的角度来看,代码v.shorts[0] = something;接受类型为"short*"的指针值,加上零,并使用结果指针来存储一个值。我认为C89的作者希望在这种情况下,混叠能够发挥作用的质量实现能够识别混叠,但在标准的字面上没有任何要求。请注意,当规则包含在C89中时,编译器只期望在非常局部的级别上应用它们;此外,这些规则通常不会给程序员带来严重的问题,除非它们在更深远的层面上得到应用。不幸的是,一些编译器正在积极地寻求尽可能地扩展规则的范围。

如果要将每个数组放在联合中的单独结构中,然后执行如下操作:

v.floats.arr[0] = value;
v.floats.arr[1] = value;
v.floats = v.floats; // Compiler knows that float* may alter float members,
                     // and that writing member of union may alter other
                     // members

…现在用其他的

编译器应该能够识别对v.floats的赋值需要不生成任何代码,但符合标准的编译器必须仍然认为它是联合的其他成员可能已被更改的适当通知。注意,从6.2版本开始,这种模式在gcc中似乎并不可靠;在某些情况下,当不需要赋值来生成任何代码时,编译器将完全忽略赋值——包括它的混叠含义。我看不出有任何理由去向后处理gcc的错误行为,但是——只要使用-fno-strict-aliasing就可以了,除非或直到gcc的混叠逻辑得到修复。