使用intel编译器在Windows和Linux之间的性能差异:查看程序集

Ins*_*oop 65 c++ assembly x86-64 icc intel-vtune

我在Windows和Linux(x86-64)上运行程序.它使用相同的编译器(Intel Parallel Studio XE 2017)编译,具有相同的选项,Windows版本比Linux版本快3倍.罪魁祸首是对std :: erf的调用,在两种情况下都在英特尔数学库中解析(默认情况下,它在Windows上动态链接,在Linux上静态链接,但在Linux上使用动态链接可以提供相同的性能).

这是一个重现问题的简单程序.

#include <cmath>
#include <cstdio>

int main() {
  int n = 100000000;
  float sum = 1.0f;

  for (int k = 0; k < n; k++) {
    sum += std::erf(sum);
  }

  std::printf("%7.2f\n", sum);
}
Run Code Online (Sandbox Code Playgroud)

当我使用vTune分析这个程序时,我发现Windows和Linux版本之间的程序集有点不同.这是Windows上的调用站点(循环)

Block 3:
"vmovaps xmm0, xmm6"
call 0x1400023e0 <erff>
Block 4:
inc ebx
"vaddss xmm6, xmm6, xmm0"
"cmp ebx, 0x5f5e100"
jl 0x14000103f <Block 3>
Run Code Online (Sandbox Code Playgroud)

并在Windows上调用erf函数的开头

Block 1:
push rbp
"sub rsp, 0x40"
"lea rbp, ptr [rsp+0x20]"
"lea rcx, ptr [rip-0xa6c81]"
"movd edx, xmm0"
"movups xmmword ptr [rbp+0x10], xmm6"
"movss dword ptr [rbp+0x30], xmm0"
"mov eax, edx"
"and edx, 0x7fffffff"
"and eax, 0x80000000"
"add eax, 0x3f800000"
"mov dword ptr [rbp], eax"
"movss xmm6, dword ptr [rbp]"
"cmp edx, 0x7f800000"
...
Run Code Online (Sandbox Code Playgroud)

在Linux上,代码有点不同.呼叫站点是:

Block 3
"vmovaps %xmm1, %xmm0"
"vmovssl  %xmm1, (%rsp)"
callq  0x400bc0 <erff>
Block 4
inc %r12d
"vmovssl  (%rsp), %xmm1"
"vaddss %xmm0, %xmm1, %xmm1"   <-------- hotspot here
"cmp $0x5f5e100, %r12d"
jl 0x400b6b <Block 3>
Run Code Online (Sandbox Code Playgroud)

并且被调用函数(erf)的开头是:

"movd %xmm0, %edx"
"movssl  %xmm0, -0x10(%rsp)"   <-------- hotspot here
"mov %edx, %eax"
"and $0x7fffffff, %edx"
"and $0x80000000, %eax"
"add $0x3f800000, %eax"
"movl  %eax, -0x18(%rsp)"
"movssl  -0x18(%rsp), %xmm0"
"cmp $0x7f800000, %edx"
jnl 0x400dac <Block 8>
...
Run Code Online (Sandbox Code Playgroud)

我已经展示了Linux上丢失时间的2个点.

有没有人理解组装足以解释我2代码的区别以及为什么Linux版本慢3倍?

chi*_*ill 42

在这两种情况下,根据Windows和GNU/Linux上的相应调用约定,参数和结果在寄存器中传递.

在GNU/Linux变体中,xmm1用于累积总和.由于它是一个call-clobbered寄存器(也称为调用者保存),因此在每次调用时都会在调用者的堆栈帧中存储(和恢复).

在Windows变体中,xmm6用于累积总和.该寄存器在Windows调用约定中被调用保存(但不在GNU/Linux中).

因此,总之,GNU/Linux版本保存/恢复xmm0(在被调用者[1]中)和xmm1(在调用者中),而Windows版本仅保存/恢复xmm6(在被调用者中).

[1]需要考虑std::errf找出原因.

  • 实际上,只有在编译器无法看到定义的外部调用时才需要遵守ABI.否则(当它可以看到被调用者的定义时)它可以执行它喜欢的任何转换,它不会改变定义良好的代码的结果,包括内联或使用自定义调用约定. (10认同)
  • @chill:没有必要知道所有的呼叫站点.当编译器可以从外部访问(并非所有已知的调用站点)并且以可以受益于不同调用约定(或程序间常量传播等)的方式在本地使用时,编译器可以(并且gcc确实)发出函数的多个版本. ). (6认同)
  • 编译器*总是*尊重ABI,只是不同的ABI以不同的方式定义调用者和被调用者保存的寄存器集. (2认同)