我正在研究一种使用 LLVM 编译的语言。只是为了好玩,我想做一些微基准测试。在其中一个循环中,我运行了数百万次正弦/余弦计算。在伪代码中,它看起来像这样:
var x: Double = 0.0
for (i <- 0 to 100 000 000)
x = sin(x)^2 + cos(x)^2
return x.toInteger
如果我使用 LLVM IR 内联汇编计算 sin/cos,格式如下:
%sc = call { double, double } asm "fsincos", "={st(1)},={st},1,~{dirflag},~{fpsr},~{flags}" (double %"res") nounwind
这比分别使用 fsin 和 fcos 而不是 fsincos 更快。但是,它比我分别调用
llvm.sin.f64
和 llvm.cos.f64
内在函数要慢,这些内在函数编译为对 C 数学库函数的调用,至少对于我正在使用的目标设置(启用 SSE 的 x86_64)来说是这样。
LLVM 似乎在单/双精度 FP 之间插入了一些转换——这可能是罪魁祸首。这是为什么?抱歉,我在装配方面是个新手:
.globl main
.align 16, 0x90
.type main,@function
main: # @main
.cfi_startproc
# BB#0: # %loopEntry1
xorps %xmm0, %xmm0
movl $-1, %eax
jmp .LBB44_1
.align 16, 0x90
.LBB44_2: # %then4
# in Loop: Header=BB44_1 Depth=1
movss %xmm0, -4(%rsp)
flds -4(%rsp)
#APP
fsincos
#NO_APP
fstpl -16(%rsp)
fstpl -24(%rsp)
movsd -16(%rsp), %xmm0
mulsd %xmm0, %xmm0
cvtsd2ss %xmm0, %xmm1
movsd -24(%rsp), %xmm0
mulsd %xmm0, %xmm0
cvtsd2ss %xmm0, %xmm0
addss %xmm1, %xmm0
.LBB44_1: # %loop2
# =>This Inner Loop Header: Depth=1
incl %eax
cmpl $99999999, %eax # imm = 0x5F5E0FF
jle .LBB44_2
# BB#3: # %break3
cvttss2si %xmm0, %eax
ret
.Ltmp160:
.size main, .Ltmp160-main
.cfi_endproc
调用 llvm sin/cos 内在函数进行相同的测试:
.globl main
.align 16, 0x90
.type main,@function
main: # @main
.cfi_startproc
# BB#0: # %loopEntry1
pushq %rbx
.Ltmp162:
.cfi_def_cfa_offset 16
subq $16, %rsp
.Ltmp163:
.cfi_def_cfa_offset 32
.Ltmp164:
.cfi_offset %rbx, -16
xorps %xmm0, %xmm0
movl $-1, %ebx
jmp .LBB44_1
.align 16, 0x90
.LBB44_2: # %then4
# in Loop: Header=BB44_1 Depth=1
movsd %xmm0, (%rsp) # 8-byte Spill
callq cos
mulsd %xmm0, %xmm0
movsd %xmm0, 8(%rsp) # 8-byte Spill
movsd (%rsp), %xmm0 # 8-byte Reload
callq sin
mulsd %xmm0, %xmm0
addsd 8(%rsp), %xmm0 # 8-byte Folded Reload
.LBB44_1: # %loop2
# =>This Inner Loop Header: Depth=1
incl %ebx
cmpl $99999999, %ebx # imm = 0x5F5E0FF
jle .LBB44_2
# BB#3: # %break3
cvttsd2si %xmm0, %eax
addq $16, %rsp
popq %rbx
ret
.Ltmp165:
.size main, .Ltmp165-main
.cfi_endproc
您能建议一下 fsincos 的理想装配是什么样子吗?附言。将 -enable-unsafe-fp-math 添加到 llc 会使转换消失并切换到双精度(fldl 等),但速度保持不变。
.globl main
.align 16, 0x90
.type main,@function
main: # @main
.cfi_startproc
# BB#0: # %loopEntry1
xorps %xmm0, %xmm0
movl $-1, %eax
jmp .LBB44_1
.align 16, 0x90
.LBB44_2: # %then4
# in Loop: Header=BB44_1 Depth=1
movsd %xmm0, -8(%rsp)
fldl -8(%rsp)
#APP
fsincos
#NO_APP
fstpl -24(%rsp)
fstpl -16(%rsp)
movsd -24(%rsp), %xmm1
mulsd %xmm1, %xmm1
movsd -16(%rsp), %xmm0
mulsd %xmm0, %xmm0
addsd %xmm1, %xmm0
.LBB44_1: # %loop2
# =>This Inner Loop Header: Depth=1
incl %eax
cmpl $99999999, %eax # imm = 0x5F5E0FF
jle .LBB44_2
# BB#3: # %break3
cvttsd2si %xmm0, %eax
ret
.Ltmp160:
.size main, .Ltmp160-main
.cfi_endproc
太多文档声称像
fsin
或 fsincos
这样的 x87 指令是执行三角函数的最快方法。这些说法往往是错误的。
最快的方法取决于您的CPU。随着 CPU 变得越来越快,旧的硬件触发指令(如
fsin
)已经跟不上步伐。对于某些 CPU,使用正弦多项式近似或其他三角函数的软件函数现在比硬件指令更快。
总而言之,
fsincos
太慢了。
有足够的证据表明 x86-64 平台已经脱离硬件触发。
fsin
。fsin
。 NetBSD 和 OpenBSD 做出了相反的选择:他们的 amd64 libm 确实使用 x87 指令。x86 后端使用
fsin
,但不在其 x86-64 后端中使用。对于 x86-64,SBCL 编译代码调用 libm 中的 sin()。我在 2010 年的 AMD Phenom II X2 560 (3.3 GHz) 上对硬件和软件进行正弦计时。我用这个循环编写了一个 C 程序:
volatile double a, s;
/* ... */
for (i = 0; i < 100000000; i++)
s = sin(a);
我用 sin() 的两种不同实现编译了该程序两次。硬 sin() 使用 x87
fsin
。软 sin() 使用多项式近似。我的 C 编译器 gcc -O2
没有用内联 fsin
替换我的 sin() 调用。
以下是 sin(0.5) 的结果:
$ time race-hard 0.5
0m3.40s real 0m3.40s user 0m0.00s system
$ time race-soft 0.5
0m1.13s real 0m1.15s user 0m0.00s system
这里的 soft sin(0.5) 速度非常快,这个 CPU 执行 soft sin(0.5) 和 soft cos(0.5) 的速度比 x87 还要快
fsin
。
对于罪恶(123):
$ time race-hard 123
0m3.61s real 0m3.62s user 0m0.00s system
$ time race-soft 123
0m3.08s real 0m3.07s user 0m0.01s system
Soft sin(123) 比 Soft sin(0.5) 慢,因为 123 对于多项式来说太大,因此该函数必须减去 2π 的某个倍数。如果我也想要 cos(123),对于 2010 年的 CPU,x87
fsincos
有可能比软 sin(123) 和软 cos(123) 更快。
fsincos
是 x87 FPU 指令,可在 80 位精度浮点数上运行。它不支持自动向量化,但提供比 64 位指令高得多的精度。
sin
和 cos
在 64 位精度的指令上运行,因此较低的精度已经使它们更快。在 FPU(long double
80 位类型)上执行的代码永远不会被自动矢量化,因为它不受支持,但常规 64 位代码(最多 double
类型)会,因此可以使用 SSE/AVX/ 使其速度提高数倍霓虹灯等
仅当您实际需要 80 位精度时才应使用 FPU。说它已经过时并不完全准确。它仅在 99% 的情况下过时,而在 1% 的情况下仍然需要。
要查看编译器生成的
fsin
和 fcos
,请使用 long double
类型(80 位浮点数)和 sinl
cosl
函数。