为什么 MinGW GCC 对 atan2、cos、exp 和 sin 使用 x87 80 位 FP 库代码?

Why does MinGW GCC use x87 80bit FP library code for atan2, cos, exp & sin?

提问人:Martin Brown 提问时间:10/26/2023 最后编辑:Martin Brown 更新时间:10/28/2023 访问量:68

问:

我在从英特尔 2023 和 MSC Visual C++ 2022 移植工作数字代码时遇到了一个奇怪的问题。 使用 GCC 编译的代码非常准确(过于准确),因为一些库调用以完整的 80 位浮点精度工作 - 特别是 sqrt、sin 和 cos。我可以通过使用 TUI 跟踪 gdb 的库调用来反汇编库代码执行来验证这一点。

它也出现在基准时间中,因为 x87 atan2、cos、exp 和 sin 都是 ~100 个周期,sqrt 是 ~80 个周期。SSE/AVX2 代码的相应时序低于 50 个,大多在 20-30 个周期左右。

奇怪的是,tan、atan 使用 AVX2 编译的。但是 cos、sin、sqrt 和 atan2 在 GCC 系统库中使用了传统的 x87 代码。我已经在 32 位端口和 64 位版本上都尝试过,并且在两者中都遇到了同样的问题。我是 GCC 的新手,所以我可能忽略了一些东西。我在 Windows 上使用默认的 MinGW 端口版本 13.1.0 (MinGW-W64 i686-ucrt-posix-dwarf),它可能有自己的特点。

顺便说一句,我刚刚注意到 MSC 2022 有时会对 x87 sqrt 进行编码,即使启用了所有 gofaster 优化和 AVX2 代码,因为这也是我以前没有注意到的基准时间中的异常值。英特尔将其编译为原生 sqrtsd,因此速度非常快。我回到了 MSC x86 的内联汇编程序,以确认 x87 trig 指令的基准时间。

编译器选项包括:
gcc -c -O3 -Ofast -march=native -mavx2 -mfpmath=sse benchmark.cpp

链接采用 GCC 默认系统库的任何内容,这似乎是问题所在 - 我的代码或任何内联生成 SSE 或 AVX2 代码的系统代码,因此 tan 和 atan 没问题,但任何生成库调用的系统都以 87 位精度执行超越函数的 x80 指令。我认为它可能与我能找到的最接近的线程有关:

为什么 MinGW-w64 浮点精度取决于 winpthreads 版本?

我想强制它使用完全使用 AVX2 或 SSE2 代码的不同 FP 库,或者使用 和 之类的东西重新编译现有库。在这里,速度比精确的标准合规性更重要。这涉及到很多三角函数。-march=native -mavx2-Ofast

我完全有可能在 GCC 中链接了错误的默认库。我有一种基于合并 BSD trig 库函数源代码的工作方法,但这并不优雅。

如果有人想在他们的系统上尝试,我可以发布基准测试的代码示例,但它会比这里看起来更长一些。我希望有人已经知道答案了......

这些是使用 AVX2 代码生成 MSC 2022 与 GCC 13.0.1 进行原始三角操作的机器周期基准。它在英特尔 i5-12600 上运行,并在两个编译器上进行了最大程度的优化。

地中海安卡 海湾合作委员会
阿坦 12 13
阿坦2 27 122
日志 11 76
经验值 11 136
14 115
因为 13 117
正余弦 19 127
18 20

使用 x87 代码的那些与它们应有的位置相比突出了一英里 ~+100 个周期。

我想在编译时使用正确的浮点库获取代码-O3 -Ofast -mavx2

这是显示我的问题的最小示例代码,以及 GDB 中反汇编的快照,它显示了 sin 如何变成 x87、FSIN 等。测试你是否受到类似折磨的另一种方法是在你最喜欢的分析器中对 sin(x) 和 tan(x) 进行基准测试,如果 tan 时间是 ~20 个周期,而 sin 是 ~40,那么你就没问题了(tan 比 sin 快 ~2 倍)。任何 trig 函数 100+ 周期,它是慢速 x87 代码。

#include "stdio.h"
#include "math.h"

// Toy use of sin & tan to see if they compile using SSE2 or x87

int main(int argc, char* argv[]){
double x, y;
if (argc>1) x = atof(argv[1]); else x = 3.1415926535/2;
y = sin(x);
printf("sin of %g is %18.10g\n", x, y);
x = x/2;
y = tan(x);
printf("tan of %g is %18.10g\n", x, y);
}

使用 GDB 中的 x87 代码反汇编 sin 例程(tan 是可以的)

 <__sinl_internal>        fldt   (%rdx)                                                                
 <__sinl_internal+2>      fsin                                                                       
 <__sinl_internal+4>      fnstsw %ax                                                                
 <__sinl_internal+6>      test   $0x400,%eax                                                        
 <__sinl_internal+11>     jne    0x7ff656332e6b                              
 <__sinl_internal+13>     mov    %rcx,%rax                                                          
 <__sinl_internal+16>     movq   $0x0,0x8(%rcx)                                                     
 <__sinl_internal+24>     fstpt  (%rcx)                                                             
 <__sinl_internal+26>     ret              

我现在相当确信@emacsdrivesmenuts是对的,我必须在 Mingw 下使用正确的 FP 优化重建默认系统数学库来解决这个问题,但我不知道该怎么做!

感谢您的启发!

GCC 浮点 MINGW AVX x87

评论

1赞 chux - Reinstate Monica 10/26/2023
“在这里,速度比精确地遵守标准更重要。” -->好的。请量化。可以容忍多少不精确度或需要多少最低速度。否则使用 atan2f()、cosf()、expf() 和 sinf()'。float versions:
0赞 Peter Cordes 10/26/2023
GCC 应该像最坏的情况一样内联,如果你不启用 (godbolt.org/z/6aW4n57ME),在 NaN 的情况下,会有一个比较/分支。(作为 或 的一部分打开。32 位调用约定强制 FP return in ,如果函数无法内联,这会使速度变慢。其他函数不会内联,32 位 MinGW 可能旨在与库中没有 SSE2 的旧机器兼容?我注意到您正在使用 i686 GCC;如果您想要数字性能,64 位代码通常更好。sqrt()sqrtsd-fno-math-errno-ffast-math-Ofastst0
1赞 emacs drives me nuts 10/26/2023
通常,这些函数由 实现,而 等选项不是 multilib 选项。这意味着此类选项对 multilib 选择没有影响。因此,您必须调查如何构建,并可能使用调整后的选项构建自己的版本。libm-O3libm
2赞 chux - Reinstate Monica 10/27/2023
@MartinBrown 由于答案尚未到来,因此在更改方面有更多的自由度,从而增加了问题。(如有疑问,请附加而不是更改)IAC,附加 50 个 LOC 示例将改进帖子。一个最小的可重复的例子,其他人可以计时,那就更好了。
1赞 Martin Brown 10/28/2023
@chux,我阅读了最小可重现的示例指南,并决定继续关注 atan2,cos,exp sin 在 Mingw 构建中是 x87 且速度较慢的这一特定问题。我将为不同编译器上具有奇怪计时行为的代码启动另一个线程。我有一些非常有趣的要分享,并非常感谢反馈。我需要一段时间来构建显示最有趣行为的最小代码示例。顺便说一句,感谢您的浮动想法,它可能会提供我忽略的加速。三次求解器需要处理较小的浮点指数。

答: 暂无答案