両方のループは無限ですが、反復ごとにどちらがより多くの命令/リソースを必要とするかを確認できます。
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
命令は.L2
、値をtrueと比較することさえせずに、無条件にプログラムをラベルにリセットします。もちろん、プログラムが何らかの形で終了するまで、すぐに再びリセットします。これは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
前のラベルに無条件に戻る)。
TL; DR
GCCでは、さまざまなループが同じアセンブリにコンパイルされます。コンパイラーは定数値を評価し、実際の比較を実行する必要はありません。
物語の教訓は:
- C ++ソースコードとCPU命令の間には変換の層があり、この層はパフォーマンスに重要な影響を与えます。
- したがって、ソースコードだけを見てパフォーマンスを評価することはできません。
- コンパイラーは、そのような些細なケースを最適化するのに十分スマートでなければなりません。プログラマーは、大多数のケースでそれらについて考えることに時間を無駄にすべきではありません。