I'm membaca sebuah buku di mana penulis mengatakan bahwa jika( a < 901 )
lebih cepat dari jika( a <= 900 )
.
Tidak persis seperti dalam contoh sederhana ini, tapi ada sedikit perubahan kinerja pada loop kode yang kompleks. Saya kira ini harus melakukan sesuatu dengan menghasilkan kode mesin dalam kasus itu's benar-benar terjadi.
Tidak, itu tidak akan menjadi lebih cepat pada kebanyakan arsitektur. Anda tidak't menentukan, tapi pada x86, semua integral perbandingan akan biasanya dilaksanakan dalam dua instruksi mesin:
test
atau cmp
instruksi, yang menetapkan EFLAGS
Jcc
(jump) instruksi, tergantung pada perbandingan jenis (dan kode tata letak):jne
- Melompat jika tidak sama --> ZF = 0
jz
- Jump if zero (sama) --> ZF = 1
jg
- Melompat, jika lebih besar --> ZF = 0 dan SF = OF
Contoh (Edited untuk singkatnya) yang Disusun dengan $ gcc -m32 -S -masm=intel tes.c
if (a < b) {
// Do something 1
}
Mengkompilasi:
mov eax, DWORD PTR [esp+24] ; a
cmp eax, DWORD PTR [esp+28] ; b
jge .L2 ; jump if a is >= b
; Do something 1
.L2:
Dan
if (a <= b) {
// Do something 2
}
Mengkompilasi:
mov eax, DWORD PTR [esp+24] ; a
cmp eax, DWORD PTR [esp+28] ; b
jg .L5 ; jump if a is > b
; Do something 2
.L5:
Jadi satu-satunya perbedaan antara keduanya adalah jg
versus jge
instruksi. Keduanya akan mengambil jumlah waktu yang sama.
I'd seperti untuk mengatasi komentar yang tidak ada yang menunjukkan bahwa berbeda melompat petunjuk mengambil jumlah waktu yang sama. Yang satu ini sedikit rumit untuk menjawab, tapi di sini's apa yang bisa saya berikan: Di Set Instruksi Intel Referensi, mereka semua dikelompokkan bersama di bawah satu instruksi umum, Jcc
(Melompat jika kondisi terpenuhi). Sama pengelompokan dilakukan bersama-sama di bawah Optimasi Referensi Manual, di Lampiran C. Latency dan Throughput.
Latency — jumlah siklus clock yang diperlukan untuk inti eksekusi untuk menyelesaikan pelaksanaan semua µops yang membentuk sebuah instruksi.
Throughput — jumlah clock cycle yang dibutuhkan untuk menunggu sebelum masalah port adalah gratis untuk menerima instruksi yang sama lagi. Selama bertahun-petunjuk, throughput dari sebuah instruksi dapat secara signifikan kurang dari latency nya
Nilai untuk Jcc
adalah:
Latency Throughput
Jcc N/A 0.5
berikut catatan kaki pada Jcc
:
- Pemilihan bersyarat jump instruksi harus didasarkan pada rekomendasi dari bagian Bagian 3.4.1, "Cabang Prediksi Optimalisasi," untuk meningkatkan prediktabilitas dari cabang-cabang. Ketika cabang-cabang yang diprediksi berhasil, latency
jcc
lebih efektif nol.
Jadi, tidak ada di Intel docs pernah memperlakukan satu Jcc
instruksi dengan cara yang berbeda dari yang lain.
Jika seseorang berpikir tentang yang sebenarnya sirkuit yang digunakan untuk melaksanakan petunjuk, seseorang dapat berasumsi bahwa tidak akan menjadi sederhana DAN/ATAU gates pada bit yang berbeda di EFLAGS
, untuk menentukan apakah kondisi terpenuhi. Maka, tidak ada alasan bahwa sebuah instruksi pengujian dua bit harus mengambil waktu lebih atau kurang dari satu pengujian hanya satu (Mengabaikan gate delay propagasi, yang jauh kurang dari jam periode.)
Edit: Floating Point
Hal ini berlaku untuk x87 floating point juga: (Cukup banyak kode yang sama seperti di atas, tetapi dengan double
bukan int
.)
fld QWORD PTR [esp+32]
fld QWORD PTR [esp+40]
fucomip st, st(1) ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
fstp st(0)
seta al ; Set al if above (CF=0 and ZF=0).
test al, al
je .L2
; Do something 1
.L2:
fld QWORD PTR [esp+32]
fld QWORD PTR [esp+40]
fucomip st, st(1) ; (same thing as above)
fstp st(0)
setae al ; Set al if above or equal (CF=0).
test al, al
je .L5
; Do something 2
.L5:
leave
ret
Secara historis (kita're berbicara 1980-an dan awal 1990-an), ada beberapa arsitektur di mana ini adalah benar. Akar masalah adalah bahwa perbandingan bilangan bulat pada dasarnya dilaksanakan melalui pengurangan bilangan bulat. Hal ini menimbulkan kasus-kasus berikut.
Comparison Subtraction
---------- -----------
A < B --> A - B < 0
A = B --> A - B = 0
A > B --> A - B > 0
Sekarang, ketika <
B pengurangan telah meminjam tinggi-bit untuk pengurangan untuk menjadi benar, seperti anda membawa dan meminjam ketika menambahkan dan mengurangkan dengan tangan. Ini "dipinjam" sedikit itu biasanya disebut sebagai membawa sedikit dan akan diuji oleh cabang instruksi. Kedua bit yang disebut nol bit akan ditetapkan jika pengurangan yang identik nol yang tersirat kesetaraan.
Biasanya setidaknya dua cabang bersyarat instruksi, satu untuk cabang di bawa sedikit dan satu di nol bit.
Sekarang, untuk mendapatkan di jantung dari masalah ini, mari's memperluas tabel sebelumnya untuk memasukkan membawa dan nol sedikit hasil.
Comparison Subtraction Carry Bit Zero Bit
---------- ----------- --------- --------
A < B --> A - B < 0 0 0
A = B --> A - B = 0 1 1
A > B --> A - B > 0 1 0
Jadi, pelaksana cabang untuk A < B
dapat dilakukan dalam satu instruksi, karena membawa sedikit lebih jelas hanya dalam hal ini, , yaitu,
;; Implementation of "if (A < B) goto address;"
cmp A, B ;; compare A to B
bcz address ;; Branch if Carry is Zero to the new address
Tapi, jika kita ingin melakukan kurang-dari-atau-sama perbandingan, kita perlu melakukan pemeriksaan tambahan dari nol bendera untuk menangkap hal kesetaraan.
;; Implementation of "if (A <= B) goto address;"
cmp A, B ;; compare A to B
bcz address ;; branch if A < B
bzs address ;; also, Branch if the Zero bit is Set
Jadi, pada beberapa mesin, menggunakan "kurang" perbandingan mungkin menyimpan satu mesin instruksi. Ini adalah relevan di era sub-megahertz kecepatan prosesor dan 1:1 CPU-ke-memori rasio kecepatan yang berbeda, tapi itu hampir benar-benar tidak relevan saat ini.
Dengan asumsi kita're berbicara tentang internal tipe integer, ada's tidak ada cara yang mungkin bisa lebih cepat dari yang lain. Mereka're jelas semantik yang identik. Mereka berdua meminta compiler untuk melakukan tepat hal yang sama. Hanya mengerikan rusak compiler akan menghasilkan kalah kode untuk salah satu dari ini.
Jika ada beberapa platform di mana <
lebih cepat dari <=
untuk bilangan bulat sederhana jenis, compiler harus: selalu mengkonversi <=
menjadi <
untuk konstanta. Setiap kompiler yang tidak't hanya akan menjadi buruk kompiler (untuk yang platform).
Saya melihat bahwa baik lebih cepat. Compiler menghasilkan mesin yang sama dengan kode pada setiap kondisi dengan nilai yang berbeda.
if(a < 901)
cmpl $900, -4(%rbp)
jg .L2
if(a <=901)
cmpl $901, -4(%rbp)
jg .L3
Contoh saya jika
dari GCC pada x86_64 pada platform Linux.
Compiler penulis adalah orang cerdas, dan mereka berpikir hal-hal ini dan banyak orang lain sebagian besar dari kita mengambil untuk diberikan.
Saya menyadari bahwa jika hal ini tidak konstan, maka mesin yang sama kode yang dihasilkan dalam kedua kasus.
int b;
if(a < b)
cmpl -4(%rbp), %eax
jge .L2
if(a <=b)
cmpl -4(%rbp), %eax
jg .L3
Untuk floating point code, <= perbandingan mungkin memang menjadi lebih lambat (dengan satu instruksi) bahkan pada arsitektur modern. Berikut ini's pertama fungsi:
int compare_strict(double a, double b) { return a < b; }
Pada PowerPC, pertama ini melakukan floating point perbandingan (yang update cr
, kondisi register), kemudian bergerak kondisi mendaftar ke GPR, pergeseran "dibandingkan kurang dari" sedikit ke tempatnya, dan kemudian kembali. Dibutuhkan empat petunjuk.
Sekarang perhatikan fungsi ini sebagai gantinya:
int compare_loose(double a, double b) { return a <= b; }
Hal ini membutuhkan pekerjaan yang sama sebagai compare_strict
di atas, tapi sekarang ada's dua bit minat: "kurang" dan "adalah sama dengan." Ini membutuhkan tambahan instruksi (cror
- syarat mendaftar bitwise OR) untuk menggabungkan dua bit menjadi satu. Jadi compare_loose
membutuhkan lima petunjuk, sementara compare_strict
membutuhkan empat.
Anda mungkin berpikir bahwa compiler bisa mengoptimalkan fungsi kedua seperti:
int compare_loose(double a, double b) { return ! (a > b); }
Namun hal ini akan benar menangani NaNs. NaN1 <= NaN2
dan NaN1 > NaN2
harus mengevaluasi ke false.
Mungkin penulis yang tidak disebutkan namanya buku telah membaca bahwa a > 0
berjalan lebih cepat dari a >= 1
dan berpikir bahwa ini berlaku secara universal.
Tapi ini karena 0
terlibat (karena CMP
bisa, tergantung pada arsitektur, diganti misalnya dengan ATAU
) dan bukan karena <
.
Setidaknya, jika ini benar compiler sepele bisa mengoptimalkan <= b untuk !(a > b), dan bahkan jika perbandingan itu sendiri sebenarnya lebih lambat, dengan semua tapi yang paling naif compiler anda tidak akan melihat perbedaan.
Mereka memiliki kecepatan yang sama. Mungkin dalam beberapa arsitektur khusus apa yang dia katakan adalah benar, tetapi dalam x86 keluarga setidaknya saya tahu mereka adalah sama. Karena untuk melakukan hal ini CPU akan melakukan pengurangan (a - b) dan kemudian memeriksa bendera flag register. Dua bit yang mendaftar disebut ZF (zero Flag) dan SF (sign flag), dan hal itu dilakukan dalam satu siklus, karena itu akan melakukannya dengan satu masker operasi.
Ini akan sangat tergantung pada arsitektur yang mendasari bahwa C disusun untuk. Beberapa prosesor dan arsitektur mungkin eksplisit instruksi untuk sama dengan, atau kurang dari dan sama dengan yang mengeksekusi di nomor yang berbeda dari siklus.
Itu akan sangat tidak biasa meskipun, sebagai compiler bisa bekerja di sekitar itu, sehingga tidak relevan.
Bagi kebanyakan kombinasi dari arsitektur, kompiler dan bahasa itu tidak akan menjadi lebih cepat.
Jawaban yang lain telah terkonsentrasi pada x86 arsitektur, dan saya don't tahu ARM arsitektur (yang anda contoh assembler tampaknya) cukup baik untuk berkomentar secara khusus pada kode yang dihasilkan, tapi ini adalah contoh micro-optimasi yang sangat arsitektur tertentu, dan sebagai kemungkinan untuk menjadi anti-optimasi seperti itu adalah untuk menjadi seorang optimisasi**.
Karena itu, saya akan menyarankan bahwa ini semacam micro-optimasi adalah sebuah contoh dari kultus kargo pemrograman daripada software terbaik praktek rekayasa.
Mungkin ada beberapa arsitektur di mana ini adalah aplikasi optimasi, tapi aku tahu setidaknya satu arsitektur mana yang sebaliknya mungkin benar. Yang mulia Transputer arsitektur hanya memiliki mesin kode instruksi sama dengan dan lebih besar dari atau sama dengan, jadi semua perbandingan harus dibangun dari primitif ini.
Bahkan kemudian, dalam hampir semua kasus, compiler bisa order evaluasi petunjuk sedemikian rupa sehingga dalam prakteknya, tidak ada perbandingan punya keuntungan lebih dari yang lain. Kasus terburuk sekalipun, mungkin perlu menambahkan reverse instruksi (REV) untuk swap atas dua item pada operan stack. Ini adalah satu byte instruksi yang mengambil satu siklus untuk menjalankan, jadi punya terkecil overhead mungkin.
Apakah atau tidak sebuah micro-optimasi seperti ini adalah optimalisasi * atau anti-optimasi* tergantung pada arsitektur tertentu yang anda gunakan, jadi hal ini biasanya ide yang buruk untuk mendapatkan ke dalam kebiasaan menggunakan arsitektur tertentu micro-optimisations, jika tidak, anda mungkin secara naluriah menggunakan satu ketika itu adalah tidak pantas untuk melakukannya, dan sepertinya ini adalah persis apa buku yang anda baca adalah advokasi.
Anda tidak harus mampu melihat perbedaan bahkan jika ada. Selain itu, dalam prakteknya, anda'll harus melakukan tambahan + 1
atau a - 1
untuk membuat kondisi berdiri kecuali anda're akan menggunakan beberapa sihir konstanta, yang merupakan praktek yang buruk dengan segala cara.
Anda bisa mengatakan bahwa jalur yang benar dalam banyak bahasa scripting, karena karakter tambahan hasil yang sedikit lebih lambat kode pengolahan. Namun, sebagai jawaban atas menunjukkan, hal itu seharusnya tidak memiliki efek dalam C++, dan apa pun yang dilakukan dengan bahasa scripting yang mungkin isn't yang bersangkutan tentang optimasi.
a < 901
vs a <= 900
. Banyak kompiler selalu menyusut besarnya konstanta dengan mengkonversi antara <
dan <=
, misalnya karena x86 operan langsung memiliki lebih pendek 1-byte pengkodean untuk -128..127.
Untuk LENGAN dan terutama AArch64, mampu mengkodekan karena langsung tergantung pada kemampuan untuk memutar bidang sempit ke dalam setiap posisi dalam kata. Jadi cmp w0, #0x00f000
akan encodeable, sementara cmp w0, #0x00effff
tidak mungkin. Jadi make-it-aturan yang lebih kecil untuk perbandingan vs. compile-time konstan doesn't selalu berlaku untuk AArch64. Dalam bahasa assembly pada kebanyakan mesin, perbandingan untuk <=
memiliki biaya yang sama sebagai pembanding untuk <
. Hal ini berlaku apakah anda're percabangan di atasnya, booleanizing itu untuk membuat 0/1 integer, atau menggunakannya sebagai predikat untuk branchless pilih operasi (seperti x86 CMOV). Dengan jawaban yang lain hanya memiliki disikapi ini bagian dari pertanyaan.
Tapi pertanyaan ini adalah tentang C++ operator, input untuk optimizer. Biasanya mereka're keduanya sama-sama efisien; saran dari pesan suara yang benar-benar palsu, karena penyusun dapat selalu mengubah perbandingan bahwa mereka menerapkan dalam asm. Tapi setidaknya ada satu pengecualian di mana menggunakan <=
sengaja dapat menciptakan sesuatu compiler dapat't mengoptimalkan.
Sebagai kondisi loop, ada kasus di mana <=
adalah kualitatif berbeda dari <
, ketika berhenti compiler membuktikan bahwa lingkaran adalah tidak terbatas. Hal ini dapat membuat perbedaan besar, menonaktifkan auto-vektorisasi.
Unsigned overflow adalah didefinisikan sebagai base-2 bungkus sekitar, seperti signed overflow (UB). Ditandatangani loop counter umumnya aman dari ini dengan kompiler untuk mengoptimalkan didasarkan pada ditandatangani meluap UB tidak terjadi: ++i <= ukuran
akan selalu akhirnya menjadi palsu. (Apa yang Setiap C Programmer Harus Tahu Tentang Perilaku Undefined)
batal foo(unsigned size) { unsigned upper_bound = size - 1; // atau ada perhitungan yang bisa menghasilkan UINT_MAX untuk(unsigned i=0 ; i <= upper_bound ; i++) ...
Penyusun hanya bisa mengoptimalkan cara-cara yang menjaga (didefinisikan dan legal yang dapat diamati) perilaku sumber C++ untuk semua mungkin nilai input, kecuali orang-orang yang menyebabkan perilaku tidak terdefinisi.
(Simple i <= ukuran
akan membuat masalah juga, tapi saya pikir menghitung batas atas yang lebih realistis contoh sengaja memperkenalkan kemungkinan dari loop tak terbatas untuk masukan anda don't peduli tapi yang kompilator harus dipertimbangkan.)
Dalam hal ini, size=0
menyebabkan upper_bound=UINT_MAX
, dan i <= UINT_MAX
selalu benar. Jadi, ini loop tak terbatas untuk size=0
, dan kompilator harus menghormati itu meskipun anda sebagai programmer mungkin tidak pernah berniat untuk lulus size=0. Jika kompilator dapat inline fungsi ini ke pemanggil di mana ia dapat membuktikan bahwa ukuran=0 adalah tidak mungkin, maka yang besar, hal ini dapat mengoptimalkan seperti itu bisa untuk i < size
.
Asm seperti if(!ukuran) melewati loop;
do{...}while(--ukuran);
adalah salah satu normal-cara yang efisien untuk mengoptimalkan untuk( i<size )
loop, jika nilai sebenarnya dari aku
isn't diperlukan dalam loop (https://stackoverflow.com/questions/47783926/why-are-loops-always-compiled-into-do-while-style-tail-jump/47790760#47790760).
Tapi yang do{}while dapat't menjadi tak terbatas: jika dimasukkan dengan ukuran==0
, kita mendapatkan 2^n iterasi. (https://stackoverflow.com/questions/40432995/iterating-over-all-unsigned-integers-in-a-for-loop C memungkinkan untuk mengekspresikan loop melalui semua unsigned bilangan bulat termasuk nol, tapi itu's tidak mudah tanpa membawa bendera cara itu di asm.)
Dengan sampul loop counter menjadi sebuah kemungkinan, modern compiler sering hanya "menyerah", dan't mengoptimalkan hampir sama agresif.
Menggunakan unsigned i <= n
kekalahan dentang's idiom-pengakuan yang mengoptimalkan sum(1 .. n)
loop dengan bentuk tertutup berdasarkan Gauss's n * (n+1) / 2
formula.
unsigned sum_1_to_n_finite(unsigned n) { unsigned total = 0; untuk (unsigned i = 0 ; i < n+1 ; ++i) total += i; return total; }
x86-64 asm dari clang7.0 dan gcc8.2 pada Godbolt compiler explorer
# 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
Tapi untuk naif versi, kita hanya mendapatkan bodoh loop dari dentang.
unsigned sum_1_to_n_naive(unsigned n) { unsigned total = 0; untuk (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 doesn't menggunakan bentuk tertutup dengan cara baik, sehingga pilihan kondisi loop doesn't benar-benar terluka; auto-vectorizes dengan SIMD integer selain itu, berjalan 4 aku
nilai-nilai secara paralel dalam unsur-unsur yang XMM mendaftar.
# "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.
Hal ini juga telah polos skalar loop yang saya pikir kegunaan untuk sangat kecil n
, dan/atau untuk infinite loop kasus.
BTW, kedua loop limbah instruksi (dan uop pada Sandybridge-keluarga Cpu) pada lingkaran di atas kepala. sub eax,1
/jnz
bukan add eax,1
/cmp/jcc akan lebih efisien. 1 uop bukan 2 (setelah makro-perpaduan sub/jcc atau cmp/jcc). Kode setelah kedua loop menulis EAX tanpa syarat, sehingga's tidak menggunakan nilai akhir dari loop counter.
Hanya jika orang-orang yang menciptakan komputer yang buruk dengan logika boolean. Yang seharusnya mereka't akan.
Setiap perbandingan (>=
<=
>
<
) dapat dilakukan dalam kecepatan yang sama.
Apa setiap perbandingan, hanya pengurangan (selisih) dan melihat jika itu's positif/negatif.
(Jika msb
diatur, nomor negatif)
Bagaimana untuk memeriksa a >= b
? Sub a-b >= 0
Check jika a-b
adalah positif.
Bagaimana untuk memeriksa a <= b
? Sub 0 <= b-a
Check jika b-
adalah positif.
Bagaimana untuk memeriksa a < b
? Sub a-b < 0
Check jika a-b
adalah negatif.
Bagaimana untuk memeriksa a > b
? Sub 0 > b-a
Check jika b-
adalah negatif.
Sederhananya, komputer hanya dapat melakukan ini di bawah tenda untuk diberikan op:
a >= b
== msb(a-b)==0
a <= b
== msb(b-a)==0
a > b
== msb(b-a)==1
a < b
== msb(a-b)==1
dan tentu saja komputer tidak't benar-benar perlu untuk melakukan ==0
atau ==1
baik.
untuk ==0
itu hanya bisa membalikkan msb
dari sirkuit.
Pokoknya, mereka pasti tidak't telah membuat a >= b
akan dihitung sebagai a>, b || a==b
lol