浮点舍入为 80 位寄存器和 64 位双精度寄存器提供不同的结果:ill-formed 代码或 gcc/clang 错误?
floating point rounding giving different results for 80-bit register and 64-bit double: ill-formed code or gcc/clang bug?
下面给出的代码显示了不同的结果,具体取决于 -O 或 -fno-inline 标志。 g++ 10.1 和 10.2 以及 x86 上的 clang++ 10 的结果相同(奇怪)。这是因为代码是 ill-formed 还是这是一个真正的错误?
每当它的 nakshatra (double) 字段 >= 27.0 时,应该设置 Nakshatra 构造函数中的“无效”标志。但是,当通过 Nakshatra(Nirayana_Longitude{360.0}) 初始化时,该标志未设置,即使缩放后的值正好变为 27.0。我假设原因是 360.0 的参数在缩放后变为 26.9999999999999990008(原始 0x4003d7ffffffffffffdc0)在 80 位内部寄存器中,它 < 27.0,但是,作为 64 位双精度存储,变为 27.0。不过,这种行为看起来很奇怪:同一个星座似乎同时 <27.0 和 >= 27.0。它应该是这样吗?
这是预期的行为吗,因为我的代码包含 UB 或其他 ill-formed?还是编译器错误?
要重现的最少代码(两个 .cpp 文件 + 一个 header,无法重现):
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
(或 ./a.exe 对于 Windows/msys2)
实际输出:
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-inline 的任何 -O 一样。使用 g++.exe(Rev5,由 MSYS2 项目构建)10.2.0 (Windows 7) 和 g++ 10.1 在 Ubuntu 18.04.5LTS 以及 clang 10 (Linux) 中复制。 msys2 下的 Clang 11 似乎工作正常(未广泛验证)。
此外,如果将所有代码合并到一个文件中,我也无法重现此行为。我还有 failed to reproduce this in wandbox,甚至使用 gcc 10.1,所以也许用于重现此行为的 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 [=17=]x0,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 [=17=]x1,0x8(%ecx) ; set invalid=true
0x00401760 <+44>: ret [=17=]x8
UPDATE:在阅读 another question 的答案后,我发现使用 -mfpmath=sse -msse2 -ffp-contract=off
或 -ffloat-store
进行编译修复了错误。尽管如此,问题仍然存在:默认情况下,g++ 和 clang 10 是否偏离优化 32 位 x86 的 C++ 标准,或者 C++ 标准是否允许这种行为? Double 同时 <27.0 和 >=27.0 在我看来不一致。
如果没有 -ffloat-store
,面向 x87 的 GCC 确实违反了标准:它甚至在语句之间保持值不四舍五入。 (-mfpmath=387
是 -m32
的默认值)。在 ISO C++ 中,像 double x = y;
这样的赋值应该四舍五入到实际的双精度值,并且可能还会传递一个函数 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 的实际行为。
下面给出的代码显示了不同的结果,具体取决于 -O 或 -fno-inline 标志。 g++ 10.1 和 10.2 以及 x86 上的 clang++ 10 的结果相同(奇怪)。这是因为代码是 ill-formed 还是这是一个真正的错误?
每当它的 nakshatra (double) 字段 >= 27.0 时,应该设置 Nakshatra 构造函数中的“无效”标志。但是,当通过 Nakshatra(Nirayana_Longitude{360.0}) 初始化时,该标志未设置,即使缩放后的值正好变为 27.0。我假设原因是 360.0 的参数在缩放后变为 26.9999999999999990008(原始 0x4003d7ffffffffffffdc0)在 80 位内部寄存器中,它 < 27.0,但是,作为 64 位双精度存储,变为 27.0。不过,这种行为看起来很奇怪:同一个星座似乎同时 <27.0 和 >= 27.0。它应该是这样吗?
这是预期的行为吗,因为我的代码包含 UB 或其他 ill-formed?还是编译器错误?
要重现的最少代码(两个 .cpp 文件 + 一个 header,无法重现):
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
(或 ./a.exe 对于 Windows/msys2)
实际输出:
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-inline 的任何 -O 一样。使用 g++.exe(Rev5,由 MSYS2 项目构建)10.2.0 (Windows 7) 和 g++ 10.1 在 Ubuntu 18.04.5LTS 以及 clang 10 (Linux) 中复制。 msys2 下的 Clang 11 似乎工作正常(未广泛验证)。
此外,如果将所有代码合并到一个文件中,我也无法重现此行为。我还有 failed to reproduce this in wandbox,甚至使用 gcc 10.1,所以也许用于重现此行为的 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 [=17=]x0,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 [=17=]x1,0x8(%ecx) ; set invalid=true
0x00401760 <+44>: ret [=17=]x8
UPDATE:在阅读 another question 的答案后,我发现使用 -mfpmath=sse -msse2 -ffp-contract=off
或 -ffloat-store
进行编译修复了错误。尽管如此,问题仍然存在:默认情况下,g++ 和 clang 10 是否偏离优化 32 位 x86 的 C++ 标准,或者 C++ 标准是否允许这种行为? Double 同时 <27.0 和 >=27.0 在我看来不一致。
如果没有 -ffloat-store
,面向 x87 的 GCC 确实违反了标准:它甚至在语句之间保持值不四舍五入。 (-mfpmath=387
是 -m32
的默认值)。在 ISO C++ 中,像 double x = y;
这样的赋值应该四舍五入到实际的双精度值,并且可能还会传递一个函数 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 的实际行为。