Khi tôi viết câu trả lời này, tôi chỉ nhìn vào câu hỏi tiêu đề về <vs. <= nói chung, không phải là ví dụ cụ thể về hằng số a < 901
so với a <= 900
. Nhiều trình biên dịch luôn thu nhỏ độ lớn của các hằng số bằng cách chuyển đổi giữa <
và <=
, ví dụ vì toán hạng tức thời x86 có mã hóa 1 byte ngắn hơn cho -128..127.
Đối với ARM và đặc biệt là AArch64, việc có thể mã hóa ngay lập tức phụ thuộc vào việc có thể xoay một trường hẹp vào bất kỳ vị trí nào trong một từ. Vì vậy, cmp w0, #0x00f000
sẽ được mã hóa, trong khi cmp w0, #0x00effff
có thể không. Vì vậy, quy tắc làm cho nó nhỏ hơn để so sánh với hằng số thời gian biên dịch không phải lúc nào cũng áp dụng cho AArch64.
<vs. <= nói chung, bao gồm cả các điều kiện biến thời gian chạy
Trong ngôn ngữ lắp ráp trên hầu hết các máy, một so sánh <=
có cùng chi phí so với so sánh <
. Điều này áp dụng cho dù bạn đang phân nhánh trên nó, booleanize nó để tạo số nguyên 0/1 hoặc sử dụng nó làm vị ngữ cho một hoạt động chọn không phân nhánh (như x86 CMOV). Các câu trả lời khác chỉ giải quyết phần này của câu hỏi.
Nhưng câu hỏi này là về các toán tử C ++, đầu vào cho trình tối ưu hóa. Thông thường cả hai đều hiệu quả như nhau; lời khuyên từ cuốn sách nghe có vẻ hoàn toàn không có thật bởi vì trình biên dịch luôn có thể biến đổi sự so sánh mà chúng thực hiện trong asm. Nhưng có ít nhất một ngoại lệ khi sử dụng <=
có thể vô tình tạo ra thứ gì đó trình biên dịch không thể tối ưu hóa.
Là một điều kiện vòng lặp, có những trường hợp <=
là chất lượng khác nhau từ <
khi nó dừng lại trình biên dịch từ chứng minh rằng một vòng lặp không phải là vô hạn. Điều này có thể tạo ra một sự khác biệt lớn, vô hiệu hóa tự động vector hóa.
Tràn không được ký được xác định rõ là bao quanh cơ sở 2, không giống như tràn tràn đã ký (UB). Các bộ đếm vòng lặp đã ký thường an toàn với điều này với các trình biên dịch tối ưu hóa dựa trên UB tràn tràn đã ký không xảy ra: ++i <= size
cuối cùng sẽ luôn trở thành sai. ( Điều mà mọi lập trình viên C nên biết về hành vi không xác định )
void foo(unsigned size) {
unsigned upper_bound = size - 1; // or any calculation that could produce UINT_MAX
for(unsigned i=0 ; i <= upper_bound ; i++)
...
Trình biên dịch chỉ có thể tối ưu hóa theo cách bảo tồn hành vi (được xác định và có thể quan sát được về mặt pháp lý) của nguồn C ++ cho tất cả các giá trị đầu vào có thể , ngoại trừ các giá trị dẫn đến hành vi không xác định.
(Một đơn giản i <= size
cũng sẽ tạo ra vấn đề, nhưng tôi nghĩ tính toán giới hạn trên là một ví dụ thực tế hơn về việc vô tình đưa ra khả năng của một vòng lặp vô hạn cho một đầu vào mà bạn không quan tâm nhưng trình biên dịch phải xem xét.)
Trong trường hợp này, size=0
dẫn đến upper_bound=UINT_MAX
, và i <= UINT_MAX
luôn luôn đúng. Vì vậy, vòng lặp này là vô hạn size=0
và trình biên dịch phải tôn trọng điều đó mặc dù bạn là lập trình viên có thể không bao giờ có ý định vượt qua size = 0. Nếu trình biên dịch có thể nội tuyến hàm này vào một người gọi trong đó nó có thể chứng minh rằng size = 0 là không thể, thì thật tuyệt, nó có thể tối ưu hóa như nó có thể i < size
.
Asm like if(!size) skip the loop;
do{...}while(--size);
là một cách hiệu quả thông thường để tối ưu hóa for( i<size )
vòng lặp, nếu giá trị thực tế i
không cần thiết bên trong vòng lặp ( Tại sao các vòng lặp luôn được biên dịch thành kiểu "do ... while" (nhảy đuôi)? ).
Nhưng điều đó làm {} trong khi không thể là vô hạn: nếu được nhập cùng size==0
, chúng ta sẽ nhận được 2 ^ n lần lặp. ( Lặp lại tất cả các số nguyên không dấu trong một vòng lặp for C cho phép thể hiện một vòng lặp trên tất cả các số nguyên không dấu bao gồm 0, nhưng không dễ dàng gì nếu không có cờ mang theo cách thực hiện.)
Với khả năng bao gồm bộ đếm vòng lặp là một khả năng, các trình biên dịch hiện đại thường chỉ "từ bỏ" và không tối ưu hóa gần như mạnh mẽ.
Ví dụ: tổng các số nguyên từ 1 đến n
Sử dụng unsign i <= n
đánh bại nhận dạng thành ngữ của clang để tối ưu hóa sum(1 .. n)
các vòng lặp với dạng đóng dựa trên n * (n+1) / 2
công thức của Gauss .
unsigned sum_1_to_n_finite(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i < n+1 ; ++i)
total += i;
return total;
}
x86-64 asm từ clang7.0 và gcc8.2 trên trình thám hiểm trình biên dịch Godbolt
# clang7.0 -O3 closed-form
cmp edi, -1 # n passed in EDI: x86-64 System V calling convention
je .LBB1_1 # if (n == UINT_MAX) return 0; // C++ loop runs 0 times
# else fall through into the closed-form calc
mov ecx, edi # zero-extend n into RCX
lea eax, [rdi - 1] # n-1
imul rax, rcx # n * (n-1) # 64-bit
shr rax # n * (n-1) / 2
add eax, edi # n + (stuff / 2) = n * (n+1) / 2 # truncated to 32-bit
ret # computed without possible overflow of the product before right shifting
.LBB1_1:
xor eax, eax
ret
Nhưng đối với phiên bản ngây thơ, chúng ta chỉ nhận được một vòng lặp ngu ngốc từ tiếng kêu.
unsigned sum_1_to_n_naive(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i<=n ; ++i)
total += i;
return total;
}
# clang7.0 -O3
sum_1_to_n(unsigned int):
xor ecx, ecx # i = 0
xor eax, eax # retval = 0
.LBB0_1: # do {
add eax, ecx # retval += i
add ecx, 1 # ++1
cmp ecx, edi
jbe .LBB0_1 # } while( i<n );
ret
GCC không sử dụng dạng đóng theo bất kỳ cách nào, do đó, việc lựa chọn điều kiện vòng lặp không thực sự làm tổn thương nó ; nó tự động vectơ với phép cộng số nguyên SIMD, chạy i
song song 4 giá trị trong các phần tử của thanh ghi XMM.
# "naive" inner loop
.L3:
add eax, 1 # do {
paddd xmm0, xmm1 # vect_total_4.6, vect_vec_iv_.5
paddd xmm1, xmm2 # vect_vec_iv_.5, tmp114
cmp edx, eax # bnd.1, ivtmp.14 # bound and induction-variable tmp, I think.
ja .L3 #, # }while( n > i )
"finite" inner loop
# before the loop:
# xmm0 = 0 = totals
# xmm1 = {0,1,2,3} = i
# xmm2 = set1_epi32(4)
.L13: # do {
add eax, 1 # i++
paddd xmm0, xmm1 # total[0..3] += i[0..3]
paddd xmm1, xmm2 # i[0..3] += 4
cmp eax, edx
jne .L13 # }while( i != upper_limit );
then horizontal sum xmm0
and peeled cleanup for the last n%3 iterations, or something.
Nó cũng có một vòng vô hướng đơn giản mà tôi nghĩ rằng nó sử dụng cho rất nhỏ n
và / hoặc cho trường hợp vòng lặp vô hạn.
BTW, cả hai vòng lặp này đều lãng phí một lệnh (và một uop trên CPU gia đình Sandybridge) trên đầu vòng lặp. sub eax,1
/ jnz
thay vì add eax,1
/ cmp / jcc sẽ hiệu quả hơn. 1 uop thay vì 2 (sau khi tổng hợp macro của sub / jcc hoặc cmp / jcc). Mã sau cả hai vòng ghi EAX vô điều kiện, do đó, nó không sử dụng giá trị cuối cùng của bộ đếm vòng lặp.