在LLVM中调用fsincos指令比调用libc sin/cos函数要慢吗?

Erk*_*ere 10 assembly llvm inline-assembly x87

我正在研究用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
Run Code Online (Sandbox Code Playgroud)

如果我使用以下形式使用LLVM IR内联汇编来计算sin/cos:

%sc = call { double, double } asm "fsincos", "={st(1)},={st},1,~{dirflag},~{fpsr},~{flags}" (double %"res") nounwind
Run Code Online (Sandbox Code Playgroud)

这比分别使用fsin和fcos而不是fsincos更快.但是,它比我分别调用llvm.sin.f64and llvm.cos.f64intrinsics,编译调用C math lib函数要慢,至少使用我正在使用的目标设置(x86_64启用了SSE).

似乎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
Run Code Online (Sandbox Code Playgroud)

调用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
Run Code Online (Sandbox Code Playgroud)

你能说一下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
Run Code Online (Sandbox Code Playgroud)

Geo*_*ler 14

硬件触发很慢.

太多文档声称x87指令喜欢fsin或是fsincos最快的三角函数方法.这些说法经常是错误的.

最快的方法取决于您的CPU.随着CPU变得更快,旧的硬件触发指令fsin就跟不上了.对于某些CPU,使用正弦或其他触发功能的多项式近似的软件功能现在比硬件指令更快.

总之,fsincos太慢了.

硬件触发已过时.

有足够的证据表明x86-64平台已经远离硬件触发.

  • 对于花车,amd64比x87更喜欢SSE.然而,SSE没有x87指令的等价物fsin.
  • 对于amd64,FreeBSDglibc中的 libm在软件中实现sin()和这样的函数,而不是x87 trig.glibc 使用多项式近似优化了sinf()(单精度正弦)的x86-64汇编,而不是x87 fsin.NetBSDOpenBSD做出了相反的选择:他们的amd64 libm确实使用了x87指令.
  • Steel Bank Common Lisp fsin在其x86后端使用但不在其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);
Run Code Online (Sandbox Code Playgroud)

我编译了这个程序两次,有两个不同的sin()实现.硬sin()使用x87 fsin.soft sin()使用多项式近似.我的C编译器gcc -O2没有用内联替换我的sin()调用fsin.

以下是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
Run Code Online (Sandbox Code Playgroud)

这里的软sin(0.5)非常快,这个CPU可以比一个x87更快地执行软sin(0.5)和soft cos(0.5)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
Run Code Online (Sandbox Code Playgroud)

软sin(123)比软sin(0.5)慢,因为123对于多项式来说太大,所以函数必须减去2π的某个倍数.如果我也想要cos(123),那么fsincos对于2010年的CPU ,x87有可能比软sin(123)和soft cos(123)更快.

  • 我确认:即使在我老化的 Intel Xeon E5420 上,一百万条 fSinCos 汇编指令也需要 644 mS,而 System:Math.Sin+System.Math.Cos 需要 101 mS (2认同)