64 bit DIV komutunun ikiye bölmenin iyi bir yol olduğunu düşünüyorsanız, derleyicinin asm çıkışının elle yazılmış kodunuzu geçmesine şaşmamalı -O0
(hızlı derleme, ekstra optimizasyon yok ve sonra / her C deyiminden önce bir hata ayıklayıcı değişkenleri değiştirebilir).
Bkz Agner Fog'un Optimize Meclisi kılavuzuEtkili bir asm yazmayı öğrenmek için . Ayrıca, belirli CPU'lar için özel ayrıntılar için talimat tabloları ve bir mikroarşı kılavuzu vardır. Ayrıca bakınızx86 daha mükemmel bağlantılar için etiket wiki.
Ayrıca derleyiciyi elle yazılmış asm ile yenmekle ilgili bu daha genel soruya bakın: Satır içi montaj dili yerel C ++ kodundan daha yavaş mı? . TL: DR: evet yanlış yaparsanız (bu soru gibi).
Genellikle derleyicinin işini yapmasına izin verirsiniz, özellikle de verimli bir şekilde derleyebilen C ++ yazmaya çalışırsanız . Ayrıca montaj derlenmiş dillerden daha hızlı mı? . Yanıtlardan biri, çeşitli C derleyicilerinin bazı basit işlevleri serin hilelerle nasıl optimize ettiğini gösteren bu düzgün slaytlara bağlantı veriyor . Matt Godbolt'un CppCon2017 tartışma “ Derleyicim son zamanlarda benim için ne yaptı? Derleyicinin Kapağının Açılması ”benzer bir damardadır .
even:
mov rbx, 2
xor rdx, rdx
div rbx
Intel div r64
Haswell'de 36 uops, 32-96 döngü gecikme süresi ve 21-74 döngü başına bir verim. (Artı 2 Uops RBX ve sıfır RDX kurmak için, ancak sipariş dışı yürütme bunları erken çalıştırabilir). DIV gibi yüksek üop sayımlı talimatlar mikro kodludur ve bu da ön uç darboğazlarına neden olabilir. Bu durumda, gecikme en önemli faktördür, çünkü döngü taşınan bir bağımlılık zincirinin bir parçasıdır.
shr rax, 1
aynı imzasız bölümü yapar: 1c gecikme ile 1 uop ve saat döngüsü başına 2 çalıştırabilir.
Karşılaştırma için, 32 bit bölme daha hızlıdır, ancak kaymalara karşı hala korkunçtur. idiv r32
9 uops, 22-29c gecikme süresi ve Haswell'deki 8-11c verim başına bir.
Gcc'nin-O0
asm çıkışına ( Godbolt derleyici gezgini ) bakarak görebileceğiniz gibi , sadece vardiya talimatlarını kullanır . clang -O0
, düşündüğünüz gibi safça derler, hatta 64 bit IDIV'i iki kez kullanıyor. (Optimizasyon yaparken, kaynak IDIV kullanıyorlarsa, kaynak aynı işlenenlerle bir bölme ve modül yaptığında derleyiciler her iki IDIV çıktısını kullanır)
GCC'nin tamamen saf bir modu yoktur; her zaman GIMPLE aracılığıyla dönüşüm gerçekleştirir, yani bazı "optimizasyonlar" devre dışı bırakılamaz . Buna sabit bölünmeyi tanımayı ve IDIV'den kaçınmak için vardiyaları (2'nin gücü) veya sabit nokta çarpımsal tersini (2'nin gücü olmayan) kullanmayı içerir ( div_by_13
yukarıdaki godbolt bağlantısına bakın).
gcc -Os
(boyutu optimize) yapar , ne yazık ki, hatta çarpımsal ters kodu sadece biraz daha büyük ama çok daha hızlı olduğu durumlarda, non-güç-of-2 bölünmesi için kullanımı idiv.
Derleyiciye yardım
(bu vaka için özet: kullanım uint64_t n
)
Her şeyden önce, sadece optimize edilmiş derleyici çıktısına bakmak ilginç. ( -O3
). -O0
hız temelde anlamsızdır.
Asm çıkışınıza bakın (Godbolt üzerinde veya GCC / clang montaj çıkışından "gürültü" nasıl kaldırılır? Bölümüne bakın ). Derleyici ilk etapta en uygun kodu yapmadığında: C / C ++ kaynağınızı derleyiciyi daha iyi kod yapmaya yönlendirecek şekilde yazmak genellikle en iyi yaklaşımdır . Birliği bilmeli ve neyin etkili olduğunu bilmelisiniz, ama bu bilgiyi dolaylı olarak uyguluyorsunuz. Derleyiciler de iyi bir fikir kaynağıdır: bazen clang serin bir şey yapar ve aynı şeyi yapmak için gcc'yi elle tutabilirsiniz: bu cevabı ve aşağıdaki @ Veedrac'ın kodundaki unrolled olmayan döngü ile ne yaptığımı görün.)
Bu yaklaşım taşınabilirdir ve 20 yıl içinde gelecekteki bazı derleyiciler, gelecekteki donanımda (x86 ya da değil) verimli olan her şeyi derleyebilir, belki de yeni ISA uzantısı veya otomatik vektörleme kullanarak. 15 yıl öncesinden elle yazılmış x86-64 asm, Skylake için genellikle en uygun şekilde ayarlanmaz. örneğin karşılaştır ve dal makro-füzyonu o zamanlar yoktu. Bir mikro mimariye yönelik el yapımı asm için şimdi en uygun olan, diğer mevcut ve gelecekteki CPU'lar için uygun olmayabilir. @ Johnfound'un cevabı hakkındaki yorumlar, bu kod üzerinde büyük etkisi olan AMD Bulldozer ve Intel Haswell arasındaki büyük farklılıkları tartışıyor. Ama teoride g++ -O3 -march=bdver3
ve g++ -O3 -march=skylake
doğru olanı yapacağız. (Veya -march=native
.) Veya -mtune=...
diğer CPU'ların desteklemeyebileceği talimatları kullanmadan ayarlamak için.
Benim hissettiğim derleyiciyi umursadığınız mevcut bir CPU için iyi olana yönlendirmek, gelecekteki derleyiciler için sorun olmamalı. Umarız kod dönüştürmenin yollarını bulmak için mevcut derleyicilerden daha iyidir ve gelecekteki CPU'lar için uygun bir yol bulabilirler. Ne olursa olsun, gelecek x86 muhtemelen mevcut x86'da iyi olan hiçbir şeyde korkunç olmayacak ve gelecekteki derleyici daha iyi bir şey görmüyorsa, C kaynağınızdan veri hareketi gibi bir şey uygularken asm'ye özgü tuzaklardan kaçınacaktır.
Elle yazılmış asm, optimize edici için bir kara kutudur, bu nedenle satır içi bir girdiyi derleme zamanı sabiti haline getirdiğinde sabit yayılım çalışmaz. Diğer optimizasyonlar da etkilenir. Asm'ı kullanmadan önce https://gcc.gnu.org/wiki/DontUseInlineAsm adresini okuyun . (Ve MSVC tarzı satır içi asm'dan kaçının: girişler / çıkışlar, ek yük ekleyen bellekten geçmelidir .)
Bu durumda : n
imzalı bir türünüz vardır ve gcc doğru yuvarlamayı veren SAR / SHR / ADD dizisini kullanır. (Negatif girişler için IDIV ve aritmetik kaydırma "yuvarlak" ifadeleri , bkz. SAR insn set ref manuel girişi ). (Gcc n
negatif olup olamayacağını veya ne olduğunu kanıtlayamazsa IDK . İmzalı taşma tanımsız bir davranıştır, bu yüzden yapabilmelidir.)
Kullanmalıydın uint64_t n
, böylece sadece SHR olabilir. Ve bu yüzden long
sadece 32 bit (örneğin x86-64 Windows) olan sistemler için taşınabilir .
BTW, gcc'nin optimize edilmiş asm çıkışı oldukça iyi görünüyor (kullanarak unsigned long n
) : içine girdiği iç döngü bunu main()
yapar:
# 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
İç döngü dalsızdır ve döngü taşınan bağımlılık zincirinin kritik yolu:
- 3 bileşenli LEA (3 döngü)
- cmov (Haswell'de 2 döngü, Broadwell'de veya daha sonra 1c).
Toplam: yineleme başına 5 döngü, gecikme darboğazı . Sıra dışı yürütme, buna paralel olarak her şeyle ilgilenir (teoride: 5c / iter'de gerçekten çalışıp çalışmadığını görmek için mükemmel sayaçlarla test etmedim).
cmov
(TEST tarafından üretilen) FLAGS girişinin üretilmesi, RAX girişinden (LEA-> MOV'dan) daha hızlıdır, bu nedenle kritik yolda değildir.
Benzer şekilde, CMOV'un RDI girişini üreten MOV-> SHR kritik yoldan çıkar, çünkü LEA'dan daha hızlıdır. IvyBridge ve sonraki sürümlerde MOV sıfır gecikme süresine sahiptir (kayıt yeniden adlandırma zamanında işlenir). (Hala bir uop ve boru hattında bir yuva alır, bu yüzden ücretsiz değil, sadece sıfır gecikme). LEA dep zincirindeki ekstra MOV, diğer CPU'lardaki darboğazın bir parçasıdır.
Cmp / jne de kritik yolun bir parçası değildir: döngüsel yoldan değil, çünkü kontrol bağımlılıkları kritik yoldaki veri bağımlılıklarından farklı olarak dal tahmini + spekülatif yürütme ile işlenir.
Derleyiciyi dayak
GCC burada oldukça iyi bir iş çıkardı. Bunun inc edx
yerineadd edx, 1
kullanarak bir kod baytı kaydedebilir , çünkü kimse P4 ve kısmi bayrağı değiştiren talimatlar için yanlış bağımlılıklarını umursamaz.
Ayrıca tüm MOV talimatlarını kaydedebilir ve TEST: SHR, CF = biti dışarı kaydırır, böylece / cmovc
yerine kullanabiliriz .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)
@ Johnfound'un başka bir akıllı hile için cevabına bakın: SHP'nin bayrak sonucuna dallayarak CMP'yi kaldırın ve CMOV için kullanın: sıfır, sadece n 1 (veya 0) ile başlasaydı. (Eğlenceli gerçek: Nehalem veya önceki sürümlerde bulunan SHR sayısı! = 1, bayrak sonuçlarını okursanız bir duraklamaya neden olur . Bu şekilde tek-uop yaptılar. 1'e kadar özel kodlama gayet iyi.)
MOV'dan kaçınmak Haswell'deki gecikmeye hiç yardımcı olmaz (x86'nın MOV'u gerçekten "ücretsiz" olabilir mi? Bunu neden hiç üretemiyorum? ). MOV sıfır gecikme olmayan Intel IvB öncesi ve AMD Bulldozer ailesi gibi CPU'larda önemli ölçüde yardımcı olur . Derleyicinin boşa giden MOV talimatları kritik yolu etkiler. BD'nin karmaşık LEA ve CMOV'larının her ikisi de daha düşük gecikmedir (sırasıyla 2c ve 1c), bu nedenle gecikmenin daha büyük bir kısmıdır. Ayrıca, yalnızca iki tamsayı ALU borusu olduğundan, iş akışı darboğazları bir sorun haline gelir. AMD işlemcisinden zamanlama sonuçları aldığı @ johnfound'un cevabına bakın .
Haswell'de bile, bu sürüm, kritik olmayan bir UOP'nin kritik yolda bir yürütme bağlantı noktasını çaldığı ve yürütmeyi 1 döngü geciktirdiği bazı gecikmelerden kaçınarak biraz yardımcı olabilir. (Buna kaynak çakışması denir). Ayrıca, n
serpiştirilmiş bir döngüde paralel olarak birden çok değer yaparken yardımcı olabilecek bir kayıt da kaydeder (aşağıya bakın).
LEA'nın gecikmesi adresleme moduna , Intel SnB ailesi CPU'lara bağlıdır. 3c için 3c ( [base+idx+const]
iki ayrı ekleme alır), ancak sadece 2c veya daha az bileşenli 1c (bir ekleme). Bazı CPU'lar (Core2 gibi) tek bir döngüde 3 bileşenli bir LEA bile yapar, ancak SnB ailesi bunu yapmaz. Daha da kötüsü, Intel SnB ailesi gecikmeleri standartlaştırır, böylece 2c uops yoktur , aksi takdirde 3 bileşenli LEA, Bulldozer gibi sadece 2c olur. (3 bileşenli LEA, AMD'de de daha yavaş değil, daha yavaştır).
Yani lea rcx, [rax + rax*2]
/ Haswell gibi Intel SnB ailesi işlemcilerde inc rcx
sadece 2c gecikme süresine sahiptir lea rcx, [rax + rax*2 + 1]
. BD'de bile, Core2'de ise daha da kötüsü. Normalde 1c gecikme süresinden tasarruf etmeye değmeyecek ekstra bir ÜOP'ye mal olur, ancak gecikme burada en büyük darboğazdır ve Haswell ekstra üop verimini işlemek için yeterince geniş bir boru hattına sahiptir.
Ne gcc, icc ne de clang (godbolt üzerinde) SHR'nin CF çıkışını kullanmaz, her zaman bir AND veya TEST kullanır . Aptal derleyiciler. : P Çok karmaşık makinelerdir, ancak zeki bir insan onları genellikle küçük ölçekli problemlerde yenebilir. (Bunu düşünmek için binlerce ila milyonlarca kez daha uzun bir süre göz önüne alındığında, elbette! Derleyiciler, işleri yapmak için mümkün olan her yolu aramak için kapsamlı algoritmalar kullanmazlar, çünkü çok fazla satır içi kodu optimize ederken bu çok uzun sürer. Ayrıca boru hattını hedef mikromimaride modellemezler, en azından IACA veya diğer statik analiz araçlarıyla aynı ayrıntıda değil ; sadece bazı buluşsal yöntemler kullanırlar.)
Basit döngü açma yardımcı olmaz ; bu döngü, döngü tepegözünde / veriminde değil, döngüde taşınan bir bağımlılık zincirinin gecikmesini azaltır. Bu, CPU'nun iki iş parçacığından gelen talimatları serpiştirmek için çok zamana sahip olduğu için hiper iş parçacığıyla (veya başka bir SMT türüyle) iyi olacağı anlamına gelir. Bu, döngüyü içeri paralel hale getirme anlamına gelir main
, ancak bu iyidir, çünkü her evre sadece bir değer aralığını kontrol edebilir n
ve sonuç olarak bir çift tamsayı üretebilir.
Tek bir iplik içinde elle serpiştirmek de uygun olabilir . Belki bir çift sayı için diziyi paralel olarak hesaplayın, çünkü her biri sadece birkaç kayıt alır ve hepsi aynı max
/ maxi
. Bu daha fazla talimat düzeyinde paralellik yaratır .
Hile, başka bir başlangıç değeri elde etmeden önce tüm n
değerlere ulaşılıp 1
beklenmeyeceğine n
veya diğer sekansın kayıtlarına dokunmadan son koşula ulaşan sadece bir tane için yeni bir başlangıç noktası alıp almayacağına karar verir. Muhtemelen her zincirin yararlı veriler üzerinde çalışmasını sağlamak en iyisidir, aksi takdirde sayacını şartlı olarak arttırmanız gerekir.
Hatta bunu henüz n
ulaşılmayan vektör öğeleri için sayacı şartlı olarak artırmak için SSE paketlenmiş karşılaştırmalı öğelerle bile yapabilirsiniz 1
. Daha sonra bir SIMD koşullu artış uygulamasının daha da uzun gecikmesini gizlemek n
için havada daha fazla değer vektörü tutmanız gerekir . Belki sadece 256b vektör (4x uint64_t
) ile değer .
Ben bir 1
"yapışkan" tespiti yapmak için en iyi strateji sayacı artırmak için eklediğiniz herkesin vektörü maskelemek olduğunu düşünüyorum. 1
Bir öğede bir öğeyi gördükten sonra , arttırma vektörü sıfır olacaktır ve + = 0 bir işlem yapılmaz.
Manuel vektörleştirme için denenmemiş fikir
# 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.
Bunu elle yazılmış asm yerine intrinseklerle uygulayabilir ve uygulamalısınız.
Algoritmik / uygulama geliştirme:
Aynı mantığı daha verimli bir asm ile uygulamanın yanı sıra, mantığı basitleştirmenin veya gereksiz çalışmalardan kaçınmanın yollarını arayın. örneğin, dizilerdeki ortak sonları saptamak için not edin. Ya da daha iyisi, aynı anda 8 bite bakın (gnasher'ın cevabı)
@ EOF, tzcnt
(veya bsf
) n/=2
bir adımda birden çok yineleme yapmak için kullanılabileceğine işaret eder . Bu muhtemelen SIMD vektörleştirmesinden daha iyidir; hiçbir SSE veya AVX komutu bunu yapamaz. Yine de n
, farklı tamsayı kayıtlarında paralel olarak birden fazla skaler yapmakla uyumludur .
Yani döngü şöyle görünebilir:
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);
Bu, önemli ölçüde daha az yineleme yapabilir, ancak BMI2 içermeyen Intel SnB ailesi CPU'larda değişken sayımlı kaymalar yavaştır. 3 uops, 2c gecikme. (FLAGS'a giriş bağımlılıkları vardır çünkü count = 0, işaretlerin değiştirilmediği anlamına gelir. Bunu bir veri bağımlılığı olarak ele alırlar ve bir uop yalnızca 2 girişe sahip olabilirler (zaten HSW / BDW öncesi)). Bu, x86'nın çılgın-CISC tasarımından şikayet eden insanların bahsettiği tür. X86 CPU'ları, ISA bugün benzer şekilde bile sıfırdan tasarlanmışsa, olduğundan daha yavaş hale getirir. (yani bu, hız / güç gerektiren "x86 vergisinin" bir parçasıdır.) SHRX / SHLX / SARX (BMI2) büyük bir kazançtır (1 uop / 1c gecikme).
Ayrıca tzcnt'yi (Haswell ve daha sonra 3c) kritik yola koyar, bu nedenle döngü taşınan bağımlılık zincirinin toplam gecikmesini önemli ölçüde uzatır. Yine de, bir CMOV veya kayıt tutma hazırlığı ihtiyacını ortadan kaldırır n>>1
. @ Veedrac'ın cevabı, çok etkili olan çoklu iterasyonlar için tzcnt / shift'i erteleyerek tüm bunların üstesinden gelir (aşağıya bakınız).
BSF veya TZCNT'yi birbirinin yerine kullanabiliriz , çünkü n
o noktada asla sıfır olamaz. TZCNT'nin makine kodu, BMI1'i desteklemeyen CPU'larda BSF olarak çözülür. (Anlamsız önekler göz ardı edilir, bu nedenle REP BSF BSF olarak çalışır).
TZCNT, onu destekleyen AMD CPU'larda BSF'den çok daha iyi performans gösterir, bu nedenle REP BSF
giriş çıkıştan ziyade sıfır ise ZF'yi ayarlamayı umursamanız bile , kullanmak iyi bir fikir olabilir . Bazı derleyiciler bunu __builtin_ctzll
bile kullandığınızda yapar -mno-bmi
.
Intel CPU'larda da aynı şeyi yaparlar, bu yüzden önemli olan tek şey baytı saklayın. Intel'deki (Skylake öncesi) TZCNT'nin, giriş = 0 olan BSF'nin hedefini değiştirmeden bıraktığı belgelenmemiş davranışı desteklemek için, tıpkı BSF gibi, sözde sadece yazma çıktı işlenenine yanlış bağımlılığı vardır. Bu nedenle, yalnızca Skylake için optimizasyon yapmadıkça bunun etrafında çalışmanız gerekir, bu nedenle ekstra REP baytından kazanılacak hiçbir şey yoktur. (Intel, x86 ISA kılavuzunun gerektirdiği şeyin ötesine geçer, olmaması gereken bir şeye bağlı olan veya geriye dönük olarak izin verilmeyen bir kodun kırılmasını önlemek için. Örneğin, Windows 9x'in güvenli olduğu TLB girişlerinin spekülatif olarak önceden getirilmesi kabul edilmez. kod yazıldığında, Intel TLB yönetim kurallarını güncellemeden önce .)
Her neyse, Haswell'deki LZCNT / TZCNT, POPCNT ile aynı yanlış dep'e sahip: bu Soru & Cevap'ya bakın . Bu nedenle gcc'nin @ Veedrac'ın kodu için asm çıkışında, dst = src kullanmadığında TZCNT'nin hedefi olarak kullanmak üzere olan kayıtta dep zincirini xor-zeroing ile kırdığını görüyorsunuz. TZCNT / LZCNT / POPCNT, hedeflerini hiçbir zaman tanımsız veya değiştirilmemiş bırakmadığından, Intel CPU'lardaki çıkışa bu yanlış bağımlılık bir performans hatası / sınırlamasıdır. Muhtemelen bazı transistörlerin / güçlerin aynı yürütme birimine giden diğer uopslar gibi davranmalarına değer. Tek üstünlük, başka bir uarch sınırlaması ile etkileşimdir: bir bellek operandını dizinlenmiş adresleme modu ile mikro kaynaştırabilirler Haswell'de, ancak Intel'in LZCNT / TZCNT için yanlış dep'i kaldırdığı Skylake'de, endeksli adresleme modlarını "laminatsız" hale getirirken, POPCNT hala herhangi bir addr modunu mikro kaynaştırabilir.
Diğer cevaplardaki fikir / kod iyileştirmeleri:
@ hidefromkgb'ın cevabı , 3n + 1'den sonra bir sağ vardiya yapabileceğinizin garantili olduğunu gözlemliyor . Bunu, adımlar arasındaki kontrolleri atlamaktan daha verimli bir şekilde hesaplayabilirsiniz. Bu cevaptaki asm uygulaması kırılmış olsa da (SHRD'den sonra bir sayım> 1 olan tanımsız olan OF'ye bağlıdır) ve yavaş: ROR rdi,2
daha hızlıdır SHRD rdi,rdi,2
ve kritik yolda iki CMOV talimatı kullanmak ekstra bir TEST'ten daha yavaştır paralel olarak çalışabilir.
Tidied / geliştirilmiş C'yi (derleyiciyi daha iyi asm üretmeye yönlendirir) koydum ve Godbolt üzerinde + C daha hızlı asm (C'nin altındaki yorumlarda) çalıştığını test ettim: @ hidefromkgb'ın cevabındaki bağlantıya bakın . (Bu yanıt, büyük Godbolt URL'lerinden 30 bin karakter sınırına ulaştı, ancak kısa bağlantılar çürüyebilir ve yine de goo.gl için çok uzundu.)
Ayrıca bir dizeye dönüştürmek write()
ve bir kerede bir karakter yazmak yerine bir çıktı yapmak için çıktı-baskı geliştirildi . Bu, perf stat ./collatz
(performans sayaçlarını kaydetmek için) ile tüm programın zamanlaması üzerindeki etkiyi en aza indirir ve kritik olmayan asmın bazılarını gizlemiştim.
@ Veedrac'ın kodu
İhtiyacımız olduğunu bildiğimiz kadar sağa kaymadan ve döngüye devam etmek için kontrol ettiğimde küçük bir hız kazandım . Sınır = 1e8 için 7.5s'den Core2Duo'da (Merom) 16.7'lik bir açma faktörü ile 7.275s'ye kadar.
kodu + Godbolt üzerine yorumlar . Bu sürümü clang ile kullanmayın; erteleme döngüsüyle aptalca bir şey yapar. Bir tmp sayacı kullanmak k
ve daha sonra eklemek count
daha sonra clang ne yapar, ancak bu biraz gcc acıyor.
Yorumlardaki tartışmaya bakın: Veedrac'ın kodu BMI1'e sahip CPU'larda mükemmeldir (yani Celeron / Pentium değil)