Es gab eine Milliarde Iterationen von Fibonacci-Herausforderungen auf dieser Website. Lassen Sie uns die Dinge mit einer Fibonacci-Herausforderung von einer Milliarde Iterationen aufpeppen!
Ihre Herausforderung besteht darin, die ersten 1000 Dezimalstellen der 1.000.000.000sten Fibonacci-Zahl mit einem möglichst kurzen Programm auszugeben. Darauf kann optional eine zusätzliche Ausgabe Ihrer Wahl folgen, einschließlich, aber nicht beschränkt auf den Rest der Ziffern.
Ich bin mit dem Kongress , dass fib 0 = 0
, fib 1 = 1
.
Ihr Programm muss schnell genug sein, damit Sie es ausführen und seine Richtigkeit überprüfen können. Zu diesem Zweck sind hier die ersten 1000 Stellen:
7952317874554683467829385196197148189255542185234398913453039937343246686182519370050999626136556779332482035723222451226291714456275648259499530612111301255499879639516053459789018700567439946844843034599802419924043753401950114830107234265037841426980398387360784284231996457340782784200767760907777703183185744656536253511502851715963351023990699232595471322670365506482435966586886048627159716916351448788527427435508113909167963907380398242848033980110276370544264285032744364781198451825462130529529633339813483105771370128111851128247136311414208318983802526907917787094802217750859685116363883374847428036737147882079956688807509158372249451437519320162582002000530798309887261257028201907509370554232931107084976854715833585623910450679449120011564762925649144509531904684984417002512086504020779012501356177874199605085558317190905395134468919443313026824813363234190494375599262553025466528838122639433600483849535070647711986769279568548796855207684897741771784375859496425384355879105799
code-golf
kolmogorov-complexity
fibonacci
restricted-time
user1502040
quelle
quelle
Your program must be fast enough for you to run it and verify its correctness.
was ist mit der Erinnerung?a+=b;b+=a;
Schleife (möglicherweise mit Java BigInteger) die naheliegende Wahl, zumindest wenn Sie überhaupt an die Leistung denken. Eine rekursive Implementierung erschien mir immer schrecklich ineffizient.write()
Systemaufrufs). Ich mag Leistungsanforderungen, das hat mir viel mehr Spaß gemacht.Antworten:
Python 2 + Sympy, 72 Bytes
Probieren Sie es online!
-10 Bytes durch Entfernen des praktisch-0-Terms dank Jeff Dege
-1 Bytes (1000 -> 1e3 dank Zacharý)
-2 Bytes durch Entfernen der unnötigen Variablen dank Erik the Outgolfer
-2 Bytes durch Verschieben nach Python 2 dank Zacharý
-3 Bytes durch 11'ing den
-11
Dank an ThePirateBay -3 Bytes durch Austauschstr
für Backticks dank Notjaganschlägt jetzt OPs ungepostete Haskell-Lösung!
quelle
from sympy import*;sqrt
keine Bytes mehrimport sympy;sympy.sqrt
sympy
ist ein symbolisches Mathematikpaket für Python, sodass es keine Probleme mit Rundungsfehlern gibt, zumindest bis sehr große Zahlen (diese Zahl ist nicht groß genug, lol). Dann berechne ich es einfach, um mir die ersten 1e3-Ziffern zu geben, da ich sonst, wenn Sie den.evalf(1e3)
Teil entfernen , eine sehr kurze Darstellung in wissenschaftlicher Notation erhalte .Python 2 , 106 Bytes
Probieren Sie es online!
Keine Bibliotheken, nur ganzzahlige Arithmetik. Läuft fast sofort.
Der Kern ist die Divide-and-Conquer-Identität:
Auf diese Weise können wir das Update
(a,b) = (f(n),f(n+1))
auf das Doppelte durchführenn -> 2*n
. Da wir bekommen wollenn=10**9
, dauert dies nurlog_2(10**9)=30
Iterationen. Wir bauen daraufn
auf,10**9
indem wirn->2*n+c
für jede Zifferc
ihrer binären Expansion wiederholt tun . Wennc==1
wird der doppelte Wert2*n -> 2*n+1
mit einer einstufigen Fibonacci-Verschiebung nach oben verschoben(a,b)=(b+a,b)
Um die Werte
a,b
überschaubar zu halten , speichern wir nur die ersten1006
Ziffern, indem wir sie durch teilen,10
bis sie darunter liegen2**3340 ~ 1e1006
.quelle
a,b,c=a*a+b*b,a*a-c*c,b*b+c*c
.x86-32-Bit-Maschinencode (bei Linux-Systemaufrufen):
106 bis105 ByteChangelog: In der schnellen Version wurde ein Byte gespeichert, da eine Off-by-One-Konstante das Ergebnis für Fib (1G) nicht ändert.
Oder 102 Bytes für eine 18% langsamere (bei Skylake) Version (Verwenden von
mov
/sub
/cmc
anstelle vonlea
/cmp
in der inneren Schleife, um Übertragen und Umbrechen zu generieren,10**9
anstatt2**32
). Oder 101 Bytes für eine ~ 5,3x langsamere Version mit einer Verzweigung im Carry-Handling in der innersten Schleife. (Ich habe eine Rate von 25,4% für Branchenfehlprognosen gemessen!)Oder 104/101 Bytes, wenn eine führende Null zulässig ist. (Es dauert 1 zusätzliches Byte, um eine Stelle der Ausgabe mit festem Code zu überspringen, was für Fib (10 ** 9) erforderlich ist.)
Leider scheint der NASM-Modus von TIO
-felf32
in den Compiler-Flags zu ignorieren . Hier ist sowieso ein Link mit meinem vollständigen Quellcode, mit all dem Durcheinander an experimentellen Ideen in Kommentaren.Dies ist ein vollständiges Programm . Es werden die ersten 1000 Stellen von Fib (10 ** 9) gedruckt, gefolgt von einigen zusätzlichen Stellen (von denen die letzten falsch sind), gefolgt von einigen Müllbytes (ohne Zeilenvorschub). Der größte Teil des Mülls ist kein ASCII-Speicher, daher möchten Sie möglicherweise eine Pipe-Funktion ausführen
cat -v
. Mein Terminalemulator (KDEkonsole
) wird dadurch jedoch nicht beschädigt. Die "Garbage Bytes" speichern Fib (999999999). Ich hatte bereits-1024
in einem Register, so war es billiger, 1024 Bytes als die richtige Größe zu drucken.Ich zähle nur den Maschinencode (Größe des Textsegments meiner statischen ausführbaren Datei) und nicht die Flusen, die sie zu einer ausführbaren ELF-Datei machen. ( Sehr kleine ELF-Programme sind möglich , aber ich wollte mich nicht darum kümmern). Es stellte sich heraus, dass es kürzer war, Stack-Speicher anstelle von BSS zu verwenden, sodass ich irgendwie rechtfertigen kann, nichts anderes in der Binärdatei zu zählen, da ich nicht von Metadaten abhängig bin. (Die normale Erzeugung einer statischen Binärdatei macht eine ELF mit 340 Byte ausführbar.)
Sie könnten aus diesem Code eine Funktion machen, die Sie von C aus aufrufen könnten. Das Speichern / Wiederherstellen des Stapelzeigers (möglicherweise in einem MMX-Register) und eines anderen Overheads würde einige Bytes kosten, aber auch Bytes, indem Sie mit der Zeichenfolge zurückkehren im Speicher, anstatt einen
write(1,buf,len)
Systemaufruf zu tätigen. Ich denke, das Golfen mit Maschinencode sollte hier ein wenig nachlassen, da noch niemand eine Antwort in einer Sprache ohne native Extended-Precision gepostet hat, aber ich denke, eine Funktionsversion davon sollte immer noch unter 120 Bytes sein, ohne das Ganze erneut zu golfen Ding.Algorithmus:
Brute Force
a+=b; swap(a,b)
, die nach Bedarf abgeschnitten wird, um nur die führenden Dezimalstellen> = 1017 beizubehalten. Es läuft in 1min13s auf meinem Computer (oder 322,47 Milliarden Taktzyklen + - 0,05%) (und könnte mit ein paar zusätzlichen Bytes Codegröße ein paar% schneller sein, oder bis zu 62s mit viel größerer Codegröße durch das Abrollen der Schleife. Nr kluge Mathematik, die gleiche Arbeit mit weniger Aufwand machen). Es basiert auf der Python-Implementierung von @ AndersKaseorg , die in 12 Minuten auf meinem Computer ausgeführt wird (4,4 GHz Skylake i7-6700k). Keine der beiden Versionen hat L1D-Cache-Fehler, daher spielt mein DDR4-2666 keine Rolle.Im Gegensatz zu Python speichere ich die Zahlen mit erweiterter Genauigkeit in einem Format, das das Abschneiden von Dezimalstellen freigibt . Ich speichere Gruppen mit 9 Dezimalstellen pro 32-Bit-Ganzzahl, sodass ein Zeiger-Offset die niedrigen 9 Stellen verwirft. Dies ist effektiv die Basis 1 Milliarde, was einer Potenz von 10 entspricht. (Es ist reiner Zufall, dass diese Herausforderung die 1 Milliarde Fibonacci-Zahl benötigt, aber es erspart mir ein paar Bytes gegenüber zwei separaten Konstanten.)
Gemäß der GMP- Terminologie wird jeder 32-Bit-Block einer Zahl mit erweiterter Genauigkeit als "Glied" bezeichnet. Die Ausführung während des Hinzufügens muss manuell mit einem Vergleich gegen 1e9 erzeugt werden, wird dann aber normalerweise als Eingabe für die übliche
ADC
Anweisung für die nächste Gliedmaße verwendet. (Ich muss auch manuell auf den[0..999999999]
Bereich umbrechen, anstatt auf 2 ^ 32 ~ = 4.295e9. Ich mache dies ohne Verzweigung mitlea
+cmov
, wobei ich das Ergebnis des Vergleichs verwende.)Wenn das letzte Glied einen Übertrag ungleich Null erzeugt, werden die nächsten zwei Iterationen der äußeren Schleife von einem Glied höher als normal gelesen, aber immer noch an dieselbe Stelle geschrieben. Dies entspricht einer
memcpy(a, a+4, 114*4)
Verschiebung um 1 Glied nach rechts, wird jedoch als Teil der nächsten beiden Additionsschleifen ausgeführt. Dies geschieht alle ~ 18 Iterationen.Hacks für Größenersparnis und Leistung:
Das übliche Zeug wie
lea ebx, [eax-4 + 1]
stattmov ebx, 1
, wenn ich das weißeax=4
. Undloop
an Orten, an denenLOOP
Langsamkeit nur einen geringen Einfluss hat.Kürzen Sie kostenlos um 1 Glied, indem Sie die Zeiger, von denen wir lesen, versetzen, während Sie weiterhin an den Anfang des Puffers in der
adc
inneren Schleife schreiben . Wir lesen aus[edi+edx]
und schreiben an[edi]
. So können wir einen Lese-Schreib-Offset für das Ziel erhaltenedx=0
oder4
erhalten. Wir müssen dies für 2 aufeinanderfolgende Iterationen tun, indem wir zuerst beide und dann nur den dst versetzen. Wir erkennen den 2. Fall, indem wiresp&4
vor dem Zurücksetzen der Zeiger auf die Vorderseite der Puffer (mit&= -1024
, weil die Puffer ausgerichtet sind) schauen . Siehe Kommentare im Code.Die Linux-Prozessstartumgebung (für eine statische ausführbare Datei) setzt die meisten Register auf Null , und der Stapelspeicher unter
esp
/rsp
wird auf Null gesetzt. Mein Programm nutzt dies aus. In einer Version mit aufrufbaren Funktionen (bei der der nicht zugewiesene Stapel möglicherweise fehlerhaft ist) könnte ich BSS für die Nullsetzung des Speichers verwenden (auf Kosten von möglicherweise 4 weiteren Bytes zum Einrichten von Zeigern). Das Nullsetzenedx
würde 2 Bytes dauern. Die x86-64-System-V-ABI garantiert keines davon, aber die Linux-Implementierung macht null (um Informationslecks aus dem Kernel zu vermeiden). Wird in einem dynamisch verknüpften Prozess/lib/ld.so
zuvor ausgeführt_start
und hinterlässt Register ungleich Null (und möglicherweise Speicherabfall unterhalb des Stapelzeigers).Ich halte
-1024
inebx
für den Einsatz außerhalb von Schleifen. Verwendungbl
als Zähler für innere Schleifen, die auf Null enden (das ist das niedrige Byte von-1024
, wodurch die Konstante für die Verwendung außerhalb der Schleife wiederhergestellt wird). Intel Haswell und neuere Versionen haben keine Nachteile für das Zusammenführen von Teilregistern für Low8-Register (und benennen sie auch nicht separat um). Daher besteht eine Abhängigkeit vom vollständigen Register wie bei AMD (hier kein Problem). Dies wäre jedoch für Nehalem und frühere Versionen schrecklich, da diese beim Zusammenführen teilweise Registerstände aufweisen. Es gibt andere Stellen, an denen ichxor
Teilregs schreibe und dann die vollständige Reg ohne -Zeroing oder a lesemovzx
In der Regel, weil ich weiß, dass vorheriger Code die oberen Bytes auf Null gesetzt hat, und das ist bei AMD und der Intel SnB-Familie in Ordnung, bei Intel vor Sandybridge jedoch langsam.Ich benutze
1024
als Anzahl der Bytes, um in stdout (sub edx, ebx
) zu schreiben , also druckt mein Programm einige Müllbytes nach den Fibonacci-Ziffern, weil esmov edx, 1000
mehr Bytes kostet.(nicht verwendet)
adc ebx,ebx
mit EBX = 0, um EBX = CF zu erhalten und 1 Byte gegenübersetc bl
.dec
/jnz
inside Eineadc
Schleife bewahrt CF, ohne dass es beimadc
Lesen von Flags auf Intel Sandybridge und höher zu einem partiellen Flag- Stillstand kommt. Es ist schlecht auf früheren CPUs , aber AFAIK kostenlos auf Skylake. Oder im schlimmsten Fall eine Extragebühr.Verwenden Sie den folgenden Speicher
esp
als riesige rote Zone . Da es sich um ein vollständiges Linux-Programm handelt, habe ich keine Signal-Handler installiert, und nichts anderes wird den Stapelspeicher des Benutzerraums asynchron belasten. Dies ist bei anderen Betriebssystemen möglicherweise nicht der Fall.Nutzen Sie die Stack-Engine , um die Bandbreite des UOP-Problems zu verringern, indem Sie
pop eax
(1 UOP + gelegentliches Stack-Sync-UOP) anstelle vonlodsd
(2 UOP bei Haswell / Skylake, 3 bei IvB und früher gemäß den Anweisungen von Agner Fog ) verwenden. IIRC, dies verringerte die Laufzeit von ungefähr 83 Sekunden auf 73. Ich könnte wahrscheinlich die gleiche Geschwindigkeit erzielen, wenn ich einenmov
mit einem Index versehenen Adressierungsmodus verwende, etwamov eax, [edi+ebp]
wennebp
der Versatz zwischen src- und dst-Puffern gehalten wird. (Dies würde den Code außerhalb der inneren Schleife komplexer machen und das Offset-Register als Teil des Austauschs von src und dst für Fibonacci-Iterationen negieren.) Weitere Informationen finden Sie im Abschnitt "Leistung" weiter unten.Starten Sie die Sequenz, indem Sie der ersten Iteration einen Übertrag (ein Byte
stc
) zuweisen , anstatt sie1
irgendwo im Speicher abzulegen. Viele andere problemspezifische Dinge sind in Kommentaren dokumentiert.NASM-Auflistung (Maschinencode + Quelle) , generiert mit
nasm -felf32 fibonacci-1G.asm -l /dev/stdout | cut -b -28,$((28+12))- | sed 's/^/ /'
. (Dann habe ich einige kommentierte Blöcke von Hand entfernt, damit die Zeilennummerierung Lücken aufweist.) Verwenden Sie, um die führenden Spalten auszublenden, damit Sie sie in YASM oder NASM einspeisen könnencut -b 27- <fibonacci-1G.lst > fibonacci-1G.asm
.Es gibt wahrscheinlich Platz, um ein paar Bytes mehr Golf zu spielen, aber ich habe bereits über 2 Tage mindestens 12 Stunden damit verbracht. Ich möchte nicht auf Geschwindigkeit verzichten, obwohl es viel mehr als schnell genug ist und es Raum gibt, es auf eine Weise zu verkleinern, die Geschwindigkeit kostet . Ein Grund für mein Posting ist, zu zeigen, wie schnell ich eine Brute-Force-Asm-Version erstellen kann. Wenn jemand wirklich die Mindestgröße anstreben möchte, aber vielleicht 10x langsamer (z. B. 1 Ziffer pro Byte), können Sie dies als Ausgangspunkt kopieren.
Die resultierende ausführbare Datei (von
yasm -felf32 -Worphan-labels -gdwarf2 fibonacci-1G.asm && ld -melf_i386 -o fibonacci-1G fibonacci-1G.o
) ist 340B (entfernt):Performance
Die innere
adc
Schleife besteht aus 10 Fused-Domain-Uops in Skylake (+1 Stack-Sync-Uop alle ~ 128 Bytes), sodass sie in Skylake mit optimalem Front-End-Durchsatz alle ~ 2,5 Zyklen ausgegeben werden kann (ohne Berücksichtigung der Stack-Sync-Uops). . Die Wartezeit auf dem kritischen Pfad beträgt 2 Zyklen für die durch die Schleife übertragene Abhängigkeitskette deradc
->cmp
-> nächsten Iterationadc
, sodass der Engpass das Front-End-Problemlimit von ~ 2,5 Zyklen pro Iteration sein sollte.adc eax, [edi + edx]
Es gibt 2 nicht fusionierte Domänen-Uops für die Ausführungsports: load + ALU. Er ist in den Decodern mit Mikrosicherungen versehen (1 UOP für verschmolzene Domänen), wird jedoch in der Ausgabephase aufgrund des indizierten Adressierungsmodus sogar in Haswell / Skylake auf 2 UOP für verschmolzene Domänen unlaminiert . Ich dachte, es würde mitadd eax, [edi + edx]
Mikrosicherungen verbunden bleiben , aber vielleicht funktioniert das Beibehalten der mit Mikrosicherungen verbundenen indizierten Adressierungsmodi nicht für Benutzeroberflächen, die bereits 3 Eingänge haben (Flags, Speicher und Ziel). Als ich es schrieb, dachte ich, es hätte keine Nachteile, aber ich habe mich geirrt. Diese Art der Behandlung von Kürzungen verlangsamt die innere Schleife jedes Mal, egal obedx
0 oder 4.Es wäre schneller, den Lese-Schreib-Versatz für das dst zu handhaben,
edi
indemedx
der Speicher versetzt und mit eingestellt wird . Alsoadc eax, [edi]
/ ... /mov [edi+edx], eax
/lea edi, [edi+4]
stattstosd
. Haswell und später können einen indizierten Speicher mit einer Fusionssicherung versehen. (Sandybridge / IvB würden es auch unlaminieren.)Auf Intel Haswell und früher
adc
undcmovc
sind 2 Uops jeweils mit 2c Latenz . (adc eax, [edi+edx]
ist noch nicht auf Haswell laminiert und wird als 3 Fused-Domain-Ups ausgegeben). Broadwell und später erlauben 3-Input-Uops für mehr als nur FMA (Haswell), Erstellenadc
undcmovc
(und ein paar andere Dinge) Single-Uop-Anweisungen, wie sie schon lange bei AMD sind. (Dies ist einer der Gründe, warum AMD bei GMP-Benchmarks mit erweiterter Genauigkeit seit langem gute Ergebnisse erzielt.) Auf jeden Fall sollte die innere Schleife von Haswell 12 Uops (gelegentlich +1 Stack-Sync-Uops) mit einem Front-End-Engpass von ~ 3c pro Uops umfassen Im besten Fall ignorieren Sie Stack-Sync-Uops.Die Verwendung
pop
ohne Ausgleichpush
innerhalb einer Schleife bedeutet, dass die Schleife nicht vom LSD (Loop Stream Detector) ausgeführt werden kann und jedes Mal neu aus dem UOP-Cache in den IDQ gelesen werden muss. Wenn überhaupt, ist es eine gute Sache bei Skylake, da eine 9- oder 10-Up-Schleife bei 4 Ups pro Zyklus nicht optimal funktioniert . Dies ist wahrscheinlich ein Grund, warum das Ersetzenlodsd
durchpop
so viel geholfen hat. (Das LSD kann die Uops nicht sperren, da sonst kein Platz für ein Stack-Sync-Uop vorhanden ist .) ( Übrigens deaktiviert ein Mikrocode-Update das LSD vollständig auf Skylake und Skylake-X, um einen Fehler zu beheben. Ich habe das gemessen vor dem Update.)Ich habe es auf Haswell profiliert und festgestellt, dass es in 381,31 Milliarden Taktzyklen läuft (unabhängig von der CPU-Frequenz, da es nur L1D-Cache verwendet, keinen Speicher). Der Durchsatz für Front-End-Probleme betrug 3,72 Fused-Domain-Ups pro Uhr gegenüber 3,70 für Skylake. (Aber natürlich waren die Instruktionen pro Zyklus von 2,87 auf 2,42 gesunken , weil
adc
undcmov
2 Ups auf Haswell sind.)push
zu ersetzenstosd
würde wahrscheinlich nicht so viel helfen, daadc [esp + edx]
jedes Mal ein Stack-Sync-Up ausgelöst würde. Und würde ein Byte kosten,std
damitlodsd
geht die andere Richtung. (mov [edi], eax
/lea edi, [edi+4]
zu ersetzenstosd
ist ein Gewinn, der von 32.909Mcycles für 100M-Iter auf 31.954Mcycles für 100M-Iter steigt. Es scheint, dass diestosd
Decodierung als 3 Uops erfolgt, wobei die Store-Adresse / Store-Daten-Uops nicht mikroverschmolzen sind, alsopush
+ Stack-Sync uops könnte noch schneller sein alsstosd
)Die tatsächliche Leistung von ~ 322,47 Milliarden Zyklen für 1G-Iterationen von 114 Gliedmaßen entspricht 2,824 Zyklen pro Iteration der inneren Schleife für die schnelle 105B-Version auf Skylake. (Siehe
ocperf.py
Ausgabe unten). Das ist langsamer als ich es aus der statischen Analyse vorhergesagt habe, aber ich habe den Overhead der äußeren Schleife und alle Stack-Sync-Ups ignoriert.Perf-Zähler für
branches
undbranch-misses
zeigen, dass die innere Schleife einmal pro äußere Schleife falsch voraussagt (bei der letzten Iteration, wenn sie nicht ausgeführt wird). Das macht auch einen Teil der Verlängerung aus.Ich könnte Codegröße sparen, indem die innerste Schleife eine Latenzzeit von 3 Zyklen für den kritischen Pfad aufweist, indem ich
mov esi,eax
/sub eax,ebp
/cmovc eax, esi
/cmc
(2 + 2 + 3 + 1 = 8B) anstelle vonlea esi, [eax - 1000000000]
/cmp ebp,eax
/cmovc
(6 + 2 + 3 = 11B) verwende ). Dascmov
/stosd
ist aus dem kritischen Pfad. (Die inkrementelle Bearbeitung vonstosd
kann separat vom Speicher ausgeführt werden, sodass jede Iteration eine kurze Abhängigkeitskette ableitet.) Es wurde verwendet, um weitere 1B zu sparen, indem der Befehl ebp init vonlea ebp, [ecx-1]
auf geändert wurdemov ebp,eax
, aber ich habe festgestellt, dass die falsche Anweisung vorliegtebp
hat das Ergebnis nicht verändert. Dies würde eine Gliedmaße genau == 1000000000 sein lassen, anstatt einen Übertrag einzuwickeln und zu erzeugen, aber dieser Fehler breitet sich langsamer aus, als wir Fib () wachsen. Dies ändert also nicht die führenden 1k-Stellen des Endergebnisses. Ich denke auch, dass sich der Fehler von selbst korrigieren kann, wenn wir ihn nur hinzufügen, da in einem Glied Platz ist, um ihn ohne Überlauf zu halten. Sogar 1G + 1G überläuft eine 32-Bit-Ganzzahl nicht, so dass sie irgendwann nach oben sickert oder abgeschnitten wird.Die 3c-Latenz-Version kostet 1 zusätzlichen UOP, sodass das Front-End sie bei Skylake einmal pro 2,75c-Zyklen ausgeben kann, was nur geringfügig schneller ist, als das Back-End sie ausführen kann. (Auf Haswell werden es insgesamt 13 Uops sein, da es immer noch
adc
und verwendetcmov
, und ein Engpass auf dem Front-End bei 3,25 c pro Iter).In der Praxis läuft es auf Skylake um einen Faktor von 1,18 langsamer (3,34 Zyklen pro Glied) als 3 / 2,5 = 1,2, was ich vorhergesagt habe, um den Front-End-Engpass durch den Latenz-Engpass zu ersetzen, indem ich nur die innere Schleife ohne Stack-Sync betrachtete uops. Da die Stack-Sync-Ups nur der schnellen Version schaden (Engpass am Front-End anstelle von Latenz), ist es nicht sehr schwierig, dies zu erklären. zB 3 / 2,54 = 1,18.
Ein weiterer Faktor ist, dass die 3c-Latenz-Version möglicherweise den Fehler beim Verlassen der inneren Schleife erkennt, während der kritische Pfad noch ausgeführt wird (da das Front-End vor dem Back-End stehen kann und die Ausführung der Schleife außerhalb der Reihenfolge ausgeführt wird). counter uops), so dass die effektive Strafe für falsche Voraussagen geringer ist. Der Verlust dieser Front-End-Zyklen lässt das Back-End aufholen.
Wäre dies nicht
cmc
der Fall , könnten wir die 3c- Version möglicherweise beschleunigen, indem wir eine Verzweigung in der äußeren Schleife anstelle der verzweigungslosen Behandlung der Verschiebungen carry_out -> edx und esp verwenden. Die Verzweigungsvorhersage und die spekulative Ausführung für eine Steuerungsabhängigkeit anstelle einer Datenabhängigkeit können dazu führen, dass die nächste Iteration dieadc
Schleife ausführt, während sich die Uops der vorherigen inneren Schleife noch im Flug befinden. In der verzweigungslosen Version haben die Ladeadressen in der inneren Schleife eine Datenabhängigkeit von CF vom letztenadc
der letzten Glieder.Die 2c-Latenz-Inner-Loop-Version hat Engpässe im Front-End, sodass das Back-End so ziemlich mithält. Wenn der Code für die äußere Schleife eine hohe Latenz aufweist, kann das Front-End ab der nächsten Iteration der inneren Schleife Uops ausgeben. (Aber in diesem Fall hat das Outer-Loop-Material viel ILP und kein Material mit hoher Latenz, sodass das Back-End nicht viel Nachholbedarf hat, wenn es im Out-of-Order-Scheduler as durch Uops kaut ihre Eingaben werden fertig).
( +- x %)
ist die Standardabweichung über die 4 Läufe für diese Zählung. Interessant, dass es so eine runde Anzahl von Anweisungen ausführt. Diese 924 Milliarden sind kein Zufall. Ich vermute, dass die äußere Schleife insgesamt 924 Anweisungen ausführt.uops_issued
uops_executed
Hierbei handelt es sich um eine Fused-Domain-Anzahl (relevant für die Bandbreite des Front-End-Problems), während es sich um eine Nicht-Fused-Domain-Anzahl handelt (Anzahl der an Ausführungsports gesendeten Uops). Micro-Fusion packt 2 nicht fusionierte Domänen-Uops in ein nicht fusioniertes Domänen-Uop, aber Mov-Elimination bedeutet, dass einige nicht fusionierte Domänen-Uops keine Ausführungsports benötigen. In der verknüpften Frage finden Sie weitere Informationen zum Zählen von "Uops" und "Fused" und "Unfused" -Domain. (Siehe auch die Anweisungstabellen und das Handbuch von Agner Fog sowie andere nützliche Links im SO x86-Tag-Wiki. )Aus einem anderen Lauf, der verschiedene Dinge misst: L1D-Cache-Fehlschläge sind völlig unbedeutend, wie erwartet, um dieselben zwei 456B-Puffer zu lesen / schreiben. Der innere Schleifenzweig sagt einmal pro äußerer Schleife etwas falsch voraus (wenn es nicht erforderlich ist, die Schleife zu verlassen). (Die Gesamtzeit ist höher, da der Computer nicht vollständig im Leerlauf war. Wahrscheinlich war der andere logische Kern einige Zeit aktiv und es wurde mehr Zeit für Interrupts aufgewendet (da die vom Benutzer gemessene Frequenz weiter unter 4,400 GHz lag). Oder mehrere Kerne waren die meiste Zeit aktiv und senkten den maximalen Turbo. Ich habe nicht verfolgt
cpu_clk_unhalted.one_thread_active
, ob die HT-Konkurrenz ein Problem war.)Mein Code kann auf Ryzen in weniger Zyklen ausgeführt werden, wodurch 5 Uops pro Zyklus ausgegeben werden können (oder 6, wenn es sich bei einigen von ihnen um 2-Uops-Anweisungen handelt, wie z. B. AVX 256b-Dateien auf Ryzen). Ich bin mir nicht sicher, was das Front-End damit
stosd
anfangen würde , das sind 3 Ups auf Ryzen (genau wie Intel). Ich denke, die anderen Anweisungen in der inneren Schleife sind die gleiche Latenz wie Skylake und alle Single-UOP. (Einschließlichadc eax, [edi+edx]
, was ein Vorteil gegenüber Skylake ist).Dies könnte wahrscheinlich deutlich kleiner sein, aber vielleicht 9x langsamer, wenn ich die Zahlen als 1 Dezimalstelle pro Byte gespeichert habe . Das Erzeugen von Carry-Outs
cmp
und das Anpassen von Carry-Outs mitcmov
würde genauso funktionieren, aber 1/9 der Arbeit erledigen. 2 Dezimalstellen pro Byte (Base-100, nicht 4-Bit-BCD mit einer langsamenDAA
) würden ebenfalls funktionieren, unddiv r8
/add ax, 0x3030
wandelt ein 0-99-Byte in der Druckreihenfolge in zwei ASCII-Stellen um. Eine Ziffer pro Byte ist jedoch nicht erforderlich. Siediv
müssen lediglich eine Schleife ausführen und 0x30 hinzufügen. Wenn ich die Bytes in Druckreihenfolge speichere, würde das die 2. Schleife wirklich einfach machen.Die Verwendung von 18 oder 19 Dezimalstellen pro 64-Bit-Ganzzahl (im 64-Bit-Modus) würde die Geschwindigkeit etwa verdoppeln, jedoch die Codegröße für alle REX-Präfixe und für 64-Bit-Konstanten erheblich beeinträchtigen. 32-Bit-Zweige im 64-Bit-Modus verhindern die Verwendung von
pop eax
anstelle vonlodsd
. Ich könnte immer noch REX-Präfixe vermeiden, indem ichesp
ein Nicht-Zeiger-Scratch-Register verwende (wobei die Verwendung vonesi
und vertauscht wirdesp
), anstatt esr8d
als 8. Register zu verwenden.Wenn Sie eine Callable-Function-Version
r8d
erstellen , ist die Konvertierung in 64-Bit und die Verwendung möglicherweise billiger als das Speichern / Wiederherstellenrsp
. 64-Bit kann die Ein-Byte-dec r32
Codierung auch nicht verwenden (da es sich um ein REX-Präfix handelt). Aber meistens habe ichdec bl
2 Bytes benutzt. (Weil ich eine Konstante in den oberen Bytes von habeebx
und sie nur außerhalb der inneren Schleifen verwende, was funktioniert, weil das untere Byte der Konstante ist0x00
.)Hochleistungsversion
Für maximale Leistung (nicht Code-Golf) sollten Sie die innere Schleife ausrollen, damit sie höchstens 22 Iterationen durchläuft. Dies ist ein kurz genug gewähltes / nicht gewähltes Muster, damit die Verzweigungsvorhersagen gut abschneiden können. In meinen Experimenten hat
mov cl, 22
eine.inner: dec cl/jnz .inner
Schleife nur sehr wenige falsche Vorhersagen (wie 0,05%, weit weniger als eine pro vollem Lauf der inneren Schleife), abermov cl,23
falsche Vorhersagen vom 0,35- bis 0,6-fachen pro innerer Schleife.46
ist besonders schlimm, da es ~ 1,28-mal pro innerer Schleife falsch vorhersagt (128-mal für 100-mal äußere Schleifeniterationen).114
genau einmal pro innerer Schleife falsch vorhergesagt, so wie ich es als Teil der Fibonacci-Schleife fand.Ich wurde neugierig und versuchte es, indem ich die innere Schleife mit einem 6 abrollte
%rep 6
(weil das 114 gleichmäßig teilt). Damit wurden Branch-Misses größtenteils beseitigt. Ich habe einedx
Negativ gemacht und es als Offset fürmov
Läden verwendet, damitadc eax,[edi]
es mit dem Mikro verschmolzen bleibt. (Und so konnte ich es vermeidenstosd
). Ich habe daslea
zu aktualisierendeedi
Element aus dem%rep
Block gezogen, sodass es nur ein Zeiger-Update pro 6 Speicher ausführt .Ich habe auch all das Teilregister-Zeug in der äußeren Schleife losgeworden, obwohl ich denke, dass das nicht signifikant war. Möglicherweise hat es etwas geholfen, dass CF am Ende der äußeren Schleife nicht vom endgültigen ADC abhängt, sodass einige der Uops der inneren Schleife gestartet werden können. Der Outer-Loop-Code könnte wahrscheinlich ein bisschen weiter optimiert werden, da
neg edx
ich das letzte Mal nach dem Ersetzenxchg
durch nur 2mov
Anweisungen (da ich bereits 1 hatte) und dem Neuanordnen der Dep-Ketten und dem Löschen der 8-Bit-Anweisungen tat Zeug registrieren.Dies ist die NASM-Quelle nur der Fibonacci-Schleife. Es ist ein direkter Ersatz für diesen Abschnitt der Originalversion.
Performance:
Das ist für die gleiche Fib (1G), was die gleiche Ausgabe in 62,3 Sekunden anstelle von 73 Sekunden ergibt. (273.146G Zyklen gegenüber 322.467G. Da im L1-Cache alles zutrifft, müssen wir uns nur die Kerntaktzyklen ansehen.)
Beachten Sie die viel niedrigere Gesamtanzahl
uops_issued
, die deutlich unter deruops_executed
Anzahl liegt. Das bedeutet, dass viele von ihnen mikrofusioniert waren: 1 UOP in der fusionierten Domäne (Issue / ROB), aber 2 UOP in der nicht fusionierten Domäne (Scheduler / Ausführungseinheiten). Und diese wenigen wurden in der Phase der Ausgabe / Umbenennung beseitigt (wiemov
das Kopieren von Registern oderxor
Ausgabe- -Nullsetzen, die ausgegeben werden müssen, aber keine Ausführungseinheit benötigen). Eliminierte Ups würden die Zählung auf die andere Weise aus dem Gleichgewicht bringen.branch-misses
ist auf ~ 400k gesunken, von 1G, also hat das Abrollen funktioniert.resource_stalls.any
Dies bedeutet, dass das Front-End nicht mehr der Engpass ist, sondern dass das Back-End in Verzug gerät und das Front-End einschränkt.idq_uops_not_delivered.core
Zählt nur Zyklen, in denen das Front-End keine Ups lieferte, das Back-End jedoch nicht blockiert war. Das ist nett und niedrig, was auf wenige Front-End-Engpässe hindeutet.Witzige Tatsache: Die Python-Version verbringt mehr als die Hälfte ihrer Zeit damit, durch 10 zu dividieren, anstatt sie zu addieren. (Ersetzen der
a/=10
mita>>=64
Geschwindigkeit um mehr als den Faktor 2 erhöht, das Ergebnis wird jedoch geändert, da binäre Kürzung! = Dezimaltrennung.)Meine asm-Version ist natürlich speziell für diese Problemgröße optimiert, wobei die Anzahl der Schleifeniterationen fest codiert ist. Selbst das Verschieben einer Zahl mit willkürlicher Genauigkeit kopiert sie, aber meine Version kann nur von einem Versatz für die nächsten zwei Iterationen lesen, um auch das zu überspringen.
Ich habe die Python-Version (64-Bit-Python2.7 unter Arch Linux) profiliert :
Die Zahlen in (parens) geben an, wie oft der Perf-Counter abgetastet wurde. Wenn Sie mehr Zähler als die von HW unterstützten anzeigen, wechselt perf zwischen verschiedenen Zählern und Extrapolaten. Das ist völlig in Ordnung für einen langen Zeitraum der gleichen Aufgabe.
Wenn ich
perf
nach dem Setzen von sysctlkernel.perf_event_paranoid = 0
(oderperf
als root) lief, würde es messen4.400GHz
.cycles:u
Zählt nicht die Zeit, die in Interrupts (oder Systemaufrufen) verbracht wurde, sondern nur Benutzerbereichszyklen. Mein Desktop war fast leer, aber das ist typisch.quelle
Haskell,
83-61BytesAusgänge ( F 1000000000 , F 1000000001 ). Auf meinem Laptop werden der linke Teil und die ersten 1000 Stellen innerhalb von 133 Sekunden mit 1,35 GB Speicher korrekt gedruckt.
Wie es funktioniert
Das Fibonacci-Rezidiv kann durch Matrixexponentiation gelöst werden:
[ F i - 1 , F i ; F i , F i + 1 ] = [0, 1; 1, 1] i ,
von denen leiten wir diese Identitäten ab:
[ 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 .
Die
p
Funktion berechnet ( F i + j , F i + j + 1 ) gegeben ( F i , F i + 1 ) und ( F j , F j + 1 ). Wenn wirf n
für ( F i , F i + 1 ) schreiben , haben wirp (f i) (f j)
=f (i + j)
.Dann,
(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)
,und wir stecken ein
f 1
=(1,1)
.quelle
Mathematica, 15
34BytesFibonacci
selbst dauert ~ 6s auf meinem Computer. Und 95 (+/- 5) s für das Frontend, um es anzuzeigen.Die ersten 1000 Stellen (34 Bytes):
⌊Fibonacci@1*^9/1*^208986640⌋&
Länger aber schneller
ToString@Fibonacci@1*^9~StringTake~1000&
:quelle
div
). Ich hörte auf, da die Leute wahrscheinlich mit der Beantwortung dieser Frage fertig waren, als ich eine gut golfene Funktion hatte, die all diese Arbeit erledigte. Aber anscheinend kann Brute-Force funktionieren, wie einige Antworten zeigen.Python 2, 70 Bytes
Dies lief in 18 Minuten und 31 Sekunden auf meinem Laptop und ergab die richtigen 1000 Ziffern, gefolgt von
74100118580
(die richtigen folgenden Ziffern sind74248787892
).quelle
div
Schleife, um 9 Dezimalstellen pro Block zu bilden. Tragen Sie während des Hinzufügens mit cmp / cmov und 2xADD anstelle von ADC.Haskell , 78 Bytes
Probieren Sie es online!
Hat 48 Sekunden mit TIO gedauert. Dieselbe rekursive Formel wie meine Python-Antwort , jedoch ohne Kürzung.
Die Konstante
2143923439
ist10**9-1
binär umgekehrt und mit einer zusätzlichen 1 am Ende. Das umgekehrte Durchlaufen der Binärziffern simuliert das Durchlaufen der Binärziffern von10**9-1
. Es scheint kürzer zu sein, dies fest zu codieren, als es zu berechnen.quelle
Haskell ,
202184174173170168164162 BytesProbieren Sie es online!
Erläuterung
Dies verwendet eine relativ schnelle Methode zur Berechnung von Fibonacci-Zahlen. Die Funktion
l
nimmt zwei Fibonacci-Zahlen und berechnet die Fibonacci-Zahlen 10 später, während sief
die n- ten und n + 1- ten Fibonacci-Zahlen und die 2n + 20- ten und 2n + 21- ten Fibonacci-Zahlen berechnet. Ich kette sie ziemlich willkürlich an, um 1 Milliarde zu bekommen und die ersten 1000 Ziffern zu bekommen.quelle
Haskell, 81 Bytes
Erläuterung
f n
Berechnet rekursiv dien
th Fibonacci-Zahl unter Verwendung der Wiederholung aus der Antwort von xnor mit Eliminierung von gemeinsamen Unterausdrücken. Im Gegensatz zu den anderen Lösungen, die O (log (n)) -Multiplikationen verwenden, haben wir eine O (log (n)) - Tiefenrekursion mit einem Verzweigungsfaktor von 2 für eine Komplexität von O (n) -Multiplikationen.Es ist jedoch nicht alles verloren! Da sich fast alle Aufrufe in der Nähe des unteren Randes des Rekursionsbaums befinden, können wir, wo immer möglich, eine schnelle native Arithmetik verwenden und viele Manipulationen an riesigen Bignums vermeiden. Es spuckt in ein paar Minuten eine Antwort auf meine Box aus.
quelle
T-SQL,
422 414453 Bytes (Verifiziert, jetzt im Wettbewerb!)BEARBEITUNG 2 : Geändert zu : Erhielt ein paar Bytes, erhöhte aber die Geschwindigkeit auf 1 Milliarde! Wird in 45 Stunden und 29 Minuten ausgeführt , überprüft die Eingabe anhand der angegebenen Zeichenfolge und zeigt zusätzliche 8 Zeichen an (was aufgrund von Rundungsfehlern möglicherweise nicht richtig ist).
INT BIGINT
DECIMAL(37,0)
T-SQL hat keine native Unterstützung für "Riesen-Zahlen" und musste daher meinen eigenen textbasierten Riesen-Zahlen-Addierer mit 1008-Zeichen-Strings rollen:
Hier ist die formatierte Version mit Kommentaren:
Grundsätzlich bearbeite ich 1008 Zeichen lange, mit Nullen gefüllte Zeichenfolgen, die meine beiden Fibonacci-Variablen darstellen,
@a
und@
.Ich füge sie
8 bis 18mal 36 Ziffern gleichzeitig hinzu, indem ich die letzten 36 Ziffern entferne, in einen überschaubaren numerischen Typ (DECIMAL(37,0)
) umwandle, sie addiere und dann wieder in eine andere lange Zeichenkette zerschmettere@c
. Ich "drehe" dann@a
und@
indem ich die letzten 36 Stellen nach vorne schiebe und den Vorgang wiederhole. 28 Umdrehungen * 36 Stellen decken alle 1008 ab. Ich muss die "eine" manuell tragen.Sobald unsere Zahl meine Stringlänge überschreitet, "verschiebe" ich mich nach links und wir verlieren etwas an Präzision, aber der Fehler liegt gut in meinen zusätzlichen Zeichen.
Ich habe versucht, eine SQL-Tabelle voller INTs und BIGINTs mit ähnlicher Logik zu verwenden, und sie war dramatisch langsamer. Seltsam.
quelle
PARI / GP, 45 Bytes
Irgendwie reicht
\p1000
das nicht. Dies funktioniert nicht mit 32-Bit-Systemen. Die letzte Unterteilung besteht darin, den Dezimalpunkt in der wissenschaftlichen Notation zu vermeiden.quelle
Pari / GP , 15 + 5 = 20 Bytes
Führen Sie den Befehl mit der Befehlszeilenoption aus
-s1g
, um 1 GB Speicher zuzuweisen.quelle
Ruby, 63 Bytes
Mann, ich bin schlecht darin, Rubin zu spielen; Aber die BigInt-Klasse tut Wunder für solche Sachen. Wir verwenden den gleichen Algorithmus wie Anders Kaseorg.
quelle