首页 > 解决方案 > 比较 int = floor(sqrt(...)) 的 Fortran 和 C++ 汇编器

问题描述

我在 Fortran 和 C++ 中分别实现了一个函数:

#include <math.h>

void dbl_sqrt_c(double *x, double *y){
   *y = sqrt(*x - 1.0);
   return;
}
pure subroutine my_dbl_sqrt(x,y) bind(c, name="dbl_sqrt_fort")
   USE, INTRINSIC :: ISO_C_BINDING
   implicit none
   real(kind=c_double), intent(in)  :: x
   real(kind=c_double), intent(out) :: y

   y = sqrt(x - 1d0)
end subroutine my_dbl_sqrt

我在编译器资源管理器中比较了它们:

Fortran:https
://godbolt.org/z/froz4rx97 C++:https ://godbolt.org/z/45aex99Yz

而我阅读汇编程序的方式,它们基本上做同样的事情,但 C++ 会检查 sqrt 的参数是否为负,而 Fortran 不会。我使用谷歌基准比较了它们的性能,但它们非常匹配:

--------------------------------------------------------
Benchmark              Time             CPU   Iterations
--------------------------------------------------------
bm_dbl_c/8          2.07 ns         2.07 ns    335965892
bm_dbl_fort/8       2.06 ns         2.06 ns    338643106

这是有趣的部分。如果我把它变成基于整数的函数:

void int_sqrt_c(int *x, int *y){
   *y = floor(sqrt(*x - 1.0));
   return;
}

pure subroutine my_int_root(x,y) bind(c, name="int_sqrt_fort")
   USE, INTRINSIC :: ISO_C_BINDING
   implicit none
   integer(kind=c_int), intent(in)  :: x
   integer(kind=c_int), intent(out) :: y

   y = floor(sqrt(x - 1d0))
end subroutine my_int_root

然后这是他们开始分歧的地方:

--------------------------------------------------------
Benchmark              Time             CPU   Iterations
--------------------------------------------------------
bm_int_c/8          3.05 ns         3.05 ns    229239198
bm_int_fort/8       2.13 ns         2.13 ns    328933185

Fortran 代码似乎并没有因为这种变化而明显变慢,但 C++ 代码却减慢了 50%。这似乎相当大。这些是程序集:

Fortran:https
://godbolt.org/z/axqqrc5E1 C++:https ://godbolt.org/z/h7K75oKbn

Fortran 程序集看起来很简单。它只是增加了和之间的转换doubleint并没有太多其他的东西,但 C++ 似乎做得更多,我不完全理解。

为什么 C++ 汇编器要复杂得多?如何改进 C++ 代码以实现匹配的性能?

标签: c++performanceassemblyx86-64

解决方案


TL;DR:你被糟糕的默认设置和与过时机器的兼容性所困扰:糟糕的默认设置是errno用于浮点计算的 gcc 设置(尽管 C 语言不需要这样做),以及与没有任何设备的 x86 机器的兼容性比 SSE2 更好的 SSE 指令。如果您想要体面的代码生成,请添加-fno-math-errno -msse4编译器标志

包含浮点硬件的现代处理器架构通常将平方根计算作为基本操作(指令)提供,如果平方根指令的操作数超出范围(负数),则会在浮点环境中设置错误标志。另一方面,旧的指令集架构可能没有浮点指令,或者没有硬件加速的平方根指令,因此 C 语言允许实现设置errno超出范围的参数,但errno它是线程本地的内存位置实际上阻止了任何健全的架构errno直接从平方根指令设置。为了获得不错的性能,gcc 通过调用硬件指令 ( ) 内联平方根计算sqrtsd,但要设置errno,它单独检查参数的符号,只有在参数为负的情况下才调用库函数,所以库函数可以设置errno。是的,这很疯狂,但这反过来又是课程的标准。-fno-math-errno您可以通过设置编译器标志来避免这种没人需要或想要的脑损伤。

相对较新的 x86-64 处理器具有比 AMD 首次开发的原始 x86-64 中更多的指令(​​其中仅包括 SSE2 向量/浮点指令)。添加的指令中有浮点/整数转换指令,允许受控舍入/截断,因此不必在软件中实现。您可以通过指定支持这些指令的目标来让 gcc 使用这些新指令,例如使用-msse4编译器标志。请注意,如果生成的程序在不支持这些指令的目标上运行,这将导致生成的程序出错,因此生成的程序的可移植性会降低(尽管它不会明显降低源代码的可移植性)。


推荐阅读