12

Compare()次のような関数があります。

inline bool Compare(bool greater, int p1, int p2) {
  if (greater) return p1>=p2;
  else return p1<=p2;
}

分岐を避けるために最適化することにしました。

inline bool Compare2(bool greater, int p1, int p2) {
  bool ret[2] = {p1<=p2,p1>=p2};
  return ret[greater];
}

次に、これを実行してテストしました:

bool x = true;
int M = 100000;
int N = 100;

bool a[N];
int b[N];
int c[N];

for (int i=0;i<N; ++i) {
  a[i] = rand()%2;
  b[i] = rand()%128;
  c[i] = rand()%128;
}

// Timed the below loop with both Compare() and Compare2()
for (int j=0; j<M; ++j) {
  for (int i=0; i<N; ++i) {
    x ^= Compare(a[i],b[i],c[i]);
  }
}

結果:

Compare(): 3.14ns avg
Compare2(): 1.61ns avg

私はケースクローズドと言います.FTWの分岐は避けてください. しかし、完全を期すために、私は置き換えました

a[i] = rand()%2;

と:

a[i] = true;

〜3.14nsのまったく同じ測定値を得ました。おそらく、その時点で分岐は行われておらず、コンパイラーは実際にステートメントCompare()を回避するために書き直しています。ifしかし、なぜCompare2()速いのでしょうか?

残念ながら、私はアセンブリ コードの知識がありません。そうでなければ、自分でこれに答えようとしたでしょう。

編集:以下はいくつかのアセンブリです:

_Z7Comparebii:
.LFB4:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    pushq   %rbp
    .cfi_def_cfa_offset 16
    movq    %rsp, %rbp
    .cfi_offset 6, -16
    .cfi_def_cfa_register 6
    movl    %edi, %eax
    movl    %esi, -8(%rbp)
    movl    %edx, -12(%rbp)
    movb    %al, -4(%rbp)
    cmpb    $0, -4(%rbp)
    je      .L2
    movl    -8(%rbp), %eax
    cmpl    -12(%rbp), %eax
    setge   %al
    jmp     .L3
.L2:
    movl    -8(%rbp), %eax
    cmpl    -12(%rbp), %eax
    setle   %al
.L3:
    leave
    ret
    .cfi_endproc
.LFE4:
    .size   _Z7Comparebii, .-_Z7Comparebii
    .section        .text._Z8Compare2bii,"axG",@progbits,_Z8Compare2bii,comdat
    .weak   _Z8Compare2bii
    .type   _Z8Compare2bii, @function
_Z8Compare2bii:
.LFB5:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    pushq   %rbp
    .cfi_def_cfa_offset 16
    movq    %rsp, %rbp
    .cfi_offset 6, -16
    .cfi_def_cfa_register 6
    movl    %edi, %eax
    movl    %esi, -24(%rbp)
    movl    %edx, -28(%rbp)
    movb    %al, -20(%rbp)
    movw    $0, -16(%rbp)
    movl    -24(%rbp), %eax
    cmpl    -28(%rbp), %eax
    setle   %al
    movb    %al, -16(%rbp)
    movl    -24(%rbp), %eax
    cmpl    -28(%rbp), %eax
    setge   %al
    movb    %al, -15(%rbp)
    movzbl  -20(%rbp), %eax
    cltq
    movzbl  -16(%rbp,%rax), %eax
    leave
    ret
    .cfi_endproc
.LFE5:
    .size   _Z8Compare2bii, .-_Z8Compare2bii
    .text

現在、テストを実行する実際のコードは、上記の 2 つの関数のインライン バージョンを使用している可能性があるため、これが分析するコードとして間違っている可能性があります。jmpそういえば にコマンドが見えますCompare()ので、分岐しているということだと思います。もしそうなら、この質問は次のようになると思います:なぜ分岐予測子は、からにCompare()変更したときのパフォーマンスを改善しないのですか?a[i]rand()%2truefalse

EDIT2:投稿をより賢明にするために、「分岐予測」を「分岐」に置き換えました。

4

3 に答える 3

4

これで大体のことはわかったと思います。

OP 編集で関数のアセンブリを投稿したときに、インライン バージョンが異なる可能性があることに気付きました。私はタイミングコードを調べたり投稿したりしていませんでした.なぜならそれはより複雑であり、インライン化のプロセスは分岐が行われるかどうかにかかわらず変わらないと思ったからCompare()です.

関数のインライン化を解除して測定を繰り返すと、次の結果が得られました。

Compare(): 7.18ns avg
Compare2(): 3.15ns avg

次に、 に置き換えるa[i]=rand()%2a[i]=false、次のようになりました。

Compare(): 2.59ns avg
Compare2(): 3.16ns avg

これは、分岐予測による利益を示しています。置換によって改善が得られなかったという事実は、a[i]もともとインライン化によって分岐が削除されたことを示しています。

謎の最後の部分は、なぜ inlinedが inlinedCompare2()より優れているのかということCompare()です。タイミングコードのアセンブリを投稿できると思います。関数がインライン化される方法のいくつかの癖がこれにつながる可能性があるのは十分にもっともらしいので、ここで調査を終了することに満足しています. Compare()アプリケーションでに置き換えCompare2()ます。

たくさんの参考になるコメントありがとうございます。

編集:他のすべてを打ち負かす理由として考えられるCompare2のは、プロセッサが両方の比較を並行して実行できることです。これが、私が行った方法で関数を作成するきっかけとなった直感でした。他のすべてのバリアントは、基本的に 2 つの論理的にシリアルな操作を必要とします。

于 2013-04-02T20:46:21.627 に答える
1

これはどう...

inline bool Compare3(bool greater, int p1, int p2) 
{
  return (!greater != !(p1 <= p2)) | (p1 == p2);
}

また

inline bool Compare4(bool greater, int p1, int p2) 
{
  return (greater ^ (p1 <= p2)) | (p1 == p2);
}
于 2013-04-02T17:52:55.713 に答える