Ketika saya menulis jawaban ini, saya hanya melihat pertanyaan judul tentang <vs <= secara umum, bukan contoh spesifik konstan a < 901
vs a <= 900
. Banyak kompiler selalu mengecilkan besarnya konstanta dengan mengkonversi antara <
dan <=
, misalnya karena operan x86 langsung memiliki pengodean 1 byte yang lebih pendek untuk -128..127.
Untuk ARM dan terutama AArch64, kemampuan untuk menyandikan secara langsung tergantung pada kemampuan untuk memutar bidang sempit ke posisi apa pun dalam sebuah kata. Jadi cmp w0, #0x00f000
akan dikodekan, sementara cmp w0, #0x00effff
mungkin tidak. Jadi aturan make-it-lebih kecil untuk perbandingan vs konstanta waktu kompilasi tidak selalu berlaku untuk AArch64.
<vs. <= secara umum, termasuk untuk kondisi variabel runtime
Dalam bahasa rakitan pada kebanyakan mesin, perbandingan untuk <=
memiliki biaya yang sama dengan perbandingan untuk <
. Ini berlaku apakah Anda bercabang di atasnya, mendudukkannya untuk membuat integer 0/1, atau menggunakannya sebagai predikat untuk operasi pilih tanpa cabang (seperti x86 CMOV). Jawaban lain hanya menjawab bagian pertanyaan ini.
Tetapi pertanyaan ini adalah tentang operator C ++, input ke optimizer. Biasanya keduanya sama-sama efisien; saran dari buku ini terdengar sangat palsu karena kompiler selalu dapat mengubah perbandingan yang mereka terapkan dalam asm. Tetapi ada setidaknya satu pengecualian di mana menggunakan <=
secara tidak sengaja dapat menciptakan sesuatu yang tidak dapat dioptimalkan oleh kompiler.
Sebagai kondisi loop, ada kasus-kasus di mana <=
secara kualitatif berbeda dari <
, ketika itu menghentikan kompiler membuktikan bahwa loop tidak terbatas. Ini dapat membuat perbedaan besar, menonaktifkan auto-vektorisasi.
Overflow unsigned didefinisikan dengan baik sebagai basis-2 membungkus, tidak seperti ditandatangani overflow (UB). Counter loop yang ditandatangani umumnya aman dari ini dengan kompiler yang dioptimalkan berdasarkan UB tidak masuk: tidak ada: ++i <= size
akhirnya akan selalu salah. ( Apa Yang Harus Setiap C Programmer Ketahui Tentang Perilaku Tidak Terdefinisi )
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++)
...
Kompiler hanya dapat mengoptimalkan dengan cara yang menjaga perilaku (didefinisikan dan diamati secara hukum) dari sumber C ++ untuk semua nilai input yang mungkin , kecuali yang mengarah pada perilaku yang tidak terdefinisi.
(Sederhana juga i <= size
akan menciptakan masalah, tetapi saya pikir menghitung batas atas adalah contoh yang lebih realistis untuk secara tidak sengaja memperkenalkan kemungkinan loop tak terbatas untuk input yang tidak Anda pedulikan tetapi yang harus dipertimbangkan oleh kompilator.)
Dalam hal ini, size=0
mengarah ke upper_bound=UINT_MAX
, dan i <= UINT_MAX
selalu benar. Jadi loop ini tidak terbatas untuk size=0
, dan kompiler harus menghargai bahwa meskipun Anda sebagai programmer mungkin tidak pernah bermaksud untuk lulus ukuran = 0. Jika kompiler dapat menguraikan fungsi ini ke dalam pemanggil di mana ia dapat membuktikan bahwa ukuran = 0 tidak mungkin, maka bagus, ia dapat mengoptimalkan seperti yang bisa dilakukan i < size
.
Asm like if(!size) skip the loop;
do{...}while(--size);
adalah salah satu cara yang biasanya efisien untuk mengoptimalkan for( i<size )
loop, jika nilai aktual i
tidak diperlukan di dalam loop ( Mengapa loop selalu dikompilasi menjadi gaya "do ... while" (tail jump)? ).
Tapi itu {} sementara tidak bisa tak terbatas: jika dimasukkan dengan size==0
, kita mendapatkan 2 ^ n iterasi. ( Iterasi atas semua bilangan bulat tak bertanda dalam untuk loop C memungkinkan untuk mengekspresikan satu lingkaran atas semua bilangan bulat tak bertanda termasuk nol, tapi itu tidak mudah tanpa membawa bendera seperti di asm.)
Dengan kemungkinan loop counter sebagai kemungkinan, kompiler modern sering kali hanya "menyerah", dan tidak mengoptimalkan secara agresif.
Contoh: jumlah bilangan bulat dari 1 hingga n
Menggunakan i <= n
kekalahan unsigned clang's pengakuan idiom yang mengoptimalkan sum(1 .. n)
loop dengan bentuk tertutup berdasarkan n * (n+1) / 2
rumus 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 dari clang7.0 dan gcc8.2 pada explorer compiler 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
Tetapi untuk versi naif, kami hanya mendapatkan loop bodoh dari dentang.
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 tidak menggunakan bentuk tertutup, jadi pilihan kondisi loop tidak terlalu menyakitkan ; itu secara otomatis melakukan vektorisasi dengan penambahan integer SIMD, menjalankan 4 i
nilai secara paralel dalam elemen register 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.
Ini juga memiliki loop skalar biasa yang saya pikir itu digunakan untuk sangat kecil n
, dan / atau untuk kasus loop tak terbatas.
BTW, kedua loop ini membuang-buang instruksi (dan uop pada Sandybridge-family CPUs) pada overhead loop. sub eax,1
/ jnz
bukannya add eax,1
/ cmp / jcc akan lebih efisien. 1 uop bukan 2 (setelah fusi makro dari sub / jcc atau cmp / jcc). Kode setelah kedua loop menulis EAX tanpa syarat, sehingga tidak menggunakan nilai akhir dari penghitung loop.