Lebih if( a < 901 )
cepat dari if( a <= 900 )
.
Tidak persis seperti pada contoh sederhana ini, tetapi ada sedikit perubahan kinerja pada kode kompleks loop. Saya kira ini harus melakukan sesuatu dengan kode mesin yang dihasilkan kalau-kalau itu benar.
c++
performance
assembly
relational-operators
mengintip
sumber
sumber
<
dua kali lebih cepat daripada mengetik<=
.Jawaban:
Tidak, itu tidak akan lebih cepat pada kebanyakan arsitektur. Anda tidak menentukan, tetapi pada x86, semua perbandingan integral akan biasanya diimplementasikan dalam dua instruksi mesin:
test
ataucmp
instruksi, yang mengaturEFLAGS
Jcc
instruksi (lompatan) , tergantung pada jenis perbandingan (dan tata letak kode):jne
- Lompat jika tidak sama ->ZF = 0
jz
- Lompat jika nol (sama) ->ZF = 1
jg
- Lompat jika lebih besar ->ZF = 0 and SF = OF
Contoh (Diedit untuk singkatnya) Disusun dengan
$ gcc -m32 -S -masm=intel test.c
Kompilasi ke:
Dan
Kompilasi ke:
Jadi satu-satunya perbedaan antara keduanya adalah instruksi
jg
versusjge
. Keduanya akan mengambil jumlah waktu yang sama.Saya ingin menyampaikan komentar bahwa tidak ada yang menunjukkan bahwa instruksi lompatan yang berbeda membutuhkan waktu yang sama. Yang ini agak sulit dijawab, tapi inilah yang bisa saya berikan: Dalam Referensi Set Instruksi Intel , mereka semua dikelompokkan bersama di bawah satu instruksi umum,
Jcc
(Lompat jika syarat terpenuhi). Pengelompokan yang sama dibuat bersama di bawah Manual Referensi Pengoptimalan , dalam Lampiran C. Latency dan Throughput.Nilai untuk
Jcc
adalah:dengan catatan kaki berikut
Jcc
:Jadi, tidak ada dalam dokumen Intel yang pernah memperlakukan satu
Jcc
instruksi secara berbeda dari yang lainnya.Jika seseorang berpikir tentang sirkuit aktual yang digunakan untuk mengimplementasikan instruksi, seseorang dapat mengasumsikan bahwa akan ada gerbang AND / OR sederhana pada bit yang berbeda
EFLAGS
, untuk menentukan apakah kondisi terpenuhi. Maka, tidak ada alasan bahwa instruksi yang menguji dua bit harus mengambil lebih banyak atau lebih sedikit waktu dari satu pengujian hanya satu (Mengabaikan penundaan propagasi gerbang, yang jauh lebih sedikit dari periode jam.)Sunting: Floating Point
Ini berlaku untuk floating point x87 juga: (Hampir sama dengan kode di atas, tetapi dengan
double
bukannyaint
.)sumber
jg
danjnle
instruksi yang sama,7F
:-)Secara historis (kita berbicara tahun 1980-an dan awal 1990-an), ada beberapa arsitektur di mana ini benar. Masalah mendasarnya adalah perbandingan integer secara inheren diimplementasikan melalui pengurangan integer. Ini memunculkan kasus-kasus berikut.
Sekarang, ketika
A < B
pengurangan harus meminjam bit tinggi agar pengurangan itu benar, sama seperti Anda membawa dan meminjam ketika menambah dan mengurangi dengan tangan. Bit "pinjaman" ini biasanya disebut sebagai bit carry dan akan diuji oleh instruksi cabang. Bit kedua yang disebut bit nol akan ditetapkan jika pengurangannya sama dengan nol yang menyiratkan kesetaraan.Biasanya ada setidaknya dua instruksi cabang bersyarat, satu untuk cabang pada carry bit dan satu pada bit nol.
Sekarang, untuk mendapatkan inti dari masalah ini, mari kita memperluas tabel sebelumnya untuk memasukkan carry dan hasil nol bit.
Jadi, mengimplementasikan cabang untuk
A < B
dapat dilakukan dalam satu instruksi, karena carry bit hanya jelas dalam hal ini, yaitu,Tetapi, jika kita ingin melakukan perbandingan yang kurang dari atau sama, kita perlu melakukan pemeriksaan tambahan dari bendera nol untuk mengetahui kasus kesetaraan.
Jadi, pada beberapa mesin, menggunakan perbandingan "kurang dari" mungkin menghemat satu instruksi mesin . Ini relevan di era kecepatan prosesor sub-megahertz dan rasio kecepatan CPU-ke-memori 1: 1, tetapi saat ini hampir sama sekali tidak relevan.
sumber
jge
, yang menguji nol dan menandatangani / membawa bendera.<=
tes dapat diimplementasikan dalam satu instruksi dengan menukar operan dan pengujian untuknot <
(setara dengan>=
) ini adalah yang diinginkan<=
dengan operan bertukar:cmp B,A; bcs addr
. Itulah alasan mengapa tes ini dihilangkan oleh Intel, mereka menganggapnya berlebihan dan Anda tidak mampu membayar instruksi yang berlebihan pada saat-saat itu :-)Dengan asumsi kita sedang berbicara tentang tipe integer internal, tidak mungkin ada yang lebih cepat dari yang lain. Mereka jelas identik secara semantik. Mereka berdua meminta kompiler untuk melakukan hal yang persis sama. Hanya kompiler yang rusak parah akan menghasilkan kode yang lebih rendah untuk salah satunya.
Jika ada beberapa platform di mana
<
lebih cepat daripada<=
untuk tipe integer sederhana, kompiler harus selalu dikonversi<=
ke<
untuk konstanta. Kompiler apa pun yang bukan hanya kompiler buruk (untuk platform itu).sumber
<
juga<=
memiliki kecepatan hingga kompiler memutuskan kecepatan mana yang akan mereka miliki. Ini adalah optimasi yang sangat sederhana untuk kompiler ketika Anda menganggap bahwa mereka umumnya sudah melakukan optimasi kode mati, optimasi panggilan ekor, pengulangan loop (dan membuka gulungan, kadang-kadang), paralelisasi otomatis dari berbagai loop, dll ... Mengapa buang waktu merenungkan optimisasi dini ? Dapatkan prototipe berjalan, buat profilnya untuk menentukan di mana letak optimisasi paling signifikan, lakukan optimasi tersebut dalam urutan signifikansi dan profil lagi di sepanjang jalan untuk mengukur kemajuan ...(a < C)
ke(a <= C-1)
(untuk beberapa konstantaC
) menyebabkanC
lebih sulit untuk dikodekan dalam set instruksi. Sebagai contoh, satu set instruksi mungkin dapat mewakili konstanta yang ditandatangani dari -127 ke 128 dalam bentuk yang ringkas dalam perbandingan, tetapi konstanta di luar rentang itu harus dimuat menggunakan pengodean yang lebih lama, lebih lambat, atau instruksi lain seluruhnya. Jadi perbandingan seperti(a < -127)
mungkin tidak memiliki transformasi langsung.a > 127
untuka > 128
karena Anda tidak punya pilihan di sana, Anda menggunakan salah satu yang Anda butuhkan. Kami membandingkana > 127
untuka >= 128
, yang tidak dapat meminta encoding yang berbeda atau instruksi yang berbeda karena mereka memiliki tabel kebenaran yang sama. Pengkodean apa pun dari satu sama juga merupakan pengodean dari yang lain.<=
ke<
untuk konstanta". Sejauh yang saya tahu, transformasi itu melibatkan perubahan konstanta. Misalnya,a <= 42
dikompilasia < 43
karena<
lebih cepat. Dalam beberapa kasus tepi, transformasi seperti itu tidak akan membuahkan hasil karena konstanta baru mungkin memerlukan lebih banyak instruksi atau lebih lambat. Tentu sajaa > 127
dana >= 128
sama dan kompiler harus mengkodekan kedua bentuk dengan cara tercepat (sama), tapi itu tidak konsisten dengan apa yang saya katakan.Saya melihat bahwa tidak ada yang lebih cepat. Kompiler menghasilkan kode mesin yang sama di setiap kondisi dengan nilai yang berbeda.
Contoh saya
if
adalah dari GCC pada platform x86_64 di Linux.Penulis kompiler adalah orang-orang yang cukup pintar, dan mereka memikirkan hal-hal ini dan banyak dari kita menerima begitu saja.
Saya perhatikan bahwa jika itu bukan konstanta, maka kode mesin yang sama dihasilkan di kedua kasus.
sumber
if(a <=900)
untuk menunjukkan bahwa itu menghasilkan asm yang persis sama :)Untuk kode floating point, perbandingan <= mungkin memang lebih lambat (dengan satu instruksi) bahkan pada arsitektur modern. Inilah fungsi pertama:
Pada PowerPC, pertama ini melakukan perbandingan floating point (yang memperbarui
cr
, register kondisi), kemudian memindahkan register kondisi ke GPR, menggeser "dibandingkan kurang dari" bit ke tempatnya, dan kemudian kembali. Dibutuhkan empat instruksi.Sekarang pertimbangkan fungsi ini sebagai gantinya:
Ini membutuhkan pekerjaan yang sama seperti di
compare_strict
atas, tetapi sekarang ada dua bagian yang menarik: "kurang dari" dan "sama dengan." Ini membutuhkan instruksi tambahan (cror
- register kondisi bitwise ATAU) untuk menggabungkan dua bit ini menjadi satu. Jadicompare_loose
membutuhkan lima instruksi, sedangkancompare_strict
membutuhkan empat.Anda mungkin berpikir bahwa kompiler dapat mengoptimalkan fungsi kedua seperti:
Namun ini secara tidak benar akan menangani NaN.
NaN1 <= NaN2
danNaN1 > NaN2
perlu keduanya mengevaluasi ke false.sumber
fucomip
set ZF dan CF.cr
adalah setara dengan bendera sepertiZF
danCF
pada x86. (Meskipun CR lebih fleksibel.) Apa yang dibicarakan poster adalah memindahkan hasilnya ke GPR: yang mengambil dua instruksi pada PowerPC, tetapi x86 memiliki instruksi pemindahan bersyarat.Mungkin penulis buku yang tidak disebutkan namanya itu telah membaca yang
a > 0
berjalan lebih cepat daripadaa >= 1
dan berpikir itu benar secara universal.Tetapi itu karena a
0
terlibat (karenaCMP
bisa, tergantung pada arsitektur, diganti misalnya denganOR
) dan bukan karena<
.sumber
(a >= 1)
menjalankan lebih lambat daripada(a > 0)
, karena yang pertama dapat diubah secara sepele menjadi yang terakhir oleh optimizer ..Paling tidak, jika ini benar, sebuah kompiler dapat dengan sepele mengoptimalkan <= b to! (A> b), dan bahkan jika perbandingan itu sendiri lebih lambat, dengan semua kecuali kompiler yang paling naif Anda tidak akan melihat perbedaan .
sumber
NOT
baru dibuat dengan instruksi lain (je
vs.jne
)Mereka memiliki kecepatan yang sama. Mungkin dalam beberapa arsitektur khusus apa yang dia katakan benar, tetapi dalam keluarga x86 setidaknya saya tahu mereka sama. Karena untuk melakukan ini, CPU akan melakukan substraksi (a - b) dan kemudian memeriksa bendera register bendera. Dua bit dari register itu disebut ZF (zero Flag) dan SF (sign flag), dan dilakukan dalam satu siklus, karena ia akan melakukannya dengan satu operasi mask.
sumber
Ini akan sangat tergantung pada arsitektur yang mendasari bahwa C dikompilasi. Beberapa prosesor dan arsitektur mungkin memiliki instruksi eksplisit untuk sama dengan, atau kurang dari dan sama dengan, yang dijalankan dalam jumlah siklus yang berbeda.
Itu akan sangat tidak biasa, karena kompiler dapat bekerja di sekitarnya, membuatnya tidak relevan.
sumber
TL; DR jawab
Untuk sebagian besar kombinasi arsitektur, kompiler dan bahasa tidak akan lebih cepat.
Jawaban penuh
Jawaban lain telah berkonsentrasi pada x86 arsitektur, dan saya tidak tahu ARM arsitektur (yang tampaknya contoh assembler Anda untuk menjadi) cukup baik untuk berkomentar secara khusus mengenai kode yang dihasilkan, tapi ini adalah contoh dari mikro-optimasi yang adalah sangat arsitektur spesifik, dan kemungkinan menjadi anti-optimasi seperti halnya menjadi optimasi .
Karena itu, saya menyarankan bahwa optimasi mikro semacam ini adalah contoh pemrograman pemujaan kargo daripada praktik rekayasa perangkat lunak terbaik.
Mungkin ada beberapa arsitektur di mana ini merupakan optimasi, tetapi saya tahu setidaknya satu arsitektur di mana yang sebaliknya mungkin benar. Arsitektur Transputer yang dimuliakan hanya memiliki instruksi kode mesin untuk sama dengan dan lebih besar dari atau sama dengan , jadi semua perbandingan harus dibangun dari primitif ini.
Bahkan kemudian, dalam hampir semua kasus, kompiler dapat memesan instruksi evaluasi sedemikian rupa sehingga dalam praktiknya, tidak ada perbandingan yang memiliki keunggulan dibandingkan yang lain. Kasus terburuk, mungkin perlu menambahkan instruksi terbalik (REV) untuk menukar dua item teratas pada tumpukan operan . Ini adalah instruksi byte tunggal yang membutuhkan siklus tunggal untuk dijalankan, sehingga memiliki overhead sekecil mungkin.
Apakah optimasi mikro seperti ini adalah optimasi atau anti-optimasi tergantung pada arsitektur spesifik yang Anda gunakan, sehingga biasanya merupakan ide yang buruk untuk membiasakan diri menggunakan arsitektur mikro optimasi tertentu, jika tidak, Anda mungkin secara naluriah gunakan satu ketika itu tidak pantas untuk melakukannya, dan sepertinya ini adalah apa buku yang Anda baca adalah advokasi.
sumber
Anda seharusnya tidak dapat melihat perbedaannya meskipun ada. Selain itu, dalam latihan, Anda harus melakukan tambahan
a + 1
ataua - 1
membuat kondisi tetap kecuali Anda akan menggunakan beberapa konstanta sihir, yang merupakan praktik yang sangat buruk.sumber
Anda bisa mengatakan bahwa baris itu benar di sebagian besar bahasa scripting, karena karakter tambahan menghasilkan pemrosesan kode yang sedikit lebih lambat. Namun, seperti yang ditunjukkan oleh jawaban teratas, seharusnya tidak berpengaruh pada C ++, dan apa pun yang dilakukan dengan bahasa scripting mungkin tidak terlalu mementingkan optimasi.
sumber
Ketika saya menulis jawaban ini, saya hanya melihat pertanyaan judul tentang <vs <= secara umum, bukan contoh spesifik konstan
a < 901
vsa <= 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, sementaracmp 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 )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 keupper_bound=UINT_MAX
, dani <= UINT_MAX
selalu benar. Jadi loop ini tidak terbatas untuksize=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 dilakukani < size
.Asm like
if(!size) skip the loop;
do{...}while(--size);
adalah salah satu cara yang biasanya efisien untuk mengoptimalkanfor( i<size )
loop, jika nilai aktuali
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 mengoptimalkansum(1 .. n)
loop dengan bentuk tertutup berdasarkann * (n+1) / 2
rumus Gauss .x86-64 asm dari clang7.0 dan gcc8.2 pada explorer compiler Godbolt
Tetapi untuk versi naif, kami hanya mendapatkan loop bodoh dari dentang.
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.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
bukannyaadd 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.sumber
<
atau<=
. Tapi tentu saja,test ecx,ecx
/bt eax, 3
/jbe
akan melompat jika ZF diset (ecx == 0), atau jika CF disetel (bit 3 dari EAX == 1), menyebabkan batal flag parsial pada sebagian besar CPU karena flag yang dibaca tidak semuanya datang dari instruksi terakhir untuk menulis bendera apa pun. Pada keluarga Sandybridge, itu tidak benar-benar macet, hanya perlu memasukkan penggabungan.cmp
Sayatest
menulis semua flag, tetapibt
membiarkan ZF tidak dimodifikasi. felixcloutier.com/x86/btHanya jika orang yang membuat komputer buruk dengan logika boolean. Seharusnya tidak begitu.
Setiap perbandingan (
>=
<=
>
<
) dapat dilakukan dalam kecepatan yang sama.Apa setiap perbandingan adalah, hanya pengurangan (perbedaan) dan melihat apakah itu positif / negatif.
(Jika
msb
diatur, jumlahnya negatif)Bagaimana cara mengeceknya
a >= b
? Suba-b >= 0
Periksa apakaha-b
positif.Bagaimana cara mengeceknya
a <= b
? Sub0 <= b-a
Periksa apakahb-a
positif.Bagaimana cara mengeceknya
a < b
? Suba-b < 0
Periksa apakaha-b
negatif.Bagaimana cara mengeceknya
a > b
? Sub0 > b-a
Periksa apakahb-a
negatif.Sederhananya, komputer bisa melakukan ini di bawah tenda untuk operasi yang diberikan:
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 perlu melakukan
==0
atau==1
salah satunya.untuk
==0
itu bisa saja membalikkanmsb
dari rangkaian.Bagaimanapun, mereka pasti tidak
a >= b
akan dihitung sebagaia>b || a==b
lolsumber