これは上級管理職が面接で尋ねた質問です。
どちらが速いですか?
while(1) {
// Some code
}
または
while(2) {
//Some code
}
内部の式は最終的にまたはwhile
に評価されるため、どちらも実行速度は同じであると述べました。この場合、どちらも に評価され、条件内に追加の条件命令はありません。したがって、どちらも実行速度は同じであり、while (1) の方が適しています。true
false
true
while
しかし、面接官は自信を持ってこう言いました。「基礎を復習してください。while(1)
は よりも速いですwhile(2)
。」 (彼は私の自信を試していたわけではありません)
これは本当ですか?
ベストアンサー1
どちらのループも無限ですが、反復ごとにどちらのループでより多くの命令/リソースが必要になるかがわかります。
gcc を使用して、次の 2 つのプログラムをさまざまな最適化レベルでアセンブリにコンパイルしました。
int main(void) {
while(1) {}
return 0;
}
int main(void) {
while(2) {}
return 0;
}
最適化を行わなくても(-O0
)、生成されたアセンブリは両方のプログラムで同一でしたしたがって、 2 つのループ間に速度差はありません。
参考までに、生成されたアセンブリを以下に示します (gcc main.c -S -masm=intel
最適化フラグを使用)。
と-O0
:
.file "main.c"
.intel_syntax noprefix
.def __main; .scl 2; .type 32; .endef
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
sub rsp, 32
.seh_stackalloc 32
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
と-O1
:
.file "main.c"
.intel_syntax noprefix
.def __main; .scl 2; .type 32; .endef
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
sub rsp, 40
.seh_stackalloc 40
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
および(同じ出力) を使用する-O2
場合:-O3
.file "main.c"
.intel_syntax noprefix
.def __main; .scl 2; .type 32; .endef
.section .text.startup,"x"
.p2align 4,,15
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
sub rsp, 40
.seh_stackalloc 40
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
実際、ループ用に生成されたアセンブリは、最適化のレベルに関係なく同一です。
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
重要な点は次のとおりです:
.L2:
jmp .L2
アセンブリをあまりよく読めませんが、これは明らかに無条件ループです。このjmp
命令は、値を true と比較することさえせずに、プログラムを無条件にラベルにリセットし.L2
、もちろん、プログラムが何らかの方法で終了するまですぐに同じことを繰り返します。これは、C/C++ コードに直接対応しています。
L2:
goto L2;
編集:
興味深いことに、最適化を行わなくても、次のループはすべてjmp
アセンブリでまったく同じ出力 (無条件) を生成しました。
while(42) {}
while(1==1) {}
while(2==2) {}
while(4<7) {}
while(3==3 && 4==4) {}
while(8-9 < 0) {}
while(4.3 * 3e4 >= 2 << 6) {}
while(-0.1 + 02) {}
そしてさらに驚いたことに:
#include<math.h>
while(sqrt(7)) {}
while(hypot(3,4)) {}
ユーザー定義関数を使用すると、状況が少し面白くなります。
int x(void) {
return 1;
}
while(x()) {}
#include<math.h>
double x(void) {
return sqrt(7);
}
while(x()) {}
では-O0
、これら 2 つの例では実際に を呼び出してx
、各反復で比較を実行します。
最初の例(1 を返す):
.L4:
call x
testl %eax, %eax
jne .L4
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
2番目の例( を返すsqrt(7)
):
.L4:
call x
xorpd %xmm1, %xmm1
ucomisd %xmm1, %xmm0
jp .L4
xorpd %xmm1, %xmm1
ucomisd %xmm1, %xmm0
jne .L4
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
ただし、-O1
および では、どちらも前の例と同じアセンブリ (jmp
前のラベルへの無条件バック) を生成します。
要約
GCC では、異なるループが同一のアセンブリにコンパイルされます。コンパイラは定数値を評価し、実際の比較は実行しません。
この話の教訓は次のとおりです。
- C ソース コードと CPU 命令の間には変換レイヤーが存在し、このレイヤーはパフォーマンスに重要な影響を及ぼします。
- したがって、ソースコードだけを見てパフォーマンスを評価することはできません。
- コンパイラは、このような些細なケースを最適化できるほど賢くなければなりません。ほとんどの場合、プログラマはそれらについて考える時間を無駄にすべきではありません。