ifステートメントとif-elseステートメントのどちらが速いですか?[閉まっている]


83

先日、この2つのスニペットについて友人と議論しました。どちらが速いのか、そしてその理由は?

value = 5;
if (condition) {
    value = 6;
}

そして:

if (condition) {
    value = 6;
} else {
    value = 5;
}

value行列の場合はどうなりますか?

注:私はそれvalue = condition ? 6 : 5;が存在することを知っており、より高速になることを期待していますが、それはオプションではありませんでした。

編集(質問は現在保留中のため、スタッフから要求されました):

  • 最適化されたバージョンと最適化されていないバージョンの両方でメインストリームコンパイラ(g ++、clang ++、vc、mingwなど)によって生成されたx86アセンブリまたはMIPSアセンブリのいずれかを検討して回答してください。
  • アセンブリが異なる場合は、バージョンが高速である理由と時期を説明してください(たとえば、「分岐がないため、分岐に次の問題が発生するため、より良い」

173
最適化は殺すすべてのこと、それは問題ではないから... ...
量子物理学者

21
個人的には、最新のコンパイラを使用しても違いが見られるとは思えません。
ジョージ

25
value = condition ? 6 : 5;代わりにif/elseを使用すると、ほとんどの場合、同じコードが生成されます。詳細を知りたい場合は、アセンブリ出力を確認してください。
Jabberwocky 2017

8
この場合の最も重要なことは、ここで最も高価なことであるブランチを回避することです。(パイプのリロード、
プリフェッチ

11
このように速度をマイクロ最適化することが理にかなっているのは、何度も実行されるループ内であり、この簡単な例のgccのように、オプティマイザーがすべての分岐命令を最適化できるか、実際の世界です。パフォーマンスは、正しい分岐予測に大きく依存します(stackoverflow.com/questions/11227809/…への必須リンク)。やむを得ずループ内で分岐する場合は、プロファイルを生成して再コンパイルすることで分岐予測を支援できます。
Davislor 2017

回答:


282

TL; DR:最適化されていないコードでは、関係ifなくelse効率的であるように見えますが、最も基本的なレベルの最適化が有効になっている場合でも、コードは基本的にに書き換えられvalue = condition + 5ます。


私はそれを試してみましたし、次のコードのためのアセンブリを生成します:

int ifonly(bool condition, int value)
{
    value = 5;
    if (condition) {
        value = 6;
    }
    return value;
}

int ifelse(bool condition, int value)
{
    if (condition) {
        value = 6;
    } else {
        value = 5;
    }
    return value;
}

最適化が無効になっているgcc6.3(-O0)では、関連する違いは次のとおりです。

 mov     DWORD PTR [rbp-8], 5
 cmp     BYTE PTR [rbp-4], 0
 je      .L2
 mov     DWORD PTR [rbp-8], 6
.L2:
 mov     eax, DWORD PTR [rbp-8]

のためifonlyに、ifelse持っている間

 cmp     BYTE PTR [rbp-4], 0
 je      .L5
 mov     DWORD PTR [rbp-8], 6
 jmp     .L6
.L5:
 mov     DWORD PTR [rbp-8], 5
.L6:
 mov     eax, DWORD PTR [rbp-8]

後者は余分なジャンプがあるため効率がやや劣りますが、両方に少なくとも2つ、最大で3つの割り当てがあるため、パフォーマンスの最後の一滴を本当に絞る必要がない限り(ヒント:スペースシャトルで作業している場合を除いて、 、そしてそれでもおそらくあなたはそうしないでしょう)違いは目立たないでしょう

ただし、最適化レベルが最も低い場合でも(-O1)、両方の関数は同じになります。

test    dil, dil
setne   al
movzx   eax, al
add     eax, 5

これは基本的に同等です

return 5 + condition;

conditionが0または1であると仮定します。より高い最適化レベルは、開始時にレジスタをmovzx効率的にゼロにすることによって回避することができることを除いて、実際には出力を変更しませんEAX


免責事項:コードを読んでいる人(将来の自分自身を含む可能性があります)には意図がすぐにはわからない可能性があるため、おそらく5 + condition自分で書くべきではありません(true整数型に変換すると標準で保証されて1いますが)。このコードのポイントは、両方の場合にコンパイラーが生成するものが(実質的に)同一であることを示すことです。Ciprian Tomoiagaは、コメントの中でそれを非常によく述べています。

人間の仕事はコードを書くことで人間のためとしましょうコンパイラの書き込みコードマシンを


50
これは素晴らしい答えであり、受け入れられるべきです。
dtell 2017

10
私は決してかかわらず、追加を使用するのでただろう(< - Pythonはあなたに何をするか。)
シプリアンTomoiagă

26
@CiprianTomoiagaそしてあなたがオプティマイザーを書いているのでない限り、あなたはすべきではありません!ほとんどすべての場合、特にコードの可読性が大幅に低下する場合は、コンパイラにこのような最適化を行わせる必要があります。パフォーマンステストで特定のコードの問題が示された場合にのみ、コードの最適化を試み、それでもクリーンでコメントを付けたままにして、測定可能な違いをもたらす最適化のみを実行する必要があります。
ミューザー2017

22
Muzerに返信したかったのですが、スレッドに何も追加されませんでした。ただし、人間の仕事は人間コードを記述し、コンパイラにマシンコードを記述させることであると言い換えたいと思います。私は、コンパイラの開発者ハメ撮り(私はないんだけど、私はそれについて少し学びました)からと言った
シプリアンTomoiagă

10
true変換された値はint常に1、期間を生成します。もちろん、あなたの状態が単に「真実」であり、bool価値trueではない場合、それはまったく別の問題です。
TC

44

CompuChipからの回答はint、どちらも同じアセンブリに最適化されているため、問題ではないことを示しています。

値が行列の場合はどうなりますか?

私はこれをより一般的な方法で解釈します。つまりvalue、構造と割り当てが高価な(そして移動が安価な)タイプの場合はどうでしょうか。

その後

T value = init1;
if (condition)
   value = init2;

conditiontrueの場合、に不要な初期化を行ってからinit1コピーの割り当てを行うため、は最適ではありません。

T value;
if (condition)
   value = init2;
else
   value = init3;

これの方が良い。ただし、デフォルトの構築に費用がかかり、コピーの構築に初期化よりも費用がかかる場合は、依然として最適ではありません。

良い条件演算子ソリューションがあります。

T value = condition ? init1 : init2;

または、条件演算子が気に入らない場合は、次のようなヘルパー関数を作成できます。

T create(bool condition)
{
  if (condition)
     return {init1};
  else
     return {init2};
}

T value = create(condition);

内容に応じてinit1init2あなたもこれを考慮することができます:

auto final_init = condition ? init1 : init2;
T value = final_init;

しかし、繰り返しますが、これは、特定のタイプに対して構築と割り当てが本当に高価な場合にのみ関係することを強調する必要があります。そしてそれでも、あなたが確かに知っているプロファイリングによってのみ。


3
高価最適化されていません。たとえば、デフォルトのコンストラクターがマトリックスをゼロにする場合、コンパイラーは、割り当てがそれらの0を上書きしていることを認識し、ゼロにしないで、このメモリーに直接書き込むことができます。もちろん、オプティマイザーは気難しい獣なので、いつキックインするかを予測するのは難しいです...
Matthieu M.

@MatthieuM。もちろん。私は「高価な」「でも、コンパイラの最適化の後に(メトリック、こともどちらかのCPUクロック、リソースの使用率などによって)を実行するには高価であることで意味。
bolov

デフォルトの構築は高価であるが、安く動くとは思えません。
プラグウォッシュ2017

6
@plugwash非常に大きな配列が割り当てられているクラスについて考えてみます。デフォルトのコンストラクターは配列を割り当てて初期化しますが、これにはコストがかかります。移動(コピーではありません!)コンストラクターは、ポインターをソースオブジェクトと交換するだけで、大きな配列を割り当てたり初期化したりする必要はありません。
トレント2017

1
パーツがシンプルである限り?:、新しい関数を導入するよりも演算子を使用する方が間違いなく好きです。結局のところ、条件を関数に渡すだけでなく、コンストラクター引数も渡す可能性があります。create()状態によっては使用できないものもあります。
cmaster - REINSTATEモニカ

12

疑似アセンブリ言語では、

    li    #0, r0
    test  r1
    beq   L1
    li    #1, r0
L1:

より速い場合とそうでない場合があります

    test  r1
    beq   L1
    li    #1, r0
    bra   L2
L1:
    li    #0, r0
L2:

実際のCPUの洗練度によって異なります。最も単純なものから最も空想的なものへの移行:

  • 任意のおよそ1990年以降に製造CPU、良好なパフォーマンスは、内のコードのフィッティングに依存命令キャッシュ。したがって、疑わしい場合は、コードサイズを最小限に抑えてください。これは、最初の例に有利に働きます。

  • 基本的な「順序どおりの5ステージパイプライン」CPUは、多くのマイクロコントローラーでほぼ使用されているものですが、条件付きまたは無条件の分岐が行われるたびにパイプラインストールが発生するため、最小化することも重要です。分岐命令の数。これも最初の例に有利に働きます。

  • やや洗練されたCPU(「アウトオブオーダー実行」を実行するには十分ですが、その概念の最もよく知られている実装を使用するには十分ではありません)は、書き込み後の危険に遭遇するたびにパイプラインストールを引き起こす可能性があります。これは、何があっても一度だけ書かれる2番目の例に有利に働きr0ます。これらのCPUは通常、命令フェッチャーで無条件の分岐を処理するのに十分な機能を備えているため、書き込み後のペナルティを分岐ペナルティと交換するだけではありません

    誰かがまだこの種のCPUをもう作っているかどうかはわかりません。ただし、アウトオブオーダー実行の「最もよく知られている実装」を使用するCPUは、使用頻度の低い命令を削減する可能性が高いため、このようなことが発生する可能性があることに注意する必要があります。実際の例は、Sandy BridgeCPUpopcntおよびlzcntCPU上の宛先レジスタへの誤ったデータ依存関係です。

  • 最高の状態では、OOOエンジンは、両方のコードフラグメントに対してまったく同じシーケンスの内部操作を発行することになります。これは、「心配しないでください。コンパイラはどちらの方法でも同じマシンコードを生成します」のハードウェアバージョンです。ただし、コードサイズは依然として重要であり、条件分岐の予測可能性についても心配する必要があります。 分岐予測の失敗は、完全なパイプラインフラッシュを引き起こす可能性があり、これはパフォーマンスに壊滅的です。ソートされていない配列よりもソートされた配列の処理が速いのなぜですか?を参照してくださいこれがどれほどの違いを生むかを理解するために。

    ブランチ非常に予測不可能であり、CPUに条件付きセットまたは条件付き移動命令がある場合、これはそれらを使用するときです。

        li    #0, r0
        test  r1
        setne r0
    

    または

        li    #0, r0
        li    #1, r2
        test  r1
        movne r2, r0
    

    条件付きセットバージョンは、他のどの代替手段よりもコンパクトです。その命令が利用可能である場合、ブランチが予測可能であったとしても、それはこのシナリオにとって正しいことであることが実質的に保証されます。条件付き移動バージョンでは、追加のスクラッチレジスタが必要であり、常に1li命令分のディスパッチと実行リソースを浪費します。ブランチが実際に予測可能であった場合、ブランチバージョンの方が高速である可能性があります。


CPUに、書き込み後のハザードによって遅延する異常なエンジンがあるかどうかについて、2番目のポイントを言い換えます。このような危険を遅滞なく処理できる故障エンジンがCPUに搭載されていれば問題ありませんが、CPUに故障エンジンがまったく搭載さていなくても問題ありません。
スーパーキャット2017

@supercat最後の段落はそのケースをカバーすることを意図していますが、それをより明確にする方法について考えます。
zwol 2017

現在のCPUにキャッシュがあり、順次実行されるコードが1回目よりも2回目で高速に実行されるかどうかはわかりません(一部のフラッシュベースのARMパーツには、数行のフラッシュデータをバッファリングできるインターフェイスがありますが、コードを実行するのと同じ速さで順番にフェッチできますが、ブランチの多いコードを高速で実行するための鍵は、コードをRAMにコピーすることです)。アウトオブオーダー実行がまったくないCPUは、書き込み後のハザードによって遅延するCPUよりもはるかに一般的です。
スーパーキャット2017

これは非常に洞察に満ちています
Julien__ 2017

9

最適化されていないコードでは、最初の例で変数を常に1回、場合によっては2回割り当てます。2番目の例では、変数を1回だけ割り当てます。条件は両方のコードパスで同じであるため、問題にはなりません。最適化されたコードでは、コンパイラに依存します。

いつものように、心配な場合は、アセンブリを生成して、コンパイラが実際に何をしているかを確認してください。


1
パフォーマンスが心配な場合は、最適化されていない状態でコンパイルすることはありません。しかし確かに、オプティマイザがどれだけ「良い」かは、コンパイラ/バージョンによって異なります。
old_timer 2017

AFAIKには、どのコンパイラ/ CPUアーキテクチャなどについてのコメントがないため、コンパイラが最適化を行わない可能性があります。8ビットPICから64ビットXeonまで何でもコンパイルできます。
ニール

8

ワンライナーでさえ、それらのどれかが速いか遅いとあなたはどう思いますか?

unsigned int fun0 ( unsigned int condition, unsigned int value )
{
    value = 5;
    if (condition) {
        value = 6;
    }
    return(value);
}
unsigned int fun1 ( unsigned int condition, unsigned int value )
{

    if (condition) {
        value = 6;
    } else {
        value = 5;
    }
    return(value);
}
unsigned int fun2 ( unsigned int condition, unsigned int value )
{
    value = condition ? 6 : 5;
    return(value);
}

高水準言語のコード行が増えると、コンパイラーはより多くのコードを処理できるようになるため、一般的なルールを作成する場合は、コンパイラーに処理するコードを増やしてください。アルゴリズムが上記の場合と同じである場合、最小限の最適化でコンパイラがそれを理解することが期待されます。

00000000 <fun0>:
   0:   e3500000    cmp r0, #0
   4:   03a00005    moveq   r0, #5
   8:   13a00006    movne   r0, #6
   c:   e12fff1e    bx  lr

00000010 <fun1>:
  10:   e3500000    cmp r0, #0
  14:   13a00006    movne   r0, #6
  18:   03a00005    moveq   r0, #5
  1c:   e12fff1e    bx  lr

00000020 <fun2>:
  20:   e3500000    cmp r0, #0
  24:   13a00006    movne   r0, #6
  28:   03a00005    moveq   r0, #5
  2c:   e12fff1e    bx  lr

大きな驚きではありませんが、最初の機能を異なる順序で実行しましたが、実行時間は同じでした。

0000000000000000 <fun0>:
   0:   7100001f    cmp w0, #0x0
   4:   1a9f07e0    cset    w0, ne
   8:   11001400    add w0, w0, #0x5
   c:   d65f03c0    ret

0000000000000010 <fun1>:
  10:   7100001f    cmp w0, #0x0
  14:   1a9f07e0    cset    w0, ne
  18:   11001400    add w0, w0, #0x5
  1c:   d65f03c0    ret

0000000000000020 <fun2>:
  20:   7100001f    cmp w0, #0x0
  24:   1a9f07e0    cset    w0, ne
  28:   11001400    add w0, w0, #0x5
  2c:   d65f03c0    ret

異なる実装が実際に異なっていないことが明らかでない場合は、これを試してみたかもしれないという考えが得られることを願っています。

マトリックスに関する限り、それがどのように重要かわからない、

if(condition)
{
 big blob of code a
}
else
{
 big blob of code b
}

value = 5かそれ以上の複雑なコードの大きなブロブの周りに、同じif-then-elseラッパーを配置するだけです。同様に、それがコードの大きなブロブであっても、比較は計算する必要があり、何かに等しいか等しくないかは、多くの場合、負の値でコンパイルされます。

00000000 <fun0>:
   0:   0f 93           tst r15     
   2:   03 24           jz  $+8         ;abs 0xa
   4:   3f 40 06 00     mov #6, r15 ;#0x0006
   8:   30 41           ret         
   a:   3f 40 05 00     mov #5, r15 ;#0x0005
   e:   30 41           ret         

00000010 <fun1>:
  10:   0f 93           tst r15     
  12:   03 20           jnz $+8         ;abs 0x1a
  14:   3f 40 05 00     mov #5, r15 ;#0x0005
  18:   30 41           ret         
  1a:   3f 40 06 00     mov #6, r15 ;#0x0006
  1e:   30 41           ret         

00000020 <fun2>:
  20:   0f 93           tst r15     
  22:   03 20           jnz $+8         ;abs 0x2a
  24:   3f 40 05 00     mov #5, r15 ;#0x0005
  28:   30 41           ret         
  2a:   3f 40 06 00     mov #6, r15 ;#0x0006
  2e:   30 41

最近、stackoverflowで他の誰かとこの演習を行いました。このmipsコンパイラは興味深いことに、その場合、関数が同じであることに気付いただけでなく、コードスペースを節約するために1つの関数が他の関数にジャンプするだけでした。ここではそれをしませんでした

00000000 <fun0>:
   0:   0004102b    sltu    $2,$0,$4
   4:   03e00008    jr  $31
   8:   24420005    addiu   $2,$2,5

0000000c <fun1>:
   c:   0004102b    sltu    $2,$0,$4
  10:   03e00008    jr  $31
  14:   24420005    addiu   $2,$2,5

00000018 <fun2>:
  18:   0004102b    sltu    $2,$0,$4
  1c:   03e00008    jr  $31
  20:   24420005    addiu   $2,$2,5

さらにいくつかのターゲット。

00000000 <_fun0>:
   0:   1166            mov r5, -(sp)
   2:   1185            mov sp, r5
   4:   0bf5 0004       tst 4(r5)
   8:   0304            beq 12 <_fun0+0x12>
   a:   15c0 0006       mov $6, r0
   e:   1585            mov (sp)+, r5
  10:   0087            rts pc
  12:   15c0 0005       mov $5, r0
  16:   1585            mov (sp)+, r5
  18:   0087            rts pc

0000001a <_fun1>:
  1a:   1166            mov r5, -(sp)
  1c:   1185            mov sp, r5
  1e:   0bf5 0004       tst 4(r5)
  22:   0204            bne 2c <_fun1+0x12>
  24:   15c0 0005       mov $5, r0
  28:   1585            mov (sp)+, r5
  2a:   0087            rts pc
  2c:   15c0 0006       mov $6, r0
  30:   1585            mov (sp)+, r5
  32:   0087            rts pc

00000034 <_fun2>:
  34:   1166            mov r5, -(sp)
  36:   1185            mov sp, r5
  38:   0bf5 0004       tst 4(r5)
  3c:   0204            bne 46 <_fun2+0x12>
  3e:   15c0 0005       mov $5, r0
  42:   1585            mov (sp)+, r5
  44:   0087            rts pc
  46:   15c0 0006       mov $6, r0
  4a:   1585            mov (sp)+, r5
  4c:   0087            rts pc

00000000 <fun0>:
   0:   00a03533            snez    x10,x10
   4:   0515                    addi    x10,x10,5
   6:   8082                    ret

00000008 <fun1>:
   8:   00a03533            snez    x10,x10
   c:   0515                    addi    x10,x10,5
   e:   8082                    ret

00000010 <fun2>:
  10:   00a03533            snez    x10,x10
  14:   0515                    addi    x10,x10,5
  16:   8082                    ret

およびコンパイラ

このiコードを使用すると、さまざまなターゲットも一致することが期待されます

define i32 @fun0(i32 %condition, i32 %value) #0 {
  %1 = icmp ne i32 %condition, 0
  %. = select i1 %1, i32 6, i32 5
  ret i32 %.
}

; Function Attrs: norecurse nounwind readnone
define i32 @fun1(i32 %condition, i32 %value) #0 {
  %1 = icmp eq i32 %condition, 0
  %. = select i1 %1, i32 5, i32 6
  ret i32 %.
}

; Function Attrs: norecurse nounwind readnone
define i32 @fun2(i32 %condition, i32 %value) #0 {
  %1 = icmp ne i32 %condition, 0
  %2 = select i1 %1, i32 6, i32 5
  ret i32 %2
}


00000000 <fun0>:
   0:   e3a01005    mov r1, #5
   4:   e3500000    cmp r0, #0
   8:   13a01006    movne   r1, #6
   c:   e1a00001    mov r0, r1
  10:   e12fff1e    bx  lr

00000014 <fun1>:
  14:   e3a01006    mov r1, #6
  18:   e3500000    cmp r0, #0
  1c:   03a01005    moveq   r1, #5
  20:   e1a00001    mov r0, r1
  24:   e12fff1e    bx  lr

00000028 <fun2>:
  28:   e3a01005    mov r1, #5
  2c:   e3500000    cmp r0, #0
  30:   13a01006    movne   r1, #6
  34:   e1a00001    mov r0, r1
  38:   e12fff1e    bx  lr


fun0:
    push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #6, r15
    cmp.w   #0, r12
    jne .LBB0_2
    mov.w   #5, r15
.LBB0_2:
    pop.w   r4
    ret

fun1:
    push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #5, r15
    cmp.w   #0, r12
    jeq .LBB1_2
    mov.w   #6, r15
.LBB1_2:
    pop.w   r4
    ret


fun2:
    push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #6, r15
    cmp.w   #0, r12
    jne .LBB2_2
    mov.w   #5, r15
.LBB2_2:
    pop.w   r4
    ret

技術的には、これらのソリューションの一部にパフォーマンスの違いがあります。結果が5の場合、結果が6コードを飛び越えたり、その逆の場合、ブランチは実行よりも高速ですか?議論することもできますが、実行は異なるはずです。しかし、それはコード内のif条件とif not条件のどちらかであり、コンパイラーはifthisジャンプオーバーelse実行を実行します。しかし、これは必ずしもコーディングスタイルによるものではなく、どのような構文でも比較とifおよびelseの場合が原因です。


0

わかりました。アセンブリはタグの1つなので、コードは疑似コード(必ずしもcである必要はありません)であると想定し、人間が6502アセンブリに変換します。

最初のオプション(他なし)

        ldy #$00
        lda #$05
        dey
        bmi false
        lda #$06
false   brk

2番目のオプション(他の場合)

        ldy #$00
        dey
        bmi else
        lda #$06
        sec
        bcs end
else    lda #$05
end     brk

前提条件:条件はYレジスタにあり、いずれかのオプションの最初の行でこれを0または1に設定すると、結果はアキュムレータになります。

したがって、各ケースの両方の可能性についてサイクルをカウントした後、最初の構成が一般的に高速であることがわかります。条件が0の場合は9サイクル、条件が1の場合は10サイクル、オプション2も条件が0の場合は9サイクルですが、条件が1の場合は13サイクルです(サイクルカウントBRKには最後のが含まれません)。

結論:構築If onlyよりも高速ですIf-Else

そして完全を期すために、ここに最適化されたものがあります value = condition + 5ソリューションます:

ldy #$00
lda #$00
tya
adc #$05
brk

これにより、時間が8サイクルに短縮されます(これもBRK最後のを含みません)。


6
この答えが残念なことに、同じソースコードをCコンパイラ(またはC ++コンパイラ)にフィードすると、Glenの脳にフィードする場合とは大きく異なる出力が生成されます。ソースコードレベルの代替案の間に違いはなく、「最適化」の可能性もありません。最も読みやすいもの(おそらくif / elseのもの)を使用してください。
Quuxplusone 2017

1
@うん。コンパイラーは、両方のバリアントを最速バージョンに最適化するか、2つの違いをはるかに上回るオーバーヘッドを追加する可能性があります。または両方。
jpaugh 2017

1
質問がC ++としてタグ付けされていることを考えると、「必ずしもCである必要はない」と仮定すると、賢明な選択のように思われます(残念ながら、関係する変数のタイプを宣言することはできません)。
Toby Speight 2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.