私は先日、これらの2つのスニペットについて友人と議論しました。どちらが速いですか、なぜですか?
value = 5;
if (condition) {
value = 6;
}
そして:
if (condition) {
value = 6;
} else {
value = 5;
}
value
が行列の場合はどうなりますか?
注:value = condition ? 6 : 5;
が存在することは知っていますが、より高速になると予想していますが、オプションではありませんでした。
編集(現時点では質問が保留になっているため、スタッフから要求されました):
TL; DR:最適化されていないコードでは、if
をelse
なしで使用すると、より効率的ではないように見えますが、最も基本的な最適化レベルを有効にしても、コードは基本的に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そして、compilerにthe machineのコードを書かせます。
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;
しかし、繰り返しますが、これは、指定されたタイプの構築と割り当てが本当に高価な場合にのみ関連することを強調する必要があります。そして、それでも、プロファイリングによってのみ確実にわかります。
擬似アセンブリ言語では、
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
命令のディスパッチおよび実行リソースの価値があります。ブランチが実際に予測可能である場合、ブランチバージョンの方が高速になる可能性があります。
最適化されていないコードでは、最初の例は変数を常に1回、時には2回割り当てます。 2番目の例では、変数を一度しか割り当てません。条件は両方のコードパスで同じであるため、問題ではありません。最適化されたコードでは、コンパイラに依存します。
いつものように、そのことを心配している場合は、アセンブリを生成し、コンパイラが実際に何をしているかを確認してください。
ライナーが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構文が原因です。
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 only
はIf-Else
構成よりも高速です。
完全を期すために、最適化されたvalue = condition + 5
ソリューションを以下に示します。
ldy #$00
lda #$00
tya
adc #$05
brk
これにより、時間は8サイクルに短縮されます(再び末尾にBRK
を含まない)。