14

私はLLVMでコンパイルされた言語に取り組んでいます。楽しみのために、いくつかのマイクロベンチマークを実行したいと思いました。1 つは、数百万回の 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

これは、fsincos の代わりに fsin と fcos を別々に使用するよりも高速です。ただし、少なくとも使用しているターゲット設定 (SSE が有効な x86_64) では、C math lib 関数への呼び出しにコンパイルする組み込み関数と を個別llvm.sin.f64に呼び出す場合よりも遅くなります。llvm.cos.f64

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 に追加すると、変換が消えて double (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
4

1 に答える 1

20

ハードウェア トリガーが遅い。

あまりにも多くの文書が、x87 命令が三角関数に似ている、fsinまたはfsincos三角関数を実行するための最速の方法であると主張しています。それらの主張はしばしば間違っています。

最速の方法は、CPU によって異なります。CPU の高速化に伴い、古いハードウェア トリガー命令などfsinは追いついていません。一部の CPU では、サイン関数または別の三角関数に多項式近似を使用するソフトウェア関数が、ハードウェア命令よりも高速になりました。

要するにfsincos遅すぎる。

ハードウェア トリガーは廃止されました。

x86-64 プラットフォームがハードウェア トリガーから離れたという十分な証拠があります。

  • amd64 は、float に対して x87 よりも SSE を優先します。しかし、SSE には、.x87 命令に相当するものはありませんfsin
  • amd64 の場合、 FreeBSDglibcの両方の libm は、x87 trig ではなく、ソフトウェアで sin() およびそのような関数を実装します。glibc はx87 ではなく多項式近似を使用して、sinf() (単精度正弦) のx86-64 アセンブリを最適化fsinしました。NetBSDOpenBSDは反対の選択をしました: amd64 の libm は x87 命令を使用します。
  • Steel Bank Common Lisp は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() の 2 つの異なる実装を使用して、このプログラムを 2 回コンパイルしました。ハード sin() は x87 を使用しfsinます。ソフト sin() は多項式近似を使用します。私の C コンパイラ はgcc -O2、私の sin() 呼び出しを inline に置き換えませんでした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

ここでソフト sin(0.5) は非常に高速で、この CPU はソフト sin(0.5) とソフト cos(0.5) を 1 つの 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

123 は多項式に対して大きすぎるため、ソフト sin(123) はソフト sin(0.5) よりも遅くなります。そのため、関数は 2π の倍数を減算する必要があります。cos(123) も必要な場合、2010 年以降のこの CPU では、x87fsincosがソフト sin(123) およびソフト cos(123) よりも高速になる可能性があります。

于 2014-06-28T20:44:31.910 に答える