Jika Anda berpikir instruksi DIV 64-bit adalah cara yang baik untuk membaginya dengan dua, maka tidak heran output kompiler mengalahkan kode tulisan tangan Anda, bahkan dengan -O0
(kompilasi cepat, tanpa optimasi tambahan, dan simpan / muat ulang ke memori setelah / sebelum setiap pernyataan C sehingga debugger dapat memodifikasi variabel).
Lihat panduan Perakitan Mengoptimalkan Agner Fog untuk mempelajari cara menulis asm efisien. Dia juga memiliki tabel instruksi dan panduan microarch untuk detail spesifik untuk CPU tertentu. Lihat jugax86 beri tag wiki untuk lebih banyak tautan perf.
Lihat juga pertanyaan yang lebih umum tentang mengalahkan compiler dengan asm yang ditulis tangan: Apakah bahasa assembly inline lebih lambat daripada kode C ++ asli? . TL: DR: ya jika Anda salah melakukannya (seperti pertanyaan ini).
Biasanya Anda baik-baik saja membiarkan kompiler melakukan tugasnya, terutama jika Anda mencoba menulis C ++ yang dapat dikompilasi secara efisien . Lihat juga apakah perakitan lebih cepat daripada bahasa yang dikompilasi? . Salah satu tautan jawaban ke slide rapi ini menunjukkan bagaimana berbagai kompiler C mengoptimalkan beberapa fungsi yang sangat sederhana dengan trik keren. Pembicaraan CppCon2017 Matt Godbolt akhir-akhir ini, “ Apa yang Telah Dilakukan Penyusun Saya untuk Saya? Membuka kunci Tutup Pengumpul ”dengan nada yang sama.
even:
mov rbx, 2
xor rdx, rdx
div rbx
Pada Intel Haswell, div r64
adalah 36 uops, dengan latensi 32-96 siklus , dan throughput satu per 21-74 siklus. (Ditambah 2 uops untuk mengatur RBX dan nol RDX, tetapi eksekusi out-of-order dapat menjalankannya lebih awal). Instruksi penghitungan-tinggi seperti DIV di-mikrokodekan, yang juga dapat menyebabkan kemacetan front-end. Dalam hal ini, latensi adalah faktor yang paling relevan karena merupakan bagian dari rantai ketergantungan yang digerakkan oleh loop.
shr rax, 1
melakukan pembagian unsigned yang sama: Ini 1 uop, dengan latensi 1c , dan dapat menjalankan 2 siklus per jam.
Sebagai perbandingan, pembagian 32-bit lebih cepat, tetapi masih mengerikan vs bergeser. idiv r32
adalah 9 uops, 22-29c latency, dan satu per 8-11c throughput di Haswell.
Seperti yang Anda lihat dari melihat -O0
output asm gcc ( Godbolt compiler explorer ), ia hanya menggunakan instruksi shift . dentang -O0
memang mengkompilasi secara naif seperti yang Anda pikirkan, bahkan menggunakan IDIV 64-bit dua kali. (Ketika mengoptimalkan, kompiler memang menggunakan kedua output IDIV ketika sumber melakukan pembagian dan modulus dengan operan yang sama, jika mereka menggunakan IDIV sama sekali)
GCC tidak memiliki mode yang sepenuhnya naif; selalu berubah melalui GIMPLE, yang berarti beberapa "optimisasi" tidak dapat dinonaktifkan . Ini termasuk mengenali pembagian-demi-konstan dan menggunakan shift (kekuatan 2) atau invers multiplikasi titik tetap (bukan kekuatan 2) untuk menghindari IDIV (lihat div_by_13
di tautan godbolt di atas).
gcc -Os
(optimalkan untuk ukuran) memang menggunakan IDIV untuk divisi non-power-of-2, sayangnya bahkan dalam kasus-kasus di mana kode inversi multiplikasi hanya sedikit lebih besar tetapi jauh lebih cepat.
Membantu kompiler
(ringkasan untuk kasus ini: gunakan uint64_t n
)
Pertama-tama, hanya menarik untuk melihat output kompiler yang dioptimalkan. ( -O3
). -O0
kecepatan pada dasarnya tidak ada artinya.
Lihatlah output asm Anda (pada Godbolt, atau lihat Bagaimana menghapus "noise" dari GCC / output rakitan? ). Ketika kompiler tidak membuat kode optimal di tempat pertama: Menulis sumber C / C ++ Anda dengan cara yang memandu kompiler membuat kode yang lebih baik biasanya merupakan pendekatan terbaik . Anda harus tahu ASM, dan tahu apa yang efisien, tetapi Anda menerapkan pengetahuan ini secara tidak langsung. Compiler juga merupakan sumber ide yang bagus: kadang-kadang dentang akan melakukan sesuatu yang keren, dan Anda dapat menahan gcc untuk melakukan hal yang sama: lihat jawaban ini dan apa yang saya lakukan dengan loop yang tidak terbuka dalam kode @ Veedrac di bawah.)
Pendekatan ini portabel, dan dalam 20 tahun beberapa kompiler masa depan dapat mengkompilasinya ke apa pun yang efisien pada perangkat keras masa depan (x86 atau tidak), mungkin menggunakan ekstensi ISA baru atau auto-vektorisasi. Tulisan tangan x86-64 asm dari 15 tahun yang lalu biasanya tidak akan optimal untuk Skylake. misal bandingkan & cabang-fusi makro tidak ada saat itu. Apa yang optimal sekarang untuk asm kerajinan tangan untuk satu mikroarsitektur mungkin tidak optimal untuk CPU lainnya saat ini dan di masa depan. Komentar pada jawaban @ johnfound membahas perbedaan besar antara AMD Bulldozer dan Intel Haswell, yang memiliki pengaruh besar pada kode ini. Namun secara teori, g++ -O3 -march=bdver3
dan g++ -O3 -march=skylake
akan melakukan hal yang benar. (Atau -march=native
.) Atau -mtune=...
hanya menyetel, tanpa menggunakan instruksi yang mungkin tidak didukung oleh CPU lain.
Perasaan saya adalah bahwa membimbing kompiler ke asm itu bagus untuk CPU saat ini yang Anda pedulikan seharusnya tidak menjadi masalah bagi kompiler masa depan. Mereka diharapkan lebih baik daripada kompiler saat ini dalam menemukan cara untuk mengubah kode, dan dapat menemukan cara yang bekerja untuk CPU di masa depan. Apapun, x86 masa depan mungkin tidak akan mengerikan pada apa pun yang baik pada x86 saat ini, dan kompiler masa depan akan menghindari jebakan asm-spesifik saat mengimplementasikan sesuatu seperti pergerakan data dari sumber C Anda, jika tidak melihat sesuatu yang lebih baik.
ASM tulisan tangan adalah kotak hitam untuk pengoptimal, jadi propagasi konstan tidak berfungsi saat inlining menjadikan input konstanta waktu kompilasi. Optimalisasi lainnya juga terpengaruh. Baca https://gcc.gnu.org/wiki/DontUseInlineAsm sebelum menggunakan asm. (Dan hindari asline inline gaya MSVC: input / output harus melalui memori yang menambah overhead .)
Dalam hal ini : Anda n
memiliki tipe yang ditandatangani, dan gcc menggunakan urutan SAR / SHR / ADD yang memberikan pembulatan yang benar. (IDIV dan "putaran" pergeseran-aritmatika berbeda untuk input negatif, lihat SAR dan masukkan entri manual ref ). (IDK jika gcc mencoba dan gagal membuktikan bahwa itu n
tidak boleh negatif, atau apa. Signed-overflow adalah perilaku yang tidak terdefinisi, jadi seharusnya bisa.)
Anda seharusnya sudah menggunakannya uint64_t n
, jadi bisa saja SHR. Dan itu portabel untuk sistem di mana long
hanya 32-bit (misalnya x86-64 Windows).
BTW, output asm yang dioptimalkan gcc terlihat cukup baik (menggunakan )unsigned long n
: loop internal itu main()
melakukan hal ini:
# from gcc5.4 -O3 plus my comments
# edx= count=1
# rax= uint64_t n
.L9: # do{
lea rcx, [rax+1+rax*2] # rcx = 3*n + 1
mov rdi, rax
shr rdi # rdi = n>>1;
test al, 1 # set flags based on n%2 (aka n&1)
mov rax, rcx
cmove rax, rdi # n= (n%2) ? 3*n+1 : n/2;
add edx, 1 # ++count;
cmp rax, 1
jne .L9 #}while(n!=1)
cmp/branch to update max and maxi, and then do the next n
Loop dalam tidak memiliki cabang, dan jalur kritis dari rantai ketergantungan loop-carry adalah:
- 3 komponen LEA (3 siklus)
- cmov (2 siklus di Haswell, 1c di Broadwell atau lebih baru).
Total: 5 siklus per iterasi, hambatan latensi . Eksekusi out-of-order menangani semua hal lain secara paralel dengan ini (dalam teori: Saya belum menguji dengan counter perf untuk melihat apakah itu benar-benar berjalan pada 5c / iter).
Input FLAGS dari cmov
(diproduksi oleh TEST) lebih cepat untuk diproduksi daripada input RAX (dari LEA-> MOV), jadi itu bukan di jalur kritis.
Demikian pula, MOV-> SHR yang menghasilkan input RDI CMOV berada di luar jalur kritis, karena juga lebih cepat daripada LEA. MOV di IvyBridge dan yang lebih baru memiliki latensi nol (ditangani saat register-rename). (Masih membutuhkan uop, dan slot di pipeline, jadi tidak gratis, hanya nol latensi). MOV ekstra dalam rantai depa LEA adalah bagian dari hambatan pada CPU lain.
Cmp / jne juga bukan bagian dari jalur kritis: ini bukan loop-carry, karena dependensi kontrol ditangani dengan prediksi cabang + eksekusi spekulatif, tidak seperti dependensi data pada jalur kritis.
Mengalahkan kompiler
GCC melakukan pekerjaan yang cukup bagus di sini. Itu bisa menyimpan satu byte kode dengan menggunakan inc edx
alih-alihadd edx, 1
, karena tidak ada yang peduli tentang P4 dan dependensi-salahnya untuk instruksi memodifikasi flag parsial.
Itu juga bisa menyimpan semua instruksi MOV, dan TEST: SHR mengeset CF = bitnya digeser, jadi kita bisa menggunakan cmovc
alih-alih test
/ cmovz
.
### Hand-optimized version of what gcc does
.L9: #do{
lea rcx, [rax+1+rax*2] # rcx = 3*n + 1
shr rax, 1 # n>>=1; CF = n&1 = n%2
cmovc rax, rcx # n= (n&1) ? 3*n+1 : n/2;
inc edx # ++count;
cmp rax, 1
jne .L9 #}while(n!=1)
Lihat jawaban @ johnfound untuk trik pintar lainnya: hapus CMP dengan bercabang pada hasil flag SHR serta menggunakannya untuk CMOV: nol hanya jika n adalah 1 (atau 0) untuk memulai. (Fakta asyik : SHR dengan hitungan! = 1 di Nehalem atau sebelumnya menyebabkan kemacetan jika Anda membaca hasil flag . Begitulah cara mereka membuatnya menjadi satu-uop. Namun, pengodean khusus shift-by-1 baik-baik saja.)
Menghindari MOV sama sekali tidak membantu latensi di Haswell ( Bisakah MOV x86 benar-benar "bebas"? Mengapa saya tidak bisa mereproduksi ini sama sekali? ). Itu membantu secara signifikan pada CPU seperti Intel pre-IvB, dan keluarga AMD Bulldozer, di mana MOV bukan nol-latensi. Instruksi MOV yang terbuang dari kompiler mempengaruhi jalan kritis Kompleks BD-LEA dan CMOV keduanya memiliki latensi yang lebih rendah (masing-masing 2c dan 1c), jadi ini adalah fraksi yang lebih besar dari latensi. Juga, bottleneck throughput menjadi masalah, karena hanya memiliki dua pipa ALU integer. Lihat jawaban @ johnfound , di mana ia mendapatkan hasil timing dari CPU AMD.
Bahkan di Haswell, versi ini dapat sedikit membantu dengan menghindari beberapa penundaan di mana uop yang tidak kritis mencuri port eksekusi dari port yang ada di jalur kritis, menunda eksekusi dengan 1 siklus. (Ini disebut konflik sumber daya). Ini juga menyimpan register, yang dapat membantu ketika melakukan beberapa n
nilai secara paralel dalam satu loop yang disisipkan (lihat di bawah).
Latensi LEA tergantung pada mode pengalamatan , pada CPU Intel SnB-family. 3c untuk 3 komponen ( [base+idx+const]
, yang membutuhkan dua tambahan terpisah), tetapi hanya 1c dengan 2 atau lebih sedikit komponen (satu tambahan). Beberapa CPU (seperti Core2) bahkan melakukan 3 komponen LEA dalam satu siklus, tetapi SnB-family tidak. Lebih buruk lagi, keluarga Intel SnB menstandarisasi latensi sehingga tidak ada 2c uops , jika tidak, LEA 3 komponen hanya akan 2c seperti Bulldozer. (LEA 3 komponen lebih lambat pada AMD juga, hanya saja tidak sebanyak).
Jadi lea rcx, [rax + rax*2]
/ inc rcx
hanya latensi 2c, lebih cepat daripada lea rcx, [rax + rax*2 + 1]
, pada CPU Intel SnB-family seperti Haswell. Break-even di BD, dan lebih buruk di Core2. Memang membutuhkan biaya tambahan, yang biasanya tidak layak untuk menyimpan latensi 1c, tetapi latensi adalah hambatan utama di sini dan Haswell memiliki saluran pipa yang cukup luas untuk menangani throughput tambahan uop.
Baik gcc, icc, atau clang (on godbolt) menggunakan output CF SHR, selalu menggunakan AND atau TEST . Kompiler konyol. : P Mereka adalah mesin-mesin rumit yang hebat, tetapi manusia yang pandai seringkali dapat mengalahkan mereka dalam masalah skala kecil. (Diberikan ribuan hingga jutaan kali lebih lama untuk memikirkannya, tentu saja! Kompiler tidak menggunakan algoritma lengkap untuk mencari setiap cara yang mungkin untuk melakukan sesuatu, karena itu akan memakan waktu terlalu lama ketika mengoptimalkan banyak kode inline, yang adalah apa mereka melakukan yang terbaik. Mereka juga tidak memodelkan pipa dalam mikroarsitektur target, setidaknya tidak dalam detail yang sama seperti IACA atau alat analisis statis lainnya; mereka hanya menggunakan beberapa heuristik.)
Buka gulungan sederhana tidak akan membantu ; bottleneck loop ini pada latensi rantai ketergantungan loop-carry, bukan pada overhead loop / throughput. Ini berarti akan lebih baik jika menggunakan hyperthreading (atau jenis SMT lainnya), karena CPU memiliki banyak waktu untuk menyisipkan instruksi dari dua utas. Ini berarti memparalelkan loop ke dalam main
, tapi itu tidak masalah karena setiap thread dapat memeriksa rentang n
nilai dan menghasilkan sepasang integer sebagai hasilnya.
Interleaving dengan tangan dalam satu utas mungkin juga bisa dilakukan . Mungkin menghitung urutan untuk sepasang angka secara paralel, karena masing-masing hanya membutuhkan pasangan register, dan mereka semua dapat memperbarui yang sama max
/ maxi
. Ini menciptakan paralelisme tingkat instruksi yang lebih banyak .
Triknya adalah memutuskan apakah akan menunggu sampai semua n
nilai telah tercapai 1
sebelum mendapatkan pasangan lain dari n
nilai awal , atau apakah akan keluar dan mendapatkan titik awal baru untuk hanya satu yang mencapai kondisi akhir, tanpa menyentuh register untuk urutan lainnya. Mungkin yang terbaik adalah menjaga setiap rantai bekerja pada data yang berguna, jika tidak Anda harus meningkatkan penghitungnya secara kondisional.
Anda mungkin bahkan dapat melakukan ini dengan hal-hal yang dibungkus-bandingkan SSE untuk meningkatkan penghitung untuk elemen vektor di mana n
belum tercapai 1
. Dan untuk menyembunyikan latensi yang lebih lama dari implementasi kenaikan-kondisional SIMD, Anda harus menjaga lebih banyak vektor n
nilai di udara. Mungkin hanya bernilai dengan vektor 256b (4x uint64_t
).
Saya pikir strategi terbaik untuk membuat deteksi 1
"lengket" adalah dengan menutupi vektor semua yang Anda tambahkan untuk menambah penghitung. Jadi setelah Anda melihat 1
sebuah elemen, vektor-kenaikan akan memiliki nol, dan + = 0 adalah no-op.
Gagasan yang belum diuji untuk vektorisasi manual
# starting with YMM0 = [ n_d, n_c, n_b, n_a ] (64-bit elements)
# ymm4 = _mm256_set1_epi64x(1): increment vector
# ymm5 = all-zeros: count vector
.inner_loop:
vpaddq ymm1, ymm0, xmm0
vpaddq ymm1, ymm1, xmm0
vpaddq ymm1, ymm1, set1_epi64(1) # ymm1= 3*n + 1. Maybe could do this more efficiently?
vprllq ymm3, ymm0, 63 # shift bit 1 to the sign bit
vpsrlq ymm0, ymm0, 1 # n /= 2
# FP blend between integer insns may cost extra bypass latency, but integer blends don't have 1 bit controlling a whole qword.
vpblendvpd ymm0, ymm0, ymm1, ymm3 # variable blend controlled by the sign bit of each 64-bit element. I might have the source operands backwards, I always have to look this up.
# ymm0 = updated n in each element.
vpcmpeqq ymm1, ymm0, set1_epi64(1)
vpandn ymm4, ymm1, ymm4 # zero out elements of ymm4 where the compare was true
vpaddq ymm5, ymm5, ymm4 # count++ in elements where n has never been == 1
vptest ymm4, ymm4
jnz .inner_loop
# Fall through when all the n values have reached 1 at some point, and our increment vector is all-zero
vextracti128 ymm0, ymm5, 1
vpmaxq .... crap this doesn't exist
# Actually just delay doing a horizontal max until the very very end. But you need some way to record max and maxi.
Anda dapat dan harus menerapkan ini dengan intrinsik alih-alih asm yang ditulis tangan.
Peningkatan algoritma / implementasi:
Selain hanya menerapkan logika yang sama dengan asm yang lebih efisien, cari cara untuk menyederhanakan logika, atau menghindari pekerjaan yang berlebihan. mis. memoize untuk mendeteksi akhiran umum untuk urutan. Atau bahkan lebih baik, lihat 8 bit tambahan sekaligus (jawaban gnasher)
@ EOF menunjukkan bahwa tzcnt
(atau bsf
) dapat digunakan untuk melakukan beberapa n/=2
iterasi dalam satu langkah. Itu mungkin lebih baik daripada vektorisasi SIMD; tidak ada instruksi SSE atau AVX yang dapat melakukannya. Ini masih kompatibel dengan melakukan beberapa skalar n
secara paralel di register integer yang berbeda.
Jadi lingkarannya mungkin terlihat seperti ini:
goto loop_entry; // C++ structured like the asm, for illustration only
do {
n = n*3 + 1;
loop_entry:
shift = _tzcnt_u64(n);
n >>= shift;
count += shift;
} while(n != 1);
Ini mungkin melakukan iterasi yang jauh lebih sedikit, tetapi perubahan jumlah variabel lambat pada CPU Intel SnB-family tanpa BMI2. 3 uops, 2c latency. (Mereka memiliki ketergantungan input pada FLAGS karena hitungan = 0 berarti bendera tidak dimodifikasi. Mereka menangani ini sebagai ketergantungan data, dan mengambil beberapa uops karena uop hanya dapat memiliki 2 input (toh HSW / BDW tetap)). Ini adalah jenis yang dikeluhkan orang tentang desain crazy-CISC x86. Itu membuat CPU x86 lebih lambat dari yang seharusnya jika ISA dirancang dari awal hari ini, bahkan dengan cara yang hampir sama. (Yaitu ini adalah bagian dari "pajak x86" yang membutuhkan kecepatan / daya.) SHRX / SHLX / SARX (BMI2) adalah kemenangan besar (latensi 1 uop / 1c).
Ini juga menempatkan tzcnt (3c di Haswell dan yang lebih baru) di jalur kritis, sehingga secara signifikan memperpanjang latensi total rantai ketergantungan loop-carry. Itu menghilangkan kebutuhan untuk CMOV, atau untuk mempersiapkan holding register n>>1
. @ Veedrac menjawab semua ini dengan menunda tzcnt / shift untuk beberapa iterasi, yang sangat efektif (lihat di bawah).
Kita dapat menggunakan BSF atau TZCNT dengan aman secara bergantian, karena n
tidak pernah bisa nol pada saat itu. Kode mesin TZCNT mendekode sebagai BSF pada CPU yang tidak mendukung BMI1. (Awalan tanpa arti diabaikan, jadi REP BSF berjalan sebagai BSF).
TZCNT berkinerja jauh lebih baik daripada BSF pada CPU AMD yang mendukungnya, jadi itu bisa menjadi ide yang baik untuk digunakan REP BSF
, bahkan jika Anda tidak peduli tentang pengaturan ZF jika inputnya nol daripada output. Beberapa kompiler melakukan ini saat Anda menggunakannya __builtin_ctzll
bahkan dengan -mno-bmi
.
Mereka melakukan hal yang sama pada CPU Intel, jadi simpan saja byte jika itu yang terpenting. TZCNT pada Intel (pra-Skylake) masih memiliki ketergantungan salah pada operan output yang seharusnya hanya ditulis, seperti BSF, untuk mendukung perilaku tidak berdokumen bahwa BSF dengan input = 0 membuat tujuannya tidak dimodifikasi. Jadi Anda perlu mengatasinya kecuali hanya mengoptimalkan untuk Skylake, jadi tidak ada untungnya dari byte REP tambahan. (Intel sering melampaui apa yang disyaratkan manual x86 ISA, untuk menghindari pemecahan kode yang digunakan secara luas yang bergantung pada sesuatu yang seharusnya tidak ada, atau yang tidak berlaku surut. Misalnya Windows 9x mengasumsikan tidak ada pengambilan prefetching spekulatif dari entri TLB , yang aman ketika kode ditulis, sebelum Intel memperbarui aturan manajemen TLB .)
Bagaimanapun, LZCNT / TZCNT di Haswell memiliki dep false yang sama dengan POPCNT: lihat T&J ini . Inilah sebabnya mengapa dalam asm output gcc untuk kode @ Veedrac, Anda melihatnya melanggar rantai dep dengan xor-zeroing pada register yang akan digunakan sebagai tujuan TZCNT ketika tidak menggunakan dst = src. Karena TZCNT / LZCNT / POPCNT tidak pernah meninggalkan tujuannya tidak terdefinisi atau tidak dimodifikasi, ketergantungan salah ini pada output pada CPU Intel adalah bug kinerja / pembatasan. Agaknya itu layak beberapa transistor / kekuatan untuk memiliki mereka berperilaku seperti uops lain yang pergi ke unit eksekusi yang sama. Satu-satunya kelebihan adalah interaksi dengan batasan uarch lain: mereka dapat micro-fuse operan memori dengan mode pengalamatan terindeks pada Haswell, tetapi pada Skylake di mana Intel menghapus dep false untuk LZCNT / TZCNT mereka "un-laminate" mode pengalamatan terindeks sementara POPCNT masih dapat melebur mikro setiap mode addr.
Perbaikan ide / kode dari jawaban lain:
@ hidefromkgb's jawaban memiliki pengamatan yang bagus bahwa Anda dijamin dapat melakukan satu shift tepat setelah 3n +1. Anda dapat menghitung ini bahkan lebih efisien daripada hanya meninggalkan cek di antara langkah-langkah. Implementasi asm dalam jawaban itu rusak, (tergantung pada OF, yang tidak didefinisikan setelah SHRD dengan hitungan> 1), dan lambat: ROR rdi,2
lebih cepat dari SHRD rdi,rdi,2
, dan menggunakan dua instruksi CMOV pada jalur kritis lebih lambat daripada TEST tambahan yang bisa berjalan secara paralel.
Saya menaruh Tidied / peningkatan C (yang memandu kompiler untuk menghasilkan asm yang lebih baik), dan menguji + bekerja lebih cepat asm (dalam komentar di bawah C) di Godbolt: lihat tautan di jawaban @ hidefromkgb . (Jawaban ini mencapai batas ar 30k dari URL Godbolt yang besar, tetapi tautan pendek dapat membusuk dan terlalu panjang untuk goo.gl.)
Juga meningkatkan hasil pencetakan untuk mengkonversi ke string dan membuat satu write()
alih-alih menulis satu karakter sekaligus. Ini meminimalkan dampak pada waktu seluruh program dengan perf stat ./collatz
(untuk merekam penghitung kinerja), dan saya menghilangkan beberapa asm non-kritis.
@ Kode Veedrac
Saya mendapat speedup minor dari menggeser ke kanan sebanyak yang kita tahu perlu lakukan, dan memeriksa untuk melanjutkan loop. Dari 7,5 untuk batas = 1e8 ke 7,275, pada Core2Duo (Merom), dengan faktor membuka gulungan 16.
kode + komentar di Godbolt . Jangan gunakan versi ini dengan dentang; ia melakukan sesuatu yang konyol dengan defer-loop. Menggunakan penghitung tmp k
dan kemudian menambahkannya untuk count
kemudian mengubah apa yang dilakukan dentang, tapi itu sedikit menyakitkan gcc.
Lihat diskusi dalam komentar: Kode Veedrac sangat baik pada CPU dengan BMI1 (yaitu bukan Celeron / Pentium)