Jeśli uważasz, że 64-bitowa instrukcja DIV jest dobrym sposobem na podzielenie przez dwa, to nic dziwnego, że dane wyjściowe asm kompilatora pobiły Twój ręcznie napisany kod, nawet przy pomocy -O0
(szybka kompilacja, bez dodatkowej optymalizacji i przechowywanie / przeładowywanie do pamięci po / przed każdą instrukcją C, aby debugger mógł modyfikować zmienne).
Zobacz przewodnik dotyczący optymalizacji w Agner Fog, aby dowiedzieć się, jak pisać efektywne asm. Ma również tabele instrukcji i przewodnik po mikroarchaach, w którym znajdują się szczegółowe informacje na temat konkretnych procesorów. Zobacz takżex86 otaguj wiki, aby uzyskać więcej linków perf.
Zobacz także to bardziej ogólne pytanie o pokonanie kompilatora przy pomocy odręcznie napisanego asm: Czy wbudowany język asemblera jest wolniejszy niż natywny kod C ++? . TL: DR: tak, jeśli zrobisz to źle (jak to pytanie).
Zwykle pozwalasz kompilatorowi na wykonanie swoich zadań, zwłaszcza jeśli próbujesz napisać C ++, który potrafi się efektywnie kompilować . Zobacz także, czy asembler jest szybszy niż skompilowane języki? . Jedna z odpowiedzi prowadzi do tych schludnych slajdów pokazujących, jak różne kompilatory C optymalizują niektóre naprawdę proste funkcje za pomocą fajnych sztuczek. Rozmowa CppCon2017 Matta Godbolta „ Co ostatnio zrobił dla mnie mój kompilator? Odblokowanie pokrywy kompilatora ”ma podobny charakter.
even:
mov rbx, 2
xor rdx, rdx
div rbx
Na Intel Haswell div r64
jest 36 uops, z opóźnieniem 32-96 cykli i przepustowością jednego na 21-74 cykli. (Plus 2 ups, aby skonfigurować RBX i zero RDX, ale wykonanie poza kolejnością może uruchomić je wcześniej). Instrukcje o wysokiej liczbie uopów, takie jak DIV, są mikrokodowane, co może również powodować wąskie gardła w interfejsie użytkownika. W tym przypadku opóźnienie jest najistotniejszym czynnikiem, ponieważ jest częścią łańcucha zależności przenoszonego przez pętlę.
shr rax, 1
robi ten sam niepodpisany podział: ma 1 opóźnienie, z opóźnieniem 1c i może działać 2 na cykl zegara.
Dla porównania podział 32-bitowy jest szybszy, ale nadal straszny w porównaniu do zmian. idiv r32
wynosi 9 ups, opóźnienie 22-29c i jeden na przepustowość 8-11c w Haswell.
Jak widać z -O0
danych wyjściowych asm gcc ( eksplorator kompilatora Godbolt ), używa on tylko instrukcji shift . clang -O0
kompiluje się naiwnie, tak jak myślałeś, nawet używając 64-bitowego IDIV dwukrotnie. (Podczas optymalizacji kompilatory używają obu danych wyjściowych IDIV, gdy źródło dokonuje podziału i modułu z tymi samymi operandami, jeśli w ogóle używają IDIV)
GCC nie ma całkowicie naiwnego trybu; zawsze przekształca się w GIMPLE, co oznacza, że niektórych „optymalizacji” nie można wyłączyć . Obejmuje to rozpoznawanie dzielenia przez stałą i stosowanie przesunięć (potęga 2) lub stałej odwrotności multiplikatywnej (brak potęgi 2), aby uniknąć IDIV (patrz div_by_13
powyższy link godbolt).
gcc -Os
(optymalizacja dla rozmiaru) robi use IDIV dla non-power-of-2 Liga, niestety nawet w przypadkach, gdy multyplikatywną kod odwrotna jest tylko nieco większy, ale znacznie szybciej.
Pomaganie kompilatorowi
(streszczenie dla tego przypadku: użyj uint64_t n
)
Po pierwsze, interesujące jest tylko zoptymalizowanie wyjścia kompilatora. ( -O3
). -O0
prędkość jest w zasadzie bez znaczenia.
Spójrz na swoje wyjście asm (na Godbolt, lub zobacz Jak usunąć „szum” z wyjścia zespołu GCC / clang? ). Kiedy kompilator nie tworzy optymalnego kodu: Pisanie źródła C / C ++ w sposób, który prowadzi kompilator do tworzenia lepszego kodu jest zazwyczaj najlepszym podejściem . Musisz znać asm i wiedzieć, co jest skuteczne, ale stosujesz tę wiedzę pośrednio. Kompilatory są również dobrym źródłem pomysłów: czasami brzęk zrobi coś fajnego i możesz przytrzymać gcc, aby zrobić to samo: zobacz tę odpowiedź i to, co zrobiłem z nierozwiniętą pętlą w kodzie @ Veedrac poniżej).
To podejście jest przenośne i za 20 lat jakiś przyszły kompilator może skompilować go na cokolwiek, co będzie wydajne na przyszłym sprzęcie (x86 lub nie), być może przy użyciu nowego rozszerzenia ISA lub auto-wektoryzacji. Ręcznie napisany x86-64 asm sprzed 15 lat zwykle nie był optymalnie dostrojony do Skylake. np. porównywanie i rozgałęzianie makr fuzji jeszcze wtedy nie istniało. To, co jest teraz optymalne dla ręcznie wykonanego asm dla jednej mikroarchitektury, może nie być optymalne dla innych obecnych i przyszłych procesorów. Komentarze do odpowiedzi @ johnfound omawiają główne różnice między AMD Bulldozer i Intel Haswell, które mają duży wpływ na ten kod. Ale teoretycznie g++ -O3 -march=bdver3
i g++ -O3 -march=skylake
zrobi to, co należy. (Lub -march=native
.) Lub -mtune=...
po prostu dostroić, bez korzystania z instrukcji, które inne procesory mogą nie obsługiwać.
Mam wrażenie, że prowadzenie kompilatora do asm, który jest dobry dla bieżącego procesora, na którym ci zależy, nie powinno stanowić problemu dla przyszłych kompilatorów. Miejmy nadzieję, że są lepsze niż obecne kompilatory w poszukiwaniu sposobów na transformację kodu i mogą znaleźć sposób, który będzie działał dla przyszłych procesorów. Niezależnie od tego, przyszłe x86 prawdopodobnie nie będzie straszne w niczym, co jest dobre na obecnym x86, a przyszły kompilator pozwoli uniknąć pułapek specyficznych dla asmów podczas implementacji czegoś takiego jak przenoszenie danych ze źródła C, jeśli nie zobaczy czegoś lepszego.
Odręcznie napisany asm jest czarną skrzynką dla optymalizatora, więc stała propagacja nie działa, gdy inlining powoduje, że dane wejściowe są stałe w czasie kompilacji. Wpływa to również na inne optymalizacje. Przeczytaj https://gcc.gnu.org/wiki/DontUseInlineAsm przed użyciem asm. (I unikaj wbudowanego asm w stylu MSVC: wejścia / wyjścia muszą przechodzić przez pamięć, co powoduje dodatkowe obciążenie ).
W tym przypadku : twój n
ma podpisany typ, a gcc używa sekwencji SAR / SHR / ADD, która daje prawidłowe zaokrąglenie. (IDIV i przesunięcie arytmetyczne „zaokrągla” inaczej dla danych ujemnych, patrz SAR wprowadzanie ustawień ręcznych ref . (IDK, jeśli gcc spróbowało i nie udało się udowodnić, że n
nie może być negatywne, czy co. Przepełnienie podpisane jest niezdefiniowanym zachowaniem, więc powinno być w stanie.)
Powinieneś był użyć uint64_t n
, aby mógł po prostu SHR. Dlatego jest przenośny dla systemów, w których long
jest tylko 32-bitowy (np. Windows x86-64).
BTW, zoptymalizowane wyjście asm gcc wygląda całkiem dobrze (przy użyciu )unsigned long n
: wewnętrzna pętla, w którą inline main()
wykonuje:
# 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
Pętla wewnętrzna nie ma rozgałęzień, a ścieżka krytyczna łańcucha zależności przenoszonej przez pętlę to:
- 3-komponentowy LEA (3 cykle)
- cmov (2 cykle na Haswell, 1c na Broadwell lub później).
Łącznie: 5 cykli na iterację, wąskie gardło związane z opóźnieniem . Wykonywanie poza kolejnością zajmuje się wszystkim innym równolegle z tym (teoretycznie: nie testowałem liczników perf, aby sprawdzić, czy naprawdę działa przy 5c / iter).
Wejście FLAGS cmov
(produkowane przez TEST) jest szybsze w produkcji niż wejście RAX (z LEA-> MOV), więc nie znajduje się na ścieżce krytycznej.
Podobnie MOV-> SHR, który wytwarza wejście RDI CMOV, znajduje się poza ścieżką krytyczną, ponieważ jest również szybszy niż LEA. MOV na IvyBridge, a później ma zerowe opóźnienie (obsługiwane w czasie zmiany nazwy rejestru). (Nadal wymaga ulepszenia i szczeliny w rurociągu, więc nie jest wolny, tylko zero opóźnień). Dodatkowe MOV w łańcuchu dep LEA jest częścią wąskiego gardła na innych procesorach.
Cmp / jne również nie jest częścią ścieżki krytycznej: nie jest przenoszony w pętli, ponieważ zależności sterujące są obsługiwane za pomocą przewidywania gałęzi + wykonania spekulatywnego, w przeciwieństwie do zależności danych na ścieżce krytycznej.
Pokonanie kompilatora
GCC wykonało tutaj całkiem dobrą robotę. inc edx
Zamiast tegoadd edx, 1
można zapisać jeden bajt kodu , ponieważ nikt nie dba o P4 i jego fałszywe zależności dla instrukcji częściowej modyfikacji flagi.
Może również zapisać wszystkie instrukcje MOV, a TEST: SHR ustawia CF = bit się przesunął, więc możemy użyć cmovc
zamiast 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)
Zobacz odpowiedź @ johnfound na inną sprytną sztuczkę: usuń CMP przez rozgałęzienie wyniku flagi SHR, a także używając go do CMOV: zero tylko, jeśli n było 1 (lub 0) na początek. (Ciekawostka: SHR z liczbą! = 1 na Nehalem lub wcześniejszym powoduje przeciągnięcie, jeśli czytasz wyniki flagi . W ten sposób zrobili to pojedynczo. Jednak specjalne kodowanie shift-by-1 jest w porządku.)
Unikanie MOV wcale nie pomaga w opóźnieniu na Haswell ( czy MOV x86 naprawdę może być „wolny”? Dlaczego w ogóle nie mogę tego odtworzyć? ). Pomaga to znacznie w procesorach takich jak Intel przed IvB i rodzina buldożerów AMD, w których MOV nie ma zerowego opóźnienia. Zmarnowane instrukcje MOV kompilatora wpływają na ścieżkę krytyczną. Kompleks LEA i CMOV BD mają zarówno niższe opóźnienie (odpowiednio 2c i 1c), więc jest to większy ułamek opóźnienia. Problemem stają się również wąskie gardła przepustowości, ponieważ ma tylko dwie całkowite rury ALU. Zobacz odpowiedź @ johnfound , gdzie ma wyniki pomiaru czasu z procesora AMD.
Nawet w Haswell ta wersja może trochę pomóc, unikając sporadycznych opóźnień, w których niekrytyczna funkcja UOP kradnie port wykonania z jednego na ścieżce krytycznej, opóźniając wykonanie o 1 cykl. (Nazywa się to konfliktem zasobów). Zapisuje również rejestr, co może być pomocne przy wykonywaniu wielu n
wartości równolegle w pętli z przeplotem (patrz poniżej).
Opóźnienie LEA zależy od trybu adresowania od procesorów z rodziny Intel SnB. 3c dla 3 składników ( [base+idx+const]
co wymaga dwóch osobnych dodatków), ale tylko 1c z 2 lub mniejszą liczbą składników (jeden dodatek). Niektóre procesory (jak Core2) wykonują nawet 3-komponentowy LEA w jednym cyklu, ale nie robi tego rodzina SnB. Co gorsza, rodzina Intel SnB standaryzuje opóźnienia, więc nie ma 2c upops , w przeciwnym razie 3-komponentowa LEA byłaby tylko 2c jak Bulldozer. (3-komponentowy LEA jest również wolniejszy na AMD, tylko nie tak bardzo).
Tak więc lea rcx, [rax + rax*2]
/ inc rcx
ma opóźnienie tylko 2c, szybsze niż lea rcx, [rax + rax*2 + 1]
w przypadku procesorów Intel SnB z rodziny takich jak Haswell. Próg rentowności na BD i gorzej na Core2. Kosztuje to dodatkowy wzrost, co zwykle nie jest warte oszczędzania opóźnienia 1c, ale opóźnienie jest tutaj głównym wąskim gardłem, a Haswell ma wystarczająco szeroki rurociąg, aby poradzić sobie z dodatkową przepustowością wzrostu.
Ani gcc, icc, ani clang (na godbolt) nie używały danych wyjściowych CF SHR, zawsze używając AND lub TEST . Głupie kompilatory. : P Są świetnymi kawałkami złożonej maszynerii, ale sprytny człowiek często potrafi je pokonać w przypadku drobnych problemów. (Biorąc pod uwagę tysiące do milionów razy dłużej o tym myśleć, oczywiście! Kompilatory nie używają wyczerpujących algorytmów do wyszukiwania każdego możliwego sposobu działania, ponieważ zajęłoby to zbyt dużo czasu podczas optymalizacji dużej ilości wstawionego kodu, co jest tym, co robią najlepiej. Nie modelują również potoku w docelowej mikroarchitekturze, przynajmniej nie tak szczegółowo, jak IACA lub inne narzędzia do analizy statycznej; używają tylko heurystyki).
Proste rozwijanie pętli nie pomoże ; to wąskie gardło pętli opóźnia łańcuch zależności przenoszonej przez pętlę, a nie narzut / przepustowość pętli. Oznacza to, że poradziłby sobie z hyperthreading (lub innym rodzajem SMT), ponieważ CPU ma dużo czasu na przeplatanie instrukcji z dwóch wątków. Oznaczałoby to równoległe ułożenie pętli main
, ale to dobrze, ponieważ każdy wątek może po prostu sprawdzić zakres n
wartości i w rezultacie wygenerować parę liczb całkowitych.
Możliwe jest również przeplatanie ręczne w jednym wątku . Może obliczyć sekwencję dla pary liczb równolegle, ponieważ każda z nich zajmuje tylko kilka rejestrów i wszystkie mogą aktualizować to samo max
/ maxi
. To tworzy więcej równoległości na poziomie instrukcji .
Sztuczka decyduje, czy poczekać, aż wszystkie n
wartości osiągną, 1
zanim uzyska kolejną parę n
wartości początkowych , czy też wyrwać się i uzyskać nowy punkt początkowy tylko dla tej, która osiągnęła warunek końcowy, bez dotykania rejestrów dla drugiej sekwencji. Prawdopodobnie najlepiej jest, aby każdy łańcuch działał na użytecznych danych, w przeciwnym razie trzeba warunkowo zwiększyć jego licznik.
Być może możesz to zrobić z pakietem SSE-porównaj, aby warunkowo zwiększyć licznik elementów wektorowych, do których jeszcze n
nie dotarł 1
. A następnie, aby ukryć jeszcze dłuższe opóźnienie implementacji warunkowego przyrostu SIMD, będziesz musiał utrzymać więcej wektorów n
wartości w powietrzu. Może warto tylko z wektorem 256b (4x uint64_t
).
Myślę, że najlepszą strategią wykrycia 1
„lepkiego” jest maskowanie wektora wszystkich, które dodajesz w celu zwiększenia licznika. Więc kiedy zobaczysz 1
element w, wektor przyrostowy będzie miał zero, a + = 0 to brak operacji.
Niezbadany pomysł na ręczną wektoryzację
# 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.
Możesz i powinieneś zaimplementować to z wewnętrznymi elementami zamiast ręcznie napisanego asm.
Poprawa algorytmu / implementacji:
Oprócz samej implementacji tej samej logiki z bardziej wydajnym asmem, poszukaj sposobów na uproszczenie logiki lub uniknięcie zbędnej pracy. np. zapamiętaj, aby wykryć wspólne zakończenia sekwencji. Albo jeszcze lepiej, spójrz na 8 bitów na raz (odpowiedź zgrzytacza)
@EOF wskazuje, że tzcnt
(lub bsf
) można wykorzystać do wielokrotnych n/=2
iteracji w jednym kroku. To prawdopodobnie lepsze niż wektoryzacja SIMD; żadna instrukcja SSE lub AVX nie może tego zrobić. n
Jednak nadal jest kompatybilny z wykonywaniem wielu skalarów równolegle w różnych rejestrach liczb całkowitych.
Pętla może więc wyglądać następująco:
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);
Może to zrobić znacznie mniej iteracji, ale zmiany procesorów o zmiennej liczbie są powolne w procesorach z rodziny Intel SnB bez BMI2. 3 ups, opóźnienie 2c. (Mają zależność wejściową od FLAGS, ponieważ liczba = 0 oznacza, że flagi są niezmodyfikowane. Obsługują to jako zależność danych i przyjmują wiele zmian, ponieważ Uop może mieć tylko 2 dane wejściowe (w każdym razie przed HSW / BDW)). Do tego odnoszą się ludzie narzekający na zwariowany projekt CISC x86. Powoduje to, że procesory x86 są wolniejsze niż byłyby, gdyby ISA został zaprojektowany od zera, nawet w bardzo podobny sposób. (tj. jest to część „podatku x86”, który kosztuje prędkość / moc.) SHRX / SHLX / SARX (BMI2) to duża wygrana (opóźnienie 1 uop / 1c).
Umieszcza także tzcnt (3c na Haswell i późniejszych) na ścieżce krytycznej, dzięki czemu znacznie wydłuża całkowite opóźnienie łańcucha zależności przenoszonego przez pętlę. Eliminuje to jednak potrzebę posiadania CMOV lub przygotowania rejestru n>>1
. Odpowiedź @ Veedrac przezwycięża to wszystko poprzez odroczenie tzcnt / shift dla wielu iteracji, co jest wysoce skuteczne (patrz poniżej).
Możemy bezpiecznie używać BSF lub TZCNT zamiennie, ponieważ n
w tym momencie nigdy nie może być zero. Kod maszynowy TZCNT dekoduje się jako BSF na procesorach, które nie obsługują BMI1. (Prefiksy bez znaczenia są ignorowane, więc REP BSF działa jako BSF).
TZCNT działa znacznie lepiej niż BSF na procesorach AMD, które go obsługują, więc warto go używać REP BSF
, nawet jeśli nie obchodzi Cię ustawienie ZF, jeśli wejście jest zerowe, a nie wyjściowe. Niektóre kompilatory robią to, __builtin_ctzll
nawet jeśli używasz -mno-bmi
.
Działają tak samo na procesorach Intela, więc po prostu zapisz bajt, jeśli to wszystko. TZCNT na platformie Intel (przed Skylake) wciąż ma fałszywą zależność od rzekomo operandu wyjściowego tylko do zapisu, podobnie jak BSF, w celu obsługi nieudokumentowanego zachowania, że BSF z wejściem = 0 pozostawia swój cel niezmodyfikowany. Musisz więc obejść ten problem, chyba że zoptymalizujesz go tylko dla Skylake, więc nie możesz zyskać na dodatkowym bajcie REP. (Intel często wykracza poza to, czego wymaga instrukcja ISA x86, aby uniknąć łamania powszechnie używanego kodu, który zależy od czegoś, czego nie powinien, lub którego nie wolno z mocą wsteczną. Np. Windows 9x nie zakłada spekulacyjnego wstępnego pobierania wpisów TLB , co było bezpieczne kiedy kod został napisany, zanim Intel zaktualizował zasady zarządzania TLB ).
W każdym razie, LZCNT / TZCNT na Haswell mają takie same fałszywe informacje jak POPCNT: zobacz to pytania i odpowiedzi . Właśnie dlatego w wyjściu asm gcc dla kodu @ Veedrac widzisz, jak zrywa łańcuch dep z zerowaniem xor w rejestrze, który ma zamiar użyć jako miejsca docelowego TZCNT, gdy nie używa dst = src. Ponieważ TZCNT / LZCNT / POPCNT nigdy nie pozostawiają miejsca docelowego niezdefiniowanego lub niezmodyfikowanego, ta fałszywa zależność od wydajności procesorów Intela jest błędem / ograniczeniem wydajności. Przypuszczalnie warto, aby niektóre tranzystory / moc zachowywały się tak, jak inne urządzenia działające w tej samej jednostce wykonawczej. Jedyną zaletą jest interakcja z innym ograniczeniem uarch: mogą one operand pamięci z trybem indeksowanego adresowania na Haswell, ale na Skylake, gdzie Intel usunął fałszywą zależność dla LZCNT / TZCNT, „un-laminate” indeksowane tryby adresowania, podczas gdy POPCNT może nadal mikrowzłączać dowolny tryb addr.
Ulepszenia pomysłów / kodu wynikające z innych odpowiedzi:
Odpowiedź @ hidefromkgb zawiera miłą spostrzeżenie, że masz gwarancję, że będziesz w stanie wykonać jedną prawą zmianę po 3n + 1. Możesz to obliczyć jeszcze bardziej efektywnie niż pomijając sprawdzanie między krokami. Implementacja asm w tej odpowiedzi jest jednak zepsuta (zależy od OF, która jest niezdefiniowana po SHRD z liczbą> 1) i powolna: ROR rdi,2
jest szybsza niż SHRD rdi,rdi,2
, a użycie dwóch instrukcji CMOV na ścieżce krytycznej jest wolniejsze niż dodatkowy TEST które mogą działać równolegle.
Umieściłem uporządkowane / ulepszone C (które prowadzi kompilator do wytwarzania lepszego asm), i przetestowałem + działając szybciej asm (w komentarzach poniżej C) na Godbolt: patrz link w odpowiedzi @ hidefromkgb . (Ta odpowiedź osiągnęła limit 30k znaków z dużych adresów URL Godbolt, ale krótkie linki mogą gnić i były zbyt długie na goo.gl.)
Poprawiono także drukowanie wyjściowe w celu konwersji na ciąg znaków i tworzenia jednego write()
zamiast pisania jednego znaku na raz. To minimalizuje wpływ na synchronizację czasową całego programu z perf stat ./collatz
(do rejestrowania liczników wydajności), a ja zaciemniłem niektóre niekrytyczne asm.
@ Kod Veedrac
Dostałem niewielkie przyspieszenie od przesunięcia w prawo, o ile wiemy, że trzeba to zrobić, i sprawdzenia, czy mogę kontynuować pętlę. Od 7,5 s dla limitu = 1e8 do 7,275 s na Core2Duo (Merom), przy współczynniku rozwinięcia 16.
kod + komentarze na temat Godbolt . Nie używaj tej wersji z clang; robi coś głupiego z pętlą odroczenia. Użycie licznika tmp, k
a następnie dodanie go do count
późniejszych zmian zmienia działanie clang, ale to trochę boli gcc.
Zobacz dyskusję w komentarzach: kod Veedrac jest doskonały na procesorach z BMI1 (tj. Nie Celeron / Pentium)