Cả hai vòng lặp là vô hạn, nhưng chúng ta có thể thấy cái nào cần nhiều hướng dẫn / tài nguyên hơn cho mỗi lần lặp.
Sử dụng gcc, tôi đã biên dịch hai chương trình sau để lắp ráp ở các mức độ tối ưu hóa khác nhau:
int main(void) {
while(1) {}
return 0;
}
int main(void) {
while(2) {}
return 0;
}
Ngay cả khi không có tối ưu hóa ( -O0
), lắp ráp được tạo là giống hệt nhau cho cả hai chương trình . Do đó, không có sự khác biệt về tốc độ giữa hai vòng.
Để tham khảo, đây là hội đồng được tạo (sử dụng gcc main.c -S -masm=intel
với cờ tối ưu hóa):
Với -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"
Với -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"
Với -O2
và -O3
(cùng đầu ra):
.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"
Trong thực tế, lắp ráp được tạo cho vòng lặp là giống hệt nhau cho mọi cấp độ tối ưu hóa:
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
Các bit quan trọng là:
.L2:
jmp .L2
Tôi không thể đọc lắp ráp rất tốt, nhưng đây rõ ràng là một vòng lặp vô điều kiện. Các jmp
hướng dẫn cách vô điều kiện reset chương trình trở lại .L2
nhãn mà không cần so sánh một giá trị so với sự thật, và tất nhiên ngay lập tức làm như vậy một lần nữa cho đến khi chương trình được bằng cách nào đó đã kết thúc. Điều này tương ứng trực tiếp với mã C / C ++:
L2:
goto L2;
Biên tập:
Thật thú vị, ngay cả khi không có tối ưu hóa , tất cả các vòng lặp sau đây đều tạo ra cùng một đầu ra (vô điều kiện jmp
) trong lắp ráp:
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) {}
Và thậm chí làm tôi ngạc nhiên:
#include<math.h>
while(sqrt(7)) {}
while(hypot(3,4)) {}
Mọi thứ trở nên thú vị hơn với các hàm do người dùng định nghĩa:
int x(void) {
return 1;
}
while(x()) {}
#include<math.h>
double x(void) {
return sqrt(7);
}
while(x()) {}
Tại -O0
, hai ví dụ này thực sự gọi x
và thực hiện so sánh cho mỗi lần lặp.
Ví dụ đầu tiên (trả về 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"
Ví dụ thứ hai (trở về 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"
Tuy nhiên, tại -O1
và trên, cả hai đều tạo ra một hội đồng giống như các ví dụ trước (một điều kiện vô điều kiện jmp
trở lại nhãn trước).
TL; DR
Theo GCC, các vòng lặp khác nhau được biên dịch để lắp ráp giống hệt nhau. Trình biên dịch đánh giá các giá trị không đổi và không bận tâm thực hiện bất kỳ so sánh thực tế nào.
Đạo đức của câu chuyện là:
- Tồn tại một lớp dịch giữa mã nguồn C ++ và các hướng dẫn CPU và lớp này có ý nghĩa quan trọng đối với hiệu năng.
- Do đó, hiệu suất không thể được đánh giá bằng cách chỉ nhìn vào mã nguồn.
- Trình biên dịch phải đủ thông minh để tối ưu hóa các trường hợp tầm thường như vậy. Các lập trình viên không nên lãng phí thời gian suy nghĩ về họ trong phần lớn các trường hợp.