Na tej stronie było miliard iteracji wyzwań Fibonacciego, więc pozwól nam urozmaicić wyzwanie wyzwaniem Fibonacciego o miliard iteracji!
Twoim wyzwaniem jest wyprowadzenie pierwszych 1000 cyfr dziesiętnych z 1 000 000 000. liczby Fibonacciego przy użyciu możliwie najkrótszego programu. Po tym opcjonalnie może nastąpić dowolne dodatkowe wyjście, które wybierzesz, w tym między innymi pozostałe cyfry.
Używam Konwencji, które fib 0 = 0
, fib 1 = 1
.
Twój program musi być wystarczająco szybki, abyś mógł go uruchomić i zweryfikować jego poprawność. W tym celu podajemy pierwsze 1000 cyfr:
7952317874554683467829385196197148189255542185234398913453039937343246686182519370050999626136556779332482035723222451226291714456275648259499530612111301255499879639516053459789018700567439946844843034599802419924043753401950114830107234265037841426980398387360784284231996457340782784200767760907777703183185744656536253511502851715963351023990699232595471322670365506482435966586886048627159716916351448788527427435508113909167963907380398242848033980110276370544264285032744364781198451825462130529529633339813483105771370128111851128247136311414208318983802526907917787094802217750859685116363883374847428036737147882079956688807509158372249451437519320162582002000530798309887261257028201907509370554232931107084976854715833585623910450679449120011564762925649144509531904684984417002512086504020779012501356177874199605085558317190905395134468919443313026824813363234190494375599262553025466528838122639433600483849535070647711986769279568548796855207684897741771784375859496425384355879105799
code-golf
kolmogorov-complexity
fibonacci
restricted-time
użytkownik1502040
źródło
źródło
Your program must be fast enough for you to run it and verify its correctness.
co z pamięcią?a+=b;b+=a;
pętla (może z Java BigInteger) jest oczywistym wyborem, przynajmniej jeśli myślisz o wydajności. Rekurencyjna implementacja zawsze wydawała mi się okropnie nieefektywna.write()
wywołanie systemowe). Lubię wymagania dotyczące wydajności, które sprawiły, że było to dla mnie dużo więcej zabawy.Odpowiedzi:
Python 2 + sympy, 72 bajty
Wypróbuj online!
-10 bajtów poprzez usunięcie praktycznie-0 terminu dzięki Jeffowi Dege
-1 bajtów (1000 -> 1e3 dzięki Zacharýowi)
-2 bajtów poprzez usunięcie niepotrzebnej zmiennej dzięki Erikowi Outgolfer
-2 bajtów poprzez przejście do Pythona 2 dzięki Zacharýowi
-3 bajty przez 11 '
-11
dzięki ThePirateBay -3 bajty przez zamianęstr
na backtyki dzięki notjaganteraz pokonuje nieopublikowane rozwiązanie haskell OP!
źródło
from sympy import*;sqrt
nie oszczędza bajtówimport sympy;sympy.sqrt
:)sympy
to symboliczny pakiet matematyczny dla Pythona, więc nie ma problemów z błędem zaokrąglenia, przynajmniej do bardzo dużych liczb (ta liczba nie jest wystarczająco duża lol). Następnie po prostu obliczam go, aby dać mi pierwsze cyfry 1e3, ponieważ w przeciwnym razie, jeśli usuniesz tę.evalf(1e3)
część, otrzymam bardzo krótką reprezentację notacji naukowej.Python 2 , 106 bajtów
Wypróbuj online!
Bez bibliotek, tylko arytmetyka liczb całkowitych. Działa prawie natychmiast.
Rdzeń stanowi tożsamość dziel i zwyciężaj:
To pozwala nam zaktualizować
(a,b) = (f(n),f(n+1))
dwukrotnien -> 2*n
. Ponieważ chcemy to zrobićn=10**9
, zajmuje to tylkolog_2(10**9)=30
iteracje. Budujemyn
nawet10**9
kilkakrotnie robin->2*n+c
dla każdej cyfryc
jej ekspansji binarnym. Kiedyc==1
podwojoną wartość przesuwa się2*n -> 2*n+1
o jednoetapowe przesunięcie Fibonacciego(a,b)=(b+a,b)
Aby zachować wartości
a,b
, którymi zarządzamy, przechowujemy tylko ich pierwsze1006
cyfry, dzieląc piętro,10
dopóki nie spadną2**3340 ~ 1e1006
.źródło
a,b,c=a*a+b*b,a*a-c*c,b*b+c*c
.x86 32-bitowy kod maszynowy (z wywołaniami systemowymi Linux):
106105 bajtówdziennik zmian: zapisano bajt w szybkiej wersji, ponieważ stała off-by-one nie zmienia wyniku dla Fib (1G).
Lub 102 bajty dla wersji wolniejszej o 18% (w Skylake) (używając
mov
/sub
/cmc
zamiastlea
/cmp
w wewnętrznej pętli, aby wygenerować wykonanie i zawijanie10**9
zamiast2**32
). Lub 101 bajtów dla wolniejszej wersji ~ 5,3x z odgałęzieniem w obsłudze przenoszenia w najbardziej wewnętrznej pętli. (Zmierzyłem 25,4% wskaźnika nieprzewidzianych oddziałów!)Lub 104/101 bajtów, jeśli dozwolone jest zero wiodące. (Potrzebny jest 1 dodatkowy bajt, aby pominąć 1 cyfrę kodu wyjściowego, co jest potrzebne w przypadku Fib (10 ** 9)).
Niestety tryb NASM TIO wydaje się ignorować
-felf32
flagi kompilatora. Oto i tak link do mojego pełnego kodu źródłowego, z całym bałaganem eksperymentalnych pomysłów w komentarzach.To jest kompletny program . Drukuje pierwsze 1000 cyfr Fib (10 ** 9), a następnie kilka dodatkowych cyfr (kilka ostatnich jest niepoprawnych), a następnie kilka bajtów śmieci (bez nowego wiersza). Większość śmieci nie jest ASCII, więc możesz chcieć przepłynąć
cat -v
.konsole
Jednak nie psuje mojego emulatora terminali (KDE ). „Bajty śmieci” przechowują Fib (999999999). Miałem już-1024
w rejestrze, więc taniej było wydrukować 1024 bajty niż odpowiedni rozmiar.Liczę tylko kod maszynowy (rozmiar segmentu tekstowego mojego statycznego pliku wykonywalnego), a nie puch, który sprawia, że jest to plik wykonywalny ELF. ( Możliwe są bardzo małe pliki wykonywalne ELF , ale nie chciałem się tym przejmować). Okazało się, że użycie pamięci stosu zamiast BSS jest krótsze, więc mogę uzasadnić, że nie liczę niczego innego w pliku binarnym, ponieważ nie zależę od żadnych metadanych. (Generowanie statycznego pliku binarnego z rozkładem w normalny sposób powoduje, że 340-bajtowy plik ELF jest wykonywalny.)
Z tego kodu można utworzyć funkcję, którą można wywołać z C. Zapisanie / przywrócenie wskaźnika stosu (być może w rejestrze MMX) może kosztować kilka bajtów, a także inne koszty ogólne, ale także zaoszczędzić bajty, zwracając ciąg znaków w pamięci zamiast
write(1,buf,len)
wywoływać system. Myślę, że golf w kodzie maszynowym powinien dać mi trochę luzu, ponieważ nikt inny nawet nie opublikował odpowiedzi w żadnym języku bez natywnej rozszerzonej precyzji, ale myślę, że wersja funkcji tego powinna nadal mieć mniej niż 120 bajtów bez ponownego gry w golfa rzecz.Algorytm:
brutalna siła
a+=b; swap(a,b)
, w razie potrzeby obcinana, aby zachować tylko wiodące> = 1017 cyfr dziesiętnych. Działa w ciągu 1 minuty 13 sekund na moim komputerze (lub 322,47 miliarda cykli zegara + - 0,05%) (i może być kilka% szybszy z kilkoma dodatkowymi bajtami rozmiaru kodu lub do 62 sekund przy znacznie większym rozmiarze kodu z rozwijania pętli. Nie sprytna matematyka, wykonująca tę samą pracę przy mniejszym obciążeniu). Opiera się na implementacji Pythona @ AndersKaseorg , która działa w 12min35s na moim komputerze (4.4GHz Skylake i7-6700k). Żadna wersja nie ma żadnych braków pamięci podręcznej L1D, więc moja pamięć DDR4-2666 nie ma znaczenia.W przeciwieństwie do Pythona przechowuję liczby o rozszerzonej precyzji w formacie, który sprawia, że obcięcie cyfr dziesiętnych jest bezpłatne . Przechowuję grupy 9 cyfr dziesiętnych na 32-bitową liczbę całkowitą, więc przesunięcie wskaźnika odrzuca niskie 9 cyfr. To faktycznie podstawa 1 miliarda, czyli potęga 10. (To czysty zbieg okoliczności, że to wyzwanie wymaga 1 miliardowej liczby Fibonacciego, ale oszczędza mi to kilka bajtów w porównaniu do dwóch oddzielnych stałych).
Zgodnie z terminologią GMP każdy 32-bitowy fragment liczby o rozszerzonej precyzji jest nazywany „kończyną”. Wykonanie podczas dodawania musi zostać wygenerowane ręcznie za pomocą porównania z 1e9, ale następnie jest normalnie wykorzystywane jako dane wejściowe do zwykłej
ADC
instrukcji dla następnej kończyny. (Muszę również ręcznie zawinąć do[0..999999999]
zakresu, a nie przy 2 ^ 32 ~ = 4,295e9. Robię to bez rozgałęzień za pomocąlea
+cmov
, używając wyniku przeprowadzania porównania.)Kiedy ostatnia kończyna wykonuje niezerowe wykonanie, kolejne dwie iteracje zewnętrznej pętli odczytują z 1 kończyny wyżej niż normalnie, ale nadal piszą w tym samym miejscu. To jest jak
memcpy(a, a+4, 114*4)
przesunięcie w prawo o 1 kończynę, ale odbywa się to w ramach dwóch następnych pętli dodawania. Dzieje się tak co ~ 18 iteracji.Hacki dla oszczędności rozmiaru i wydajności:
Zwykłe rzeczy jak
lea ebx, [eax-4 + 1]
zamiastmov ebx, 1
, kiedy to wiemeax=4
. A używanieloop
w miejscach, w którychLOOP
powolność ma niewielki wpływ.Obetnij o 1 kończynę za darmo, przesuwając wskaźniki, z których czytamy, jednocześnie zapisując do początku bufora w
adc
pętli wewnętrznej. Czytamy[edi+edx]
i piszemy do[edi]
. Możemy więc uzyskaćedx=0
lub4
uzyskać przesunięcie odczytu-zapisu dla miejsca docelowego. Musimy to zrobić dla 2 kolejnych iteracji, najpierw kompensując oba, a następnie tylko kompensując dst. Drugi przypadek wykrywamy, sprawdzającesp&4
przed zresetowaniem wskaźników z przodu buforów (używając&= -1024
, ponieważ bufory są wyrównane). Zobacz komentarze w kodzie.Środowisko uruchamiania procesu Linux (dla statycznego pliku wykonywalnego) zeruje większość rejestrów, a pamięć stosu poniżej
esp
/rsp
jest zerowana. Mój program to wykorzystuje. W wersji tego z funkcją wywoływania (gdzie nieprzydzielony stos może być brudny), mógłbym użyć BSS do zerowania pamięci (kosztem może 4 dodatkowych bajtów do ustawienia wskaźników). Zerowanieedx
zajmie 2 bajty. X86-64 System V ABI nie gwarantuje żadnego z nich, ale jego implementacja w Linuksie jest zerowa (aby uniknąć wycieku informacji z jądra). W dynamicznie połączonym procesie/lib/ld.so
działa wcześniej_start
i pozostawia rejestry niezerowe (i prawdopodobnie śmieci w pamięci poniżej wskaźnika stosu).Trzymam
-1024
sięebx
do stosowania na zewnątrz pętli. Użyjbl
jako licznik dla wewnętrznych pętli, kończących się na zero (co jest niskim bajtem-1024
, przywracając w ten sposób stałą do użycia poza pętlą). Intel Haswell, a później nie ma częściowych kar za łączenie rejestrów za niskie rejestry (a nawet nie zmienia ich osobno) , więc istnieje zależność od pełnego rejestru, jak na AMD (tutaj nie ma problemu). Byłoby to okropne w przypadku Nehalem i wcześniejszych, które mają częściowe rejestracje podczas łączenia. Są inne miejsca, w których piszę częściowe regi, a następnie czytam pełny reg bezxor
zerowania lub amovzx
, zwykle dlatego, że wiem, że jakiś poprzedni kod zerował górne bajty, i znowu jest to w porządku w przypadku AMD i rodziny Intel SnB, ale powolne w przypadku Intel przed Sandybridge.Używam
1024
jako liczby bajtów do zapisu do stdout (sub edx, ebx
), więc mój program drukuje niektóre bajty śmieci po cyfrach Fibonacciego, ponieważmov edx, 1000
kosztuje więcej bajtów.(nie używane)
adc ebx,ebx
z EBX = 0, aby uzyskać EBX = CF, oszczędzając 1 bajt vssetc bl
.dec
/jnz
wewnątrzadc
pętli zachowuje CF bez powodowania przeciągnięcia częściowej flagi podczasadc
odczytywania flag na Intel Sandybridge i nowszych. Jest zły na wcześniejszych procesorach , ale AFAIK za darmo na Skylake. Lub, w najgorszym wypadku, dodatkowa zaleta.Użyj pamięci poniżej
esp
jako gigantycznej czerwonej strefy . Ponieważ jest to kompletny program dla systemu Linux, wiem, że nie zainstalowałem żadnych programów obsługi sygnałów i że nic innego nie asynchronicznie zablokuje pamięci stosu przestrzeni użytkownika. Może się tak nie zdarzyć w przypadku innych systemów operacyjnych.Skorzystaj z silnika stosu, aby zaoszczędzić przepustowość problemową UOP, używając
pop eax
(1 uop + okazjonalne synchronizowanie stosu uop) zamiastlodsd
(2 uops na Haswell / Skylake, 3 na IvB i wcześniejszych zgodnie z tabelami instrukcji Agner Fog ). IIRC, skróciło to czas działania z około 83 sekund do 73. Prawdopodobnie mógłbym uzyskać taką samą prędkość z używaniamov
z trybem adresowania indeksowanego, tak jakmov eax, [edi+ebp]
gdzieebp
zachowuje przesunięcie między buforami src i dst. (Sprawiłoby to, że kod poza pętlą wewnętrzną byłby bardziej złożony, ponieważ musiałby zanegować rejestr przesunięcia w ramach zamiany src i dst dla iteracji Fibonacciego.) Więcej informacji w sekcji „wydajność” poniżej.rozpocznij sekwencję, nadając pierwszej iteracji przeniesienie (jeden bajt
stc
), zamiast zapisywać1
gdziekolwiek w pamięci. Wiele innych specyficznych dla problemu rzeczy udokumentowanych w komentarzach.Lista NASM (kod maszynowy + źródło) , wygenerowana z
nasm -felf32 fibonacci-1G.asm -l /dev/stdout | cut -b -28,$((28+12))- | sed 's/^/ /'
. (Następnie ręcznie usunąłem kilka bloków komentowanych rzeczy, więc numeracja linii ma luki.) Aby usunąć główne kolumny i wprowadzić je do YASM lub NASM, użyjcut -b 27- <fibonacci-1G.lst > fibonacci-1G.asm
.Prawdopodobnie jest jeszcze miejsce na grę w golfa, ale spędziłem na tym co najmniej 12 godzin w ciągu 2 dni. Nie chcę poświęcać prędkości, mimo że jest ona o wiele bardziej niż wystarczająco szybka i jest miejsce na jej zmniejszenie w sposób, który kosztuje prędkość . Częścią mojego powodu publikowania postów jest pokazanie, jak szybko mogę stworzyć wersję asm brutalnej siły. Jeśli ktoś naprawdę chce wybrać rozmiar minimalny, ale może 10-krotnie wolniejszy (np. 1 cyfra na bajt), możesz skopiować go jako punkt wyjścia.
Wynikowy plik wykonywalny (z
yasm -felf32 -Worphan-labels -gdwarf2 fibonacci-1G.asm && ld -melf_i386 -o fibonacci-1G fibonacci-1G.o
) to 340B (pozbawiony):Występ
adc
Pętla wewnętrzna to 10 skoków domeny połączonej w Skylake (+1 skok synchronizacji stosu co ~ 128 bajtów), więc może wydawać jeden na ~ 2,5 cykli w Skylake z optymalną wydajnością front-end (ignorując wzrost synchronizacji stosu) . Opóźnienie krytycznego ścieżka 2 cykli, gdyżadc
->cmp
-> kolejnej iteracji wadc
pętli prowadzi łańcuch zależność, a więc szyjka powinny być ograniczenie problem czołowy wynosi około 2,5 cykli na iteracji.adc eax, [edi + edx]
to 2 nieuprawnione nieuprawnione domeny dla portów wykonania: load + ALU. Mikro-topi się w dekoderach (1 UOP w domenie z fuzją), ale un-laminuje na etapie wydania do 2 UUP w domenie z fuzji, ze względu na tryb adresowania indeksowanego, nawet w Haswell / Skylake . Myślałem, że pozostanie mikro-skondensowany, podobnie jakadd eax, [edi + edx]
robi, ale być może utrzymywanie indeksowanych trybów adresowania mikro-skondensowany nie działa dla uops, które mają już 3 wejścia (flagi, pamięć i miejsce docelowe). Kiedy to napisałem, myślałem, że to nie będzie miało negatywnego wyniku, ale się myliłem. Ten sposób obsługi obcinania spowalnia wewnętrzną pętlę za każdym razem, niezależnie od tego, czyedx
wynosi 0, czy 4.Byłoby szybciej obsłużyć przesunięcie odczytu-zapisu dla dst poprzez przesunięcie
edi
i użycieedx
do dostosowania magazynu. Więcadc eax, [edi]
/ ... /mov [edi+edx], eax
/lea edi, [edi+4]
zamiaststosd
. Haswell i później mogą przechowywać indeksowany sklep w postaci mikro-bezpieczników. (Sandybridge / IvB też to rozwiąże).Na Intel Haswell i wcześniejszych,
adc
icmovc
są 2 upops każdy, z opóźnieniem 2c . (adc eax, [edi+edx]
wciąż nie jest laminowany na Haswell i wydaje się, że 3 nie działa w domenie połączonej). Broadwell, a później zezwalają na 3-wejściowe uopsy dla więcej niż tylko FMA (Haswell), tworzeniaadc
icmovc
(i kilku innych rzeczy) instrukcji dla pojedynczych uupów, jakby były na AMD od dłuższego czasu. (Jest to jeden z powodów, dla których AMD od dawna dobrze sobie radzi w testach porównawczych GMP o rozszerzonej precyzji.) W każdym razie wewnętrzna pętla Haswella powinna wynosić 12 jednostek (czasami +1 synchronizacja stosów), z wąskim gardłem na poziomie ~ 3 centów za sztukę iter najlepszy przypadek, ignorując ups synchronizacji stosu.Używanie
pop
bez równoważeniapush
wewnątrz pętli oznacza, że pętla nie może uruchomić się z LSD (detektor strumienia pętli) i musi być za każdym razem ponownie odczytywana z pamięci podręcznej uop do IDQ. Jeśli już, to dobrze, że w Skylake jest dobra, ponieważ pętla 9 lub 10 jednostek nie wydaje się optymalnie przy 4 jednostkach każdego cyklu . Jest to prawdopodobnie część tego, dlaczego zastąpienielodsd
przezpop
tak bardzo pomogło. (LSD nie może zablokować opcji Uops, ponieważ nie pozostawiłoby to miejsca na wstawienie opcji UOP synchronizacji stosu .) (BTW, aktualizacja mikrokodu wyłącza LSD całkowicie w Skylake i Skylake-X, aby naprawić błąd. Zmierzyłem powyżej przed otrzymaniem tej aktualizacji).Profilowałem go na Haswell i stwierdziłem, że działa w 381,31 miliarda cykli zegara (niezależnie od częstotliwości procesora, ponieważ używa tylko pamięci podręcznej L1D, a nie pamięci). Przepustowość problemu front-end wyniosła 3,72 Uops-Fused-domena na zegar, w porównaniu do 3,70 dla Skylake. (Ale oczywiście instrukcje na cykl spadła do 2,42 z 2,87, bo
adc
icmov
są 2 UOPs na Haswell).push
zastąpieniestosd
prawdopodobnie nie pomogłoby tak bardzo, ponieważadc [esp + edx]
uruchamiałoby uop synchronizacji stosu za każdym razem. I kosztowałby bajt,std
więclodsd
idzie w innym kierunku. (mov [edi], eax
/lea edi, [edi+4]
do zastąpieniastosd
to wygrana, przechodząc z 32.909 motocykli dla iterów 100M do 31.954 motocykli dla iterów 100 mln. Wygląda na to, żestosd
dekoduje się jako 3 uops, przy czym uops-store / store-data uops nie są mikro-stopione, więcpush
+ synchronizacja stosu Ups może nadal być szybszy niżstosd
)Rzeczywista wydajność ~ 322,47 miliarda cykli dla iteracji 1G 114 kończyn wynosi 2,824 cykli na iterację pętli wewnętrznej , dla szybkiej wersji 105B na Skylake. (Patrz
ocperf.py
dane wyjściowe poniżej). Jest to wolniejsze niż przewidywałem na podstawie analizy statycznej, ale ignorowałem obciążenie związane z zewnętrzną pętlą i wszelkimi dodatkami synchronizacji stosu.Perf liczy
branches
ibranch-misses
pokazuje, że wewnętrzna pętla błędnie przewiduje raz na zewnętrzną pętlę (podczas ostatniej iteracji, gdy nie jest pobierana). To także stanowi część dodatkowego czasu.Mógłbym zapisać rozmiar kodu, sprawiając, że najbardziej wewnętrzna pętla ma 3-cyklowe opóźnienie dla ścieżki krytycznej, używając
mov esi,eax
/sub eax,ebp
/cmovc eax, esi
/cmc
(2 + 2 + 3 + 1 = 8B) zamiastlea esi, [eax - 1000000000]
/cmp ebp,eax
/cmovc
(6 + 2 + 3 = 11B ).cmov
/stosd
Jest wyłączony ścieżce krytycznej. (Edycja przyrostowastosd
może działać niezależnie od sklepu, więc każda iteracja wyklucza krótki łańcuch zależności). Dawniej zapisywał kolejny 1B, zmieniając instrukcję init ebp zlea ebp, [ecx-1]
namov ebp,eax
, ale odkryłem, że źleebp
nie zmienił wyniku. Pozwoliłoby to kończynie dokładnie == 1000000000 zamiast owijania i generowania przeniesienia, ale ten błąd propaguje się wolniej niż rośnie Fib (), więc nie zmienia to wiodących cyfr 1k końcowego wyniku. Myślę też, że ten błąd może się poprawić, gdy dodajemy, ponieważ w kończynie jest miejsce, aby go utrzymać bez przepełnienia. Nawet 1G + 1G nie przepełnia 32-bitowej liczby całkowitej, więc ostatecznie przesiąknie w górę lub zostanie obcięta.Wersja 3c z opóźnieniem ma 1 dodatkowy UOP, więc front-end może wydawać go raz na 2,75c cykli w Skylake, tylko nieco szybciej niż back-end może go uruchomić. (W Haswell będzie to łącznie 13 uops, ponieważ nadal używa
adc
icmov
oraz wąskie gardło w interfejsie na poziomie 3,25 c na iter).W praktyce działa on o 1,18 wolniej na Skylake (3,34 cykli na kończynę), zamiast 3 / 2,5 = 1,2, które przewidywałem, że zastąpię wąskie gardło z przodu z wąskim gardłem od samego spojrzenia na wewnętrzną pętlę bez synchronizacji stosu ups. Ponieważ uops-sync stosy szkodzą tylko szybkiej wersji (wąskie gardło na froncie zamiast opóźnień), nie trzeba wiele wyjaśniać. np. 3 / 2,54 = 1,18.
Innym czynnikiem jest to, że wersja 3c z opóźnieniem może wykryć nieprzewidywalne wyjście z wewnętrznej pętli, gdy ścieżka krytyczna jest nadal wykonywana (ponieważ front-end może wyprzedzić back-end, pozwalając na wykonanie poza kolejnością, uruchamiając pętlę- counter uops), więc skuteczna kara za niepoprawne przewidywanie jest niższa. Utrata tych cykli front-end pozwala dogonić back-end.
Gdyby tak nie było, moglibyśmy przyspieszyć
cmc
wersję 3c , używając gałęzi w zewnętrznej pętli zamiast bezgałęziowej obsługi przeniesień carry_out -> edx i esp. Przewidywanie rozgałęzień + wykonywanie spekulatywne dla zależności sterującej zamiast zależności danych może pozwolić następnej iteracji na uruchomienieadc
pętli, podczas gdy wzloty z poprzedniej pętli wewnętrznej były nadal w locie. W wersji bez rozgałęzienia adresy obciążenia w pętli wewnętrznej zależą od CF od ostatniejadc
ostatniej kończyny.Wąskie gardła w wewnętrznej pętli z opóźnieniem 2c z przodu, więc back-end prawie nadąża. Gdyby kod pętli zewnętrznej miał duże opóźnienie, front-end mógłby wyprzedzić wydawanie wzlotów z następnej iteracji pętli wewnętrznej. (Ale w tym przypadku zewnętrzna pętla ma dużo ILP i nie ma opóźnień, więc back-end nie ma wiele do nadrobienia, gdy zaczyna przeżuwać błędy w harmonogramie poza kolejnością, ponieważ ich dane wejściowe są gotowe).
( +- x %)
jest odchyleniem standardowym dla 4 przebiegów dla tej liczby. Ciekawe, że działa tak okrągła liczba instrukcji. 924 miliardów to nie przypadek. Wydaje mi się, że w zewnętrznej pętli działa łącznie 924 instrukcji.uops_issued
jest liczbą domen połączonych (istotną dla przepustowości problemów frontonu), podczas gdyuops_executed
jest liczbą domen nieużywanych (liczba operacji wysyłania do portów wykonawczych). Mikro-fuzja pakuje 2 nieuprawnione domeny w jedną domenę o fuzji domeny, ale mov-eliminacja oznacza, że niektóre uopsy domeny nie wymagają portów wykonawczych. Zobacz połączone pytanie, aby uzyskać więcej informacji na temat liczenia domen upops i fused vs. (Zobacz także tabele instrukcji Agner Fog i przewodnik uarch oraz inne przydatne linki w wiki tagu SO x86 ).Z innego pomiaru mierzącego różne rzeczy: brak pamięci podręcznej L1D jest całkowicie nieistotny, zgodnie z oczekiwaniami dla odczytu / zapisu tych samych dwóch buforów 456B. Gałąź pętli wewnętrznej błędnie przewiduje raz na pętlę zewnętrzną (gdy nie jest podejmowana, aby opuścić pętlę). (Całkowity czas jest dłuższy, ponieważ komputer nie był całkowicie bezczynny. Prawdopodobnie drugi logiczny rdzeń był aktywny przez pewien czas, a więcej czasu spędzano na przerwaniach (ponieważ częstotliwość mierzona w przestrzeni użytkownika była dalej niższa niż 4.400 GHz). Lub więcej rdzeni było aktywnych przez dłuższy czas, obniżając maksymalne turbo. Nie śledziłem,
cpu_clk_unhalted.one_thread_active
czy konkurencja HT jest problemem.)Mój kod może być uruchamiany w mniejszej liczbie cykli na Ryzen, co może powodować 5 uopsów na cykl (lub 6, gdy niektóre z nich to instrukcje 2 uop, takie jak AVX 256b na Ryzen). Nie jestem pewien, co zrobiłby jego front-end
stosd
, czyli 3 ulepszenia na Ryzen (tak samo jak Intel). Myślę, że pozostałe instrukcje w wewnętrznej pętli mają takie same opóźnienia jak Skylake i wszystkie pojedynczo. (W tymadc eax, [edi+edx]
, co stanowi przewagę nad Skylake).Mogłoby to być prawdopodobnie znacznie mniejsze, ale może 9-krotnie wolniejsze, jeśli zapisałem liczby jako 1 cyfrę dziesiętną na bajt . Generowanie wykonania
cmp
i dostosowywanie za pomocącmov
działałoby tak samo, ale wykonaj 1/9 pracy. Działa również 2 cyfry dziesiętne na bajt (base-100, nie 4-bitowy BCD ze spowolnieniemDAA
) idiv r8
/add ax, 0x3030
zamienia 0-99 bajtów na dwie cyfry ASCII w kolejności drukowania. Ale 1 cyfra na bajt wcale nie potrzebujediv
, wystarczy zapętlić i dodać 0x30. Jeśli przechowam bajty w kolejności drukowania, to sprawi, że druga pętla stanie się naprawdę prosta.Zastosowanie 18 lub 19 cyfr dziesiętnych na 64-bitową liczbę całkowitą (w trybie 64-bitowym) sprawiłoby, że działałby on około dwa razy szybciej, ale kosztował znaczny rozmiar kodu dla wszystkich prefiksów REX i dla 64-bitowych stałych. 32-bitowe kończyny w trybie 64-bitowym uniemożliwiają użycie
pop eax
zamiastlodsd
. Nadal mogłem uniknąć prefiksów REX, używającesp
jako rejestru non-point scratch (zamieniając użycieesi
iesp
), zamiast używaćr8d
jako 8. rejestr.W przypadku wersji z funkcją wywoływania konwersja do wersji 64-bitowej i używanie
r8d
może być tańsze niż zapisywanie / przywracaniersp
. 64-bitowe również nie mogą używaćdec r32
kodowania jednobajtowego (ponieważ jest to przedrostek REX). Ale głównie skończyło się nadec bl
tym, że 2 bajty. (Ponieważ mam stałą w górnych bajtachebx
i używam jej tylko poza wewnętrznymi pętlami, co działa, ponieważ niski bajt stałej jest0x00
.)Wersja o wysokiej wydajności
Aby uzyskać maksymalną wydajność (nie kod-golf), należy rozwinąć wewnętrzną pętlę, aby działała co najwyżej 22 iteracje, co jest wystarczająco krótkim wzorcem wziętym / nie wziętym, aby predyktory gałęzi działały dobrze. W moich eksperymentach,
mov cl, 22
zanim.inner: dec cl/jnz .inner
pętla zawiera bardzo mało nieprzewidywalnych wyników (np. 0,05%, znacznie mniej niż jeden na pełny przebieg wewnętrznej pętli), alemov cl,23
błędnie przewiduje od 0,35 do 0,6 razy na wewnętrzną pętlę.46
jest szczególnie zły, nieprzewidywalny ~ 1,28 razy na pętlę wewnętrzną (128 mln razy dla iteracji 100M pętli zewnętrznej).114
źle przewidziany dokładnie raz na wewnętrzną pętlę, tak samo jak znalazłem jako część pętli Fibonacciego.Zainteresowałem się i wypróbowałem to, rozwijając wewnętrzną pętlę o 6 za pomocą
%rep 6
(ponieważ to równomiernie dzieli 114). To w większości wyeliminowało brakujące oddziały. Zrobiłemedx
ujemny i użyłem go jako offsetu dlamov
sklepów, więcadc eax,[edi]
mogłem pozostać mikro-stopiony. (I tak mogłem uniknąćstosd
). Wyciągnąłemlea
aktualizacjęedi
z%rep
bloku, więc wykonuje tylko jedną aktualizację wskaźnika na 6 sklepów.Pozbyłem się również wszystkich częściowych rejestrów w zewnętrznej pętli, choć nie sądzę, żeby to miało znaczenie. Być może pomógł nieznacznie mieć CF na końcu zewnętrznej pętli, niezależny od ostatecznego ADC, więc niektóre ze zmian w pętli wewnętrznej można zacząć. Kod pętli zewnętrznej można prawdopodobnie zoptymalizować nieco bardziej, ponieważ
neg edx
była to ostatnia rzecz, którą zrobiłem, po zastąpieniuxchg
tylko 2mov
instrukcjami (ponieważ wciąż miałem 1) i ponownym rozmieszczeniu łańcuchów dep wraz z upuszczeniem 8-bitów zarejestrować rzeczy.To jest źródło NASM tylko pętli Fibonacciego. Jest to drop-in zamiennik tej sekcji oryginalnej wersji.
Występ:
To dotyczy tego samego Fib (1G), wytwarzając tę samą moc wyjściową w 62,3 sekundy zamiast 73 sekund. (273.146G cykli, w porównaniu z 322.467G. Ponieważ wszystko uderza w pamięć podręczną L1, cykle zegara rdzenia to naprawdę wszystko, na co musimy spojrzeć.)
Zwróć uwagę na znacznie niższą całkowitą
uops_issued
liczbę, znacznie poniżejuops_executed
liczby. Oznacza to, że wiele z nich zostało połączonych w trybie mikro: 1 uop w domenie z fuzją (problem / ROB), ale 2 uop w domenie bez fuzji (harmonogram / jednostki wykonawcze)). I niewielu zostało wyeliminowanych na etapie wydania / zmiany nazwy (np.mov
Kopiowanie rejestru lubxor
zerowanie, które wymagają wydania, ale nie potrzebują jednostki wykonawczej). Wyeliminowane ups będą równoważyć liczbę w drugą stronę.branch-misses
spada do ~ 400k, z 1G, więc rozwijanie działało.resource_stalls.any
jest teraz znaczący, co oznacza, że front-end nie jest już wąskim gardłem: zamiast tego back-end jest w tyle i ogranicza front-end.idq_uops_not_delivered.core
liczy tylko cykle, w których front-end nie dostarczał poprawek, ale back-end nie został zablokowany. To miłe i niskie, wskazujące na kilka wąskich gardeł z przodu.Ciekawostka: wersja Pythona spędza ponad połowę czasu dzieląc przez 10, a nie dodając. (Wymiana
a/=10
za>>=64
prędkościami go o więcej niż współczynnik 2, ale zmienia wynik bo obcinanie binarnym! = Dziesiętny obcięcie).Moja wersja asm jest oczywiście zoptymalizowana specjalnie dla tego rozmiaru problemu, a iteracja pętli liczy się na sztywno. Nawet przesunięcie liczby o dowolnej dokładności spowoduje jej skopiowanie, ale moja wersja może po prostu odczytać offset z następnych dwóch iteracji, aby pominąć nawet to.
Profilowałem wersję Pythona (64-bitowy python2.7 na Arch Linux):
Liczby w (parens) określają, ile czasu próbkowany był licznik perf. Patrząc na więcej liczników niż obsługuje HW, perf obraca się między różnymi licznikami i ekstrapoluje. To całkiem dobrze na długi okres tego samego zadania.
Gdybym uruchomił
perf
po ustawieniu sysctlkernel.perf_event_paranoid = 0
(lub uruchomieniuperf
jako root), to by to zmierzyło4.400GHz
.cycles:u
nie liczy czasu spędzonego na przerwaniach (lub wywołaniach systemowych), tylko cykle przestrzeni użytkownika. Mój pulpit był prawie całkowicie bezczynny, ale jest to typowe.źródło
Haskell,
8361 bajtówWyjścia ( F 1000000000 , F 1000000001 ). Na moim laptopie poprawnie drukuje lewy paren i pierwsze 1000 cyfr w ciągu 133 sekund, wykorzystując 1,35 GiB pamięci.
Jak to działa
Nawrót Fibonacciego można rozwiązać za pomocą potęgowania macierzy:
[ M I - 1 , M i ; F I , M i + 1 ] = [0, 1; 1, 1] i ,
z którego czerpiemy te tożsamości:
[ F i + j - 1 , F i + j ; F i + j , F i + j + 1 ] = [ F i - 1 , F i ; F i , F i + 1 ] ⋅ [ F j - 1 , F j ; F j , F j + 1 ],
F i + j = F i+ 1 F j + 1 - F i - 1 F j - 1 = F i + 1 F j + 1 - ( F i + 1 - F i ) ( F j + 1 - F j ),
F i + j + 1 = F i F j + F i + 1 F j + 1 .
W
p
Oblicza funkcyjną ( C i + J , M i + j + 1 ), ze względu na ( F I , M i + 1 ) i ( K J , K j + 1 ). Piszącf n
dla ( F i , F i + 1 ), mamyp (f i) (f j)
=f (i + j)
.Następnie,
(t=<<t.p) (f i)
=
t ((t.p) (f i)) (f i)
=
t (p (f i).p (f i).p (f i)) (f i)
=
(p (f i).p (f i).p (f i).p (f i).p (f i).p (f i).p (f i).p (f i).p (f i)) (f i)
=
f (10 * i)
,(t$t=<<t.p) (f i)
=
((t=<<t.p).(t=<<t.p).(t=<<t.p)) (f i)
=
f (10^3 * i)
,t(t$t=<<t.p) (f i)
=
((t$t=<<t.p).(t$t=<<t.p).(t$t=<<t.p)) (f i)
=
f (10^9 * i)
,i podłączamy
f 1
=(1,1)
.źródło
Mathematica, 15
34bajtyFibonacci
sam zajmuje ~ 6s na moim komputerze. I 95 (+/- 5) s dla interfejsu użytkownika, aby go wyświetlić.Pierwsze 1000 cyfr (34 bajty):
⌊Fibonacci@1*^9/1*^208986640⌋&
Dłuższy, ale szybszy
ToString@Fibonacci@1*^9~StringTake~1000&
:źródło
div
). Przestałem, bo ludzie prawdopodobnie skończyliby patrzeć na to pytanie, zanim miałem dobrze zagraną funkcję, która wykonała całą tę pracę. Ale najwyraźniej brutalna siła może działać, jak pokazują niektóre odpowiedzi.Python 2, 70 bajtów
Trwało to przez 18 minut i 31 sekund na moim laptopie, generując prawidłowe 1000 cyfr, a następnie
74100118580
(prawidłowe są następujące cyfry74248787892
).źródło
div
pętla umożliwiająca wykonanie 9 cyfr dziesiętnych na porcję. Noś podczas dodawania za pomocą cmp / cmov i 2xADD zamiast ADC.Haskell , 78 bajtów
Wypróbuj online!
Zajęło 48 sekund na TIO. Ta sama rekurencyjna formuła jak moja odpowiedź w języku Python , ale bez obcinania.
Stała
2143923439
jest10**9-1
odwrócona binarnie, a na końcu jest dodatkowa 1. Iteracja cyfr binarnych w odwrotnej kolejności symuluje iterację cyfr binarnych z10**9-1
. Wydaje się, że kodowanie tego jest krótsze niż jego obliczenie.źródło
Haskell ,
202184174173170168164162 bajtówWypróbuj online!
Wyjaśnienie
Wykorzystuje to dość szybki sposób obliczania liczb fibonacciego. Funkcja
l
przyjmuje dwie liczby Fibonacciego i oblicza liczby Fibonacciego 10 później, podczas gdyf
bierze n- ta i n + 1- tą liczbę Fibonacciego i oblicza 2n + 20- te i 2n + 21- te liczby Fibonacciego. Łączę je raczej przypadkowo, aby zdobyć 1 miliard i zdobyć pierwsze 1000 cyfr.źródło
Haskell, 81 bajtów
Wyjaśnienie
f n
rekurencyjnie obliczan
liczbę F fibonacciego, używając wzorca z odpowiedzi xnora z eliminacją podwyrażenia wspólnego. W przeciwieństwie do innych opublikowanych rozwiązań, które wykorzystują multiplikacje O (log (n)), mamy rekurencję głębokości O (log (n)) o współczynniku rozgałęzienia 2, dla złożoności mnożenia O (n).Jednak nie wszystko jest stracone! Ponieważ prawie wszystkie wywołania będą znajdować się w dolnej części drzewa rekurencji, w miarę możliwości możemy użyć szybkiej natywnej arytmetyki i uniknąć mnóstwa manipulacji dużymi bignum. Wyrzuca odpowiedź w ciągu kilku minut na moim pudełku.
źródło
T-SQL,
422 414453 bajtów (zweryfikowany, teraz konkuruje!)EDYCJA 2 : Zmieniono na , Zyskałem kilka bajtów, ale zwiększyłem prędkość wystarczającą do ukończenia do 1 miliarda! Ukończony w ciągu 45 godzin 29 minut , weryfikuje podany ciąg znaków i wyświetla dodatkowe 8 znaków (które mogą, ale nie muszą być poprawne z powodu błędów zaokrąglania).
INT BIGINT
DECIMAL(37,0)
T-SQL nie ma natywnej obsługi „ogromnej liczby”, więc musiałem rzucić własny tekstowy sumator dużej liczby przy użyciu ciągów 1008 znaków:
Oto sformatowana wersja z komentarzami:
Zasadniczo ręcznie manipuluję ciągami wypełnionymi zerami 1008 znaków reprezentujących moje dwie zmienne Fibonacciego
@a
i@
.Dodaję je
8 1836 cyfr na raz, usuwając ostatnie 36 cyfr, konwertując na możliwy do zarządzania typ numeryczny (DECIMAL(37,0)
), dodając je, a następnie rozbijając z powrotem na kolejny długi ciąg@c
. Następnie „obracam”@a
i@
przesuwam ostatnie 36 cyfr do przodu i powtarzam proces. 28 obrotów * 36 cyfr obejmuje wszystkie 1008. Muszę „nosić ten” ręcznie.Gdy nasza liczba zaczyna przekraczać długość mojego łańcucha, „przesuwam w lewo” i zaczynamy tracić precyzję, ale błąd mieści się w zakresie moich dodatkowych znaków.
Próbowałem użyć tabeli SQL pełnej INT i BIGINT, z podobną logiką, i było to znacznie wolniejsze. Dziwne.
źródło
PARI / GP, 45 bajtów
Jakoś
\p1000
nie wystarczy. To nie działa z systemami 32-bitowymi. Ostatecznym podziałem jest unikanie kropki dziesiętnej w notacji naukowej.źródło
Pari / GP , 15 + 5 = 20 bajtów
Uruchom z opcją wiersza polecenia,
-s1g
aby przydzielić 1 GB pamięci.źródło
Ruby, 63 bajty
człowieku, jestem zły w golfie ruby; ale klasa BigInt robi cuda dla tego rodzaju rzeczy. Używamy tego samego algorytmu, co Anders Kaseorg.
źródło