web-dev-qa-db-ja.com

ifステートメントとif-elseステートメントのどちらが速いですか?

私は先日、これらの2つのスニペットについて友人と議論しました。どちらが速いですか、なぜですか?

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

そして:

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

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

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

編集(現時点では質問が保留になっているため、スタッフから要求されました):

  • 主流コンパイラによって生成されたx86 Assemblyのいずれかを考慮して回答してください(たとえばg ++、clang ++、vc、mingw)最適化バージョンと非最適化バージョンの両方、またはMIPS Assembly
  • アセンブリが異なる場合、バージョンが高速である理由と(e.g。「ブランチングとブランチングに問題が発生していないため、より良い」
79
Julien__

TL; DR:最適化されていないコードでは、ifelseなしで使用すると、より効率的ではないように見えますが、最も基本的な最適化レベルを有効にしても、コードは基本的にvalue = condition + 5に書き換えられます。


I 試してみてください そして次のコードのアセンブリを生成しました:

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;
}

最適化を無効にしたgcc 6.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がゼロまたは1であると仮定します。最適化レベルを高くしても、出力を変更することはありません。ただし、開始時にmovzxレジスタを効率的にゼロにすることでEAXを回避できます。


免責事項:おそらくあなたは5 + conditionを自分で書くべきではありません(標準ではtrueから整数型への変換が1を与えることを保証していますが) (これには将来の自己が含まれる場合があります)。このコードのポイントは、両方の場合にコンパイラーが生成するものが(実際に)同一であることを示すことです。 Ciprian Tomoiaga はコメントでそれを非常によく述べています:

humanの仕事はコードを書くことです[for humansそして、compilerthe machineのコードを書かせます。

272
CompuChip

CompuChip の答えは、intの場合、両方とも同じアセンブリに最適化されていることを示しているため、問題ではありません。

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

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

それから

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

conditionがtrueの場合、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);

init1およびinit2に応じて、これも検討できます。

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

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

44
bolov

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

    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年以降に製造されたanyCPUでは、良好なパフォーマンスは 命令キャッシュ 内のコードフィッティングに依存します。したがって、疑わしい場合は、コードサイズを最小限にしてください。これは、最初の例の方が有利です。

  • 基本的な「 インオーダー、5ステージパイプライン 」CPU(多くのマイクロコントローラーで得られるものとほぼ同じ)を使用すると、分岐のたびに パイプラインバブル があります。条件付きまたは無条件-使用されるため、分岐命令の数を最小限にすることも重要です。これはまた、最初の例の方が有利です。

  • " out-of-order execution "を実行するのに十分なほど洗練されたCPUですが、その概念の最もよく知られている実装を使用するのに十分ではありません。 書き込み後書き込みハザード 。これは、secondの例のほうが有利です。ここで、r0は何があっても1回だけ書き込まれます。これらのCPUは通常、命令フェッチャーで無条件分岐を処理するのに十分なほど洗練されているため、are n'tは、書き込み後の書き込みペナルティを分岐と交換するだけです。ペナルティ。

    まだこの種のCPUを製造している人がいるかどうかはわかりません。ただし、doがアウトオブオーダー実行の「最も知られている実装」を使用するCPUは、使用頻度の低い命令のコーナーをカットする可能性が高く、そのため、この種のことが起こる可能性があることに注意する必要があります。実際の例は、 Sandy Bridge CPUのpopcntおよびlzcntのデスティネーションレジスタに対する誤ったデータ依存関係 です。

  • 上限では、OOOエンジンが両方のコードフラグメントに対してまったく同じ内部操作シーケンスを発行します。これは、「心配する必要はありません。コンパイラはどちらの方法でも同じマシンコードを生成します」のハードウェアバージョンです。ただし、コードサイズは依然として重要であり、条件分岐の予測可能性についても心配する必要があります。 分岐予測 障害により、完全なパイプラインが発生する可能性がありますflush。これはパフォーマンスに致命的です。 ソートされていない配列よりもソートされた配列を処理するほうが速いのはなぜですか? を参照してください。

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

        li    #0, r0
        test  r1
        setne r0
    

    または

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

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

11
zwol

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

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

10
Neil

ライナーが1つでも速くなったり遅くなったりすると、どのように思われますか?

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
}

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

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

私たちは最近、他の誰かとこの演習を行ったところ、スタックオーバーフローが発生しました。興味深いことに、この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条件ではなく、コンパイラがifジャンプを実行し、elseが実行されることです。しかし、これは必ずしもコーディングスタイルによるものではなく、比較およびif構文とelse構文が原因です。

8
old_timer

OK、アセンブリはタグの1つであるため、コードは擬似コード(必ずしもcではない)であると想定し、人間が6502アセンブリに変換します。

1番目のオプション(他のオプションなし)

        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に設定します。結果はアキュムレータになります。

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

結論:If onlyIf-Else構成よりも高速です。

完全を期すために、最適化されたvalue = condition + 5ソリューションを以下に示します。

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

これにより、時間は8サイクルに短縮されます(再び末尾にBRKを含まない)。

0
Glen Yates