我正在研究用LLVM编译的语言 . 只是为了好玩,我想做一些微基准测试 . 其中一个,我在一个循环中运行了一百万个sin / cos计算 . 在伪代码中,它看起来像这样:
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的理想组装方式吗? PS . 将-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
1 回答
硬件触发很慢 .
太多文档声称像
fsin
或fsincos
这样的x87指令是执行三角函数的最快方法 . 这些说法经常是错误的 .最快的方法取决于您的CPU . 随着CPU变得更快,像
fsin
这样的旧硬件触发指令没有跟上步伐 . 对于某些CPU,使用正弦或其他触发功能的多项式近似的软件功能现在比硬件指令更快 .简而言之,
fsincos
太慢了 .硬件触发已过时 .
有足够的证据表明x86-64平台已经远离硬件触发 .
amd64喜欢SSE超过x87的浮点数 . 然而,SSE没有像
fsin
这样的x87指令的等价物 .对于amd64,FreeBSD和glibc中的libm实现了sin()和软件中的此类函数,而不是x87 trig . glibc具有optimized x86-64 assembly for sinf()(单精度正弦),具有多项式逼近,而不是x87
fsin
. NetBSD和OpenBSD做出了相反的选择:他们的amd64 libm确实使用了x87指令 .钢铁银行Common Lisp在x86 backend中使用了
fsin
,但在x86-64后端却没有 . 对于x86-64,SBCL编译calls sin() in libm的代码 .硬件触发失败了比赛 .
我从2010年开始在AMD Phenom II X2 560(3.3 GHz)上定时硬件和软件 . 我用这个循环编写了一个C程序:
我编译了这个程序两次,有两个不同的sin()实现 . 硬sin()使用x87
fsin
. soft sin()使用多项式近似 . 我的C编译器gcc -O2
没有用内联fsin
替换我的sin()调用 .以下是sin(0.5)的结果:
这里的软sin(0.5)非常快,这个CPU可以比一个x87
fsin
更快地执行软sin(0.5)和软cos(0.5) .而对于罪(123):
软sin(123)比软sin(0.5)慢,因为123对于多项式来说太大,所以函数必须减去2π的某个倍数 . 如果我也想要cos(123),那么对于2010年的CPU,x87
fsincos
可能比soft sin(123)和soft cos(123)更快 .