下面给出的代码根据-O或-fno内联标志显示不同的结果。x86上g++10.1和10.2以及clang++10的结果相同(奇怪(。这是因为代码格式不正确,还是因为这是一个真正的错误?
";无效";Nakshatra构造函数中的标志应在其Nakshatra(double(字段>=时设置27.0。但是,当通过Nakshatra(Nirayana_Longitude{360.0}(初始化时,即使缩放后的值恰好变为27.0,也不会设置该标志。我假设原因是缩放后360.0的自变量在80位内部寄存器中变为26.9999999999990008(原始0x4003d7fffffffdc0(,这是<27.0,但是,被存储为64位双精度,变为27.0。尽管如此,这种行为看起来很奇怪:同一个nakshatra似乎同时<27.0和>=27.0.它是应该的样子吗?
这是因为我的代码包含UB或其他格式错误而导致的预期行为吗?或者是编译器错误
要复制的最小代码(两个.cpp文件+一个标头,不能用更少的代码复制(:
main.cpp:
#include "nakshatra.h"
#include <iostream>
#include <iomanip>
int main() {
Nakshatra n{Nirayana_Longitude{360.0}};
std::cout << std::fixed << std::setprecision(40) << std::boolalpha;
std::cout << n.nakshatra << "n";
std::cout << "invalid (should be true): " << n.invalid << "n";
std::cout << "n.nakshatra >= 27.0: " << (n.nakshatra >= 27.0) << "n";
}
nakshatra.h:
struct Nirayana_Longitude {
double longitude;
};
class Nakshatra
{
public:
double nakshatra;
bool invalid = false;
Nakshatra(double nakshatra_value) : nakshatra(nakshatra_value) {
if (nakshatra < 0.0 || nakshatra >= 27.0) {
invalid = true;
}
}
Nakshatra(Nirayana_Longitude longitude);
};
nakshatra.cpp:
#include "nakshatra.h"
// this constructor has to be implemented in a separate .cpp file to reproduce the bug,
// moving it to ether nakshatra.h or main.c fixes the problem (perhaps due to
// compiler removing the relevant code from runtime).
Nakshatra::Nakshatra(Nirayana_Longitude longitude) : Nakshatra(longitude.longitude * (27.0 / 360.0))
{
}
要编译并运行:$ g++ -O2 main.cpp nakshatra.cpp && ./a.out
或$ clang++ -O2 main.cpp nakshatra.cpp && ./a.out
(或适用于Windows/msys 2的./a.exe(
实际输出:
27.0000000000000000000000000000000000000000
invalid (should be true): false
n.nakshatra >= 27.0: true
预期输出:
27.0000000000000000000000000000000000000000
invalid (should be true): true
n.nakshatra >= 27.0: true
使用-O、-Og、-O1或-O2进行编译体现了这种奇怪的行为,不使用-O进行编译可以很好地工作,就像任何使用-fno内联的-O一样。在Ubuntu 18.04.5LTS中使用g++.exe(Rev5,由MSYS2项目构建(10.2.0(Windows 7(和g++10.1以及clang 10(Linux(进行复制。msys2下的Clang 11似乎运行良好(没有广泛验证(。
此外,如果将所有代码组合到一个文件中,我也无法重现这种行为。此外,即使使用gcc 10.1,我也未能在wandbox中重现这一行为,因此可能用于重现这种行为的CPU是相关的:Intel Core i5-660(3.33GHz(。不幸的是,否则优秀的编译器exporer不支持多个编译单元,因此无法在那里重现。
为了完整起见,这是显示奇怪行为时生成的汇编代码的一个示例。
0x00401734 <+0>: fldl 0x404080 ; (27.0/360.0)?
0x0040173a <+6>: fmull 0x4(%esp) ; argument *= (27.0*360.0), giving 26.9999999999999990008 (raw 0x4003d7fffffffffffdc0)
0x0040173e <+10>: fstl (%ecx)
0x00401740 <+12>: movb $0x0,0x8(%ecx) ; set invalid=false
0x00401744 <+16>: fldz
0x00401746 <+18>: fcomip %st(1),%st
0x00401748 <+20>: ja 0x40175a <_ZN9NakshatraC2E18Nirayana_Longitude+38> ; jump if argument < 0.0
0x0040174a <+22>: flds 0x404088 ; 27.0?
0x00401750 <+28>: fxch %st(1)
0x00401752 <+30>: fcomip %st(1),%st
0x00401754 <+32>: fstp %st(0)
0x00401756 <+34>: jb 0x401760 <_ZN9NakshatraC2E18Nirayana_Longitude+44> ; jump if argument is >= 27.0
0x00401758 <+36>: jmp 0x40175c <_ZN9NakshatraC2E18Nirayana_Longitude+40>
0x0040175a <+38>: fstp %st(0)
0x0040175c <+40>: movb $0x1,0x8(%ecx) ; set invalid=true
0x00401760 <+44>: ret $0x8
UPDATE:在阅读了另一个问题的答案后,我发现使用-mfpmath=sse -msse2 -ffp-contract=off
或-ffloat-store
编译可以修复该错误。然而,问题仍然存在:g++和clang 10是否默认偏离了用于优化32位x86的C++标准,或者C++标准允许这种行为?双是<27.0和>27.0同时在我看来不一致。
如果没有-ffloat-store
,以x87为目标的GCC确实违反了标准:它甚至在语句之间保持值不舍入。(-mfpmath=387
是-m32
的默认值(。像double x = y;
这样的赋值应该在ISO C++中四舍五入到实际的双精度,并且可能还会传递一个函数arg。
因此,我认为您的代码对于ISO C++规则是安全的,即使GCC声称要执行FLT_EVAL_METHOD==2。(https://en.cppreference.com/w/cpp/types/climits/FLT_EVAL_METHOD)
另请参阅https://randomascii.wordpress.com/2012/03/21/intermediate-floating-point-precision/有关实际问题的更多信息,请参阅x86的实际编译器。
https://gcc.gnu.org/wiki/x87note并没有真正提到GCC舍入与ISO C++需要舍入之间的区别,只是描述了GCC的实际行为。