打破int和float之间严格别名的实际后果

时间:2016-10-20 11:06:46

标签: c++ strict-aliasing

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

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。在实践中,这可能意味着什么,但我很难想象一个合理的实施会导致上面的代码开始第三次世界大战或解体我的电脑。在现实生活中,我只能想象两件事情在发生:

  1. 编译器可能会通过优化来搞砸某些东西,而不是意识到它在这里处理相同的内存。在这种情况下,不太可能,因为写入和读取发生在完全不同的地方。

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

  3. 实际上,除了一次之外,几乎始终是案例2。在使用MSVC 2010在发布模式下编译的程序运行大约100-150个输入文件之后,在其中一个文件中生成了一个不正确的值,该值与根据常识的预期值完全不同。这也是一个非常重要的一点,所以我没有1.5,而是117.9。我能够将其追溯到完全准确的读取,并在修复代码以遵守严格的别名规则后,一切正常。

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

    硬件是运行32位Windows 7的旧的2核64位Intel CPU,如果有任何帮助的话。该程序是一个单线程控制台应用程序,没有什么花哨的。问题是100%可重复,相同的输入文件总是产生相同的输出,并且它始终是错误的相同值。

1 个答案:

答案 0 :(得分:0)

从标准的角度来看,代码v.shorts[0] = something;采用类型为&#34; short *&#34;的指针值,添加零,并使用结果指针存储值。我认为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的破坏行为,但是 - 除非或直到gcc的别名逻辑得到修复,否则只需使用-fno-strict-aliasing guilt-free。