Il y a eu un milliard d'itérations de défis Fibonacci sur ce site Web, alors laissez pimenter les choses avec un défi Fibonacci d'un milliard d'itérations!
Votre défi est de générer les 1000 premiers chiffres décimaux du millionième millionième nombre de Fibonacci avec un programme aussi bref que possible. Cela peut ensuite éventuellement être suivi de toute sortie supplémentaire de votre choix, y compris, sans toutefois s'y limiter, le reste des chiffres.
J'utilise la convention qui fib 0 = 0
, fib 1 = 1
.
Votre programme doit être assez rapide pour que vous puissiez l'exécuter et en vérifier l'exactitude. Pour cela, voici les 1000 premiers chiffres:
7952317874554683467829385196197148189255542185234398913453039937343246686182519370050999626136556779332482035723222451226291714456275648259499530612111301255499879639516053459789018700567439946844843034599802419924043753401950114830107234265037841426980398387360784284231996457340782784200767760907777703183185744656536253511502851715963351023990699232595471322670365506482435966586886048627159716916351448788527427435508113909167963907380398242848033980110276370544264285032744364781198451825462130529529633339813483105771370128111851128247136311414208318983802526907917787094802217750859685116363883374847428036737147882079956688807509158372249451437519320162582002000530798309887261257028201907509370554232931107084976854715833585623910450679449120011564762925649144509531904684984417002512086504020779012501356177874199605085558317190905395134468919443313026824813363234190494375599262553025466528838122639433600483849535070647711986769279568548796855207684897741771784375859496425384355879105799
code-golf
kolmogorov-complexity
fibonacci
restricted-time
utilisateur1502040
la source
la source
Your program must be fast enough for you to run it and verify its correctness.
qu'en est-il de la mémoire?a+=b;b+=a;
visais l'évidence, je pense qu'une boucle (peut-être avec Java BigInteger) est le choix évident, du moins si vous songez même à la performance. Une implémentation récursive m'a toujours semblé horriblement inefficace.write()
appel système). J'aime l'exigence de performance, cela le rendait beaucoup plus amusant pour moi.Réponses:
Python 2 + sympy, 72 octets
Essayez-le en ligne!
-10 octets en supprimant le terme pratiquement-0 grâce à Jeff Dege
-1 octet (1000 -> 1e3 grâce à Zacharý)
-2 octets en supprimant la variable inutile grâce à Erik the Outgolfer
-2 octets en passant à Python 2 grâce à Zacharý
-3 octets en 11 'les
-11
remerciements à ThePirateBay -3 octets en échangeantstr
des backticks grâce à notjaganbat désormais la solution haskell non intégrée de OP!
la source
from sympy import*;sqrt
ne permet pas d'économiser plus d'octetsimport sympy;sympy.sqrt
:)sympy
est un paquet mathématique symbolique pour Python, il n'y a donc aucun problème d'erreur d'arrondi, du moins jusqu'à ce que les nombres soient très grands (ce nombre n'est pas assez grand lol). Ensuite, je calcule simplement pour me donner les 1e3 premiers chiffres car sinon, si vous supprimez la.evalf(1e3)
partie, cela me donne une représentation très courte de la notation scientifique.Python 2 , 106 octets
Essayez-le en ligne!
Aucune bibliothèque, juste arithmétique entière. Fonctionne presque instantanément.
Le noyau est l'identité diviser pour régner:
Cela nous permet de mettre
(a,b) = (f(n),f(n+1))
à jour pour doublern -> 2*n
. Puisque nous voulons obtenirn=10**9
, cela ne prend que deslog_2(10**9)=30
itérations. Nous construisonsn
en10**9
faisant de manière répétéen->2*n+c
pour chaque chiffrec
de son développement binaire. Quandc==1
, la valeur doublée est augmentée2*n -> 2*n+1
avec un décalage de Fibonacci en une étape(a,b)=(b+a,b)
Pour que les valeurs restent
a,b
gérables, nous ne stockons que leurs premiers1006
chiffres par division du sol10
jusqu'à ce qu'ils soient en dessous2**3340 ~ 1e1006
.la source
a,b,c=a*a+b*b,a*a-c*c,b*b+c*c
.Code machine x86 32 bits (avec appels système Linux):
106105 octetschangelog: enregistre un octet dans la version rapide, car une constante par un ne change pas le résultat pour Fib (1G).
Ou 102 octets pour une version de 18% plus lente (sur Skylake) (en utilisant
mov
/sub
/cmc
au lieu delea
/cmp
dans la boucle interne, pour générer le report et l’emballage à la10**9
place de2**32
). Ou 101 octets pour une version ~ 5.3x plus lente avec une branche dans la gestion du report dans la boucle la plus interne. (J'ai mesuré un taux de pronostic erroné de branche de 25,4%!)Ou 104/101 octets si un zéro est autorisé. (Il faut 1 octet supplémentaire pour que 1 code soit ignoré, ce qui est nécessaire pour Fib (10 ** 9)).
Malheureusement, le mode NASM de TIO semble ignorer
-felf32
dans les drapeaux du compilateur. Voici quand même un lien avec mon code source complet, avec tout le fouillis d'idées expérimentales dans les commentaires.Ceci est un programme complet . Il imprime les 1000 premiers chiffres de Fib (10 ** 9) suivis de quelques chiffres supplémentaires (les derniers étant erronés), suivis de quelques octets parasites (sans nouvelle ligne). La plupart des déchets sont non-ASCII, vous voudrez peut-être passer à travers
cat -v
. Cela ne casse pas mon émulateur de terminal (KDEkonsole
), cependant. Les "octets de mémoire" stockent Fib (999999999). J'avais déjà-1024
dans un registre, il était donc moins cher d'imprimer 1024 octets que la taille appropriée.Je ne compte que le code machine (taille du segment de texte de mon exécutable statique), pas le fluff qui en fait un exécutable ELF. (De très petits exécutables ELF sont possibles , mais je ne voulais pas m'en soucier). Il s'est avéré que l'utilisation de la mémoire de pile au lieu de BSS était plus courte, je peux donc justifier de ne rien compter d'autre dans le binaire, car je ne dépend d'aucune métadonnée. (Produire un binaire statique épuré de la manière habituelle rend un exécutable ELF de 340 octets.)
Vous pouvez créer une fonction à partir de ce code que vous pourriez appeler à partir de C. Cela coûterait quelques octets pour sauvegarder / restaurer le pointeur de pile (peut-être dans un registre MMX) et un autre temps système, mais également pour économiser des octets en retournant avec la chaîne en mémoire, au lieu de faire un
write(1,buf,len)
appel système. Je pense que jouer au golf dans le code machine devrait me faire perdre un peu de temps ici, car personne d’autre n’a même posté de réponse dans une langue qui ne possède pas une précision étendue native, mais je pense qu’une version fonctionnelle de cela devrait être inférieure à 120 octets sans avoir à re-jouer au golf dans son ensemble. chose.Algorithme:
force brute
a+=b; swap(a,b)
, tronquée au besoin pour ne conserver que le premier> = 1017 chiffres décimaux. Il fonctionne en 1min13s sur mon ordinateur (ou 322,47 milliards de cycles d'horloge + - 0,05%) (et pourrait être quelques% plus rapide avec quelques octets de taille de code supplémentaires, ou jusqu'à 62s avec une taille de code beaucoup plus grande à partir du déroulement de la boucle. Non mathématiques intelligentes, faisant juste le même travail avec moins de frais généraux). Il est basé sur l'implémentation Python de @ AndersKaseorg , qui s'exécute en 12 min 35 s sur mon ordinateur (Skylake i7-6700k à 4,4 GHz). Aucune des versions ne contient de cache L1D manquant, mon DDR4-2666 n'a donc aucune importance.À la différence de Python, je stocke les nombres à précision étendue dans un format qui permet de tronquer les chiffres décimaux gratuitement . Je stocke des groupes de 9 chiffres décimaux par entier de 32 bits, de sorte qu'un décalage de pointeur ignore les 9 chiffres les plus bas. Il s’agit bien d’un milliard de base, ce qui représente une puissance de 10. (C’est une pure coïncidence que ce défi nécessite le milliardième chiffre de Fibonacci, mais il m’économise quelques octets au lieu de deux constantes distinctes.)
Selon la terminologie GMP , chaque bloc de 32 bits d’un nombre à précision étendue est appelé un "membre". L'exécution lors de l'ajout doit être générée manuellement avec une comparaison avec 1e9, mais est ensuite utilisée normalement comme entrée de l'
ADC
instruction habituelle pour le membre suivant. (Je dois aussi[0..999999999]
retourner manuellement à la plage plutôt qu'à 2 ^ 32 ~ = 4.295e9. Je le fais sans branche aveclea
+cmov
, en utilisant le résultat de report de la comparaison.)Lorsque le dernier membre produit un report non nul, les deux itérations suivantes de la boucle externe lisent un membre plus haut que la normale, mais écrivent toujours au même endroit. Cela revient à faire un
memcpy(a, a+4, 114*4)
virage à droite d'un membre, mais dans le cadre des deux boucles d'addition suivantes. Cela se produit chaque ~ 18 itérations.Des astuces pour gagner en taille et en performance:
Les trucs habituels comme
lea ebx, [eax-4 + 1]
au lieu demov ebx, 1
, quand je le saiseax=4
. Et utiliserloop
dans des endroits oùLOOP
la lenteur n'a qu'un impact minime.Découpez gratuitement un membre en décalant les pointeurs à partir desquels nous lisons, tout en écrivant au début du tampon dans la
adc
boucle interne. Nous lisons depuis[edi+edx]
et écrivons à[edi]
. Nous pouvons donc obteniredx=0
ou4
obtenir un décalage lecture-écriture pour la destination. Nous devons faire cela pour 2 itérations successives, en compensant d'abord les deux, puis en ne compensant que le dst. Nous détectons le 2e cas en regardantesp&4
avant de réinitialiser les pointeurs au début des tampons (en utilisant&= -1024
, car les tampons sont alignés). Voir les commentaires dans le code.L'environnement de démarrage de processus Linux (pour un exécutable statique) contient la plupart des registres, et la mémoire de pile en dessous de
esp
/rsp
est mise à zéro. Mon programme en profite. Dans une version appelable de cette fonction (où une pile non allouée pourrait être sale), je pourrais utiliser BSS pour la mémoire mise à zéro (au prix de peut-être 4 octets supplémentaires pour configurer les pointeurs). La réduction à zéroedx
prendrait 2 octets. L’ABI System V x86-64 ne garantit aucun de ces éléments, mais son implémentation en fait zéro (pour éviter les fuites d’informations hors du noyau). Dans un processus lié dynamiquement,/lib/ld.so
s'exécute avant_start
et laisse des registres différents de zéro (et probablement de la mémoire en mémoire sous le pointeur de pile).Je garde
-1024
enebx
pour une utilisation en dehors des boucles. Utiliserbl
comme un compteur pour les boucles internes, se terminant par zéro (qui est l’octet de poids faible de-1024
, restaurant ainsi la constante pour une utilisation en dehors de la boucle). Intel Haswell et les versions ultérieures ne prévoient pas de pénalité pour la fusion de registres partiels pour les registres low8 (et ne les renomment même pas séparément) . Il existe donc une dépendance sur le registre complet, comme sur AMD (ce n’est pas un problème ici). Cela serait horrible sur Nehalem et plus tôt, cependant, qui ont des blocages de registres partiels lors de la fusion. Il y a d'autres endroits où j'écris des regs partiels, puis je lis les regs complets sansxor
-zeroing ou unmovzx
, généralement parce que je sais que certains codes antérieurs ont mis à zéro les octets supérieurs, et encore une fois, c’est bien sur AMD et la famille Intel SnB, mais lent sur Intel avant Sandybridge.J'utilise
1024
comme nombre d'octets pour écrire dans stdout (sub edx, ebx
), de sorte que mon programme imprime des octets parasites après les chiffres de Fibonacci, car ilsmov edx, 1000
coûtent plus d'octets.(non utilisé)
adc ebx,ebx
avec EBX = 0 pour obtenir EBX = CF, en économisant 1 octet contresetc bl
.dec
/ à l'jnz
intérieur d'uneadc
boucle préserve CF sans provoquer deadc
blocage d'indicateurs partiels lors de la lecture d'indicateurs sur Intel Sandybridge et versions ultérieures. C'est mauvais sur les anciens processeurs , mais autant que je sache, sur Skylake. Ou au pire, un extra uop.Utilisez la mémoire ci-dessous
esp
comme une zone rouge géante . Comme il s’agit d’un programme complet pour Linux, je sais que je n’ai installé aucun gestionnaire de signaux, et que rien d’autre ne videra la mémoire de pile d’espace utilisateur de manière asynchrone. Cela peut ne pas être le cas sur d'autres systèmes d'exploitation.Tirez parti du moteur de pile pour économiser la bande passante des problèmes uop en utilisant
pop eax
(1 uop + synchronisation ponctuelle occasionnelle) au lieu delodsd
(2 uops sur Haswell / Skylake, 3 sur IvB et plus tôt selon les tableaux d'instructions d'Agner Fog )). IIRC, le temps d'exécution est passé d'environ 83 secondes à 73 secondes. Je pourrais probablement obtenir la même vitesse d'utilisationmov
d'un mode d'adressage indexé, comme dans lemov eax, [edi+ebp]
cas oùebp
le décalage entre les tampons src et dst est maintenu. (Cela compliquerait le code en dehors de la boucle interne, car il faudrait annuler le registre de décalage dans le cadre de l'échange de src et de dst pour les itérations de Fibonacci.) Voir la section "performance" ci-dessous pour plus d'informations.Commencez la séquence en donnant à la première itération un report (un octet
stc
), au lieu de stocker un1
en mémoire n'importe où. Beaucoup d'autres choses spécifiques à un problème documentées dans les commentaires.Liste NASM (code machine + source) , générée avec
nasm -felf32 fibonacci-1G.asm -l /dev/stdout | cut -b -28,$((28+12))- | sed 's/^/ /'
. (Ensuite, j'ai enlevé à la main quelques blocs d'éléments commentés, afin que la numérotation des lignes comporte des espaces.) Pour effacer les colonnes de tête afin de pouvoir l'insérer dans YASM ou NASM, utilisezcut -b 27- <fibonacci-1G.lst > fibonacci-1G.asm
.Il y a probablement de la place pour jouer au golf encore quelques octets, mais j'ai déjà passé au moins 12 heures à ce sujet pendant 2 jours. Je ne veux pas sacrifier la vitesse, même si elle est beaucoup plus rapide et qu'il est possible de la réduire à un coût aussi rapide . Une partie de ma raison d’afficher montre à quelle vitesse je peux créer une version brute-force asm. Si quelqu'un veut vraiment opter pour une taille minimale, mais peut-être 10 fois plus lente (par exemple, un chiffre par octet), n'hésitez pas à copier ceci comme point de départ.
Le fichier exécutable résultant (from
yasm -felf32 -Worphan-labels -gdwarf2 fibonacci-1G.asm && ld -melf_i386 -o fibonacci-1G fibonacci-1G.o
) est 340B (stripped):Performance
La
adc
boucle interne correspond à 10 Uops à domaine fondu sur Skylake (+1 pile de synchronisation tous les 128 octets environ). Elle peut ainsi émettre au moins 2,5 cycles sur Skylake avec un débit frontal optimal (en ignorant les uops de synchronisation de pile). . La latence du chemin critique est de 2 cycles, pour la chaîne de dépendance acheminée par la boucle de l'itérationadc
->cmp
-> suivanteadc
;adc eax, [edi + edx]
est 2 uops de domaine non-fusionné pour les ports d’exécution: charge + ALU. Il micro-fusionnent dans les décodeurs (1 uop à domaine fondu), mais non laminés à l'étape d'émission en 2 uops à domaine fondu, en raison du mode d'adressage indexé, même sur Haswell / Skylake . Je pensais qu'il resterait micro-fusionné, comme leadd eax, [edi + edx]
fait le reste , mais peut-être que conserver les modes d'adressage indexés micro-fusionnés ne fonctionne pas pour les Uops qui ont déjà 3 entrées (drapeaux, mémoire et destination). Quand je l'ai écrit, je pensais qu'il n'y aurait pas d'inconvénient en termes de performances, mais je me suis trompé. Cette façon de gérer la troncature ralentit la boucle interne à chaque fois, qu’il s’agisse deedx
0 ou de 4.Il serait plus rapide de gérer le décalage lecture-écriture pour le dst en décalant
edi
et en utilisantedx
pour ajuster le magasin. Doncadc eax, [edi]
/ ... /mov [edi+edx], eax
/lea edi, [edi+4]
au lieu destosd
. Haswell et plus tard peuvent garder un magasin indexé micro-fusionné. (Sandybridge / IvB le désamorcerait aussi.)Sur Intel Haswell et les versions antérieures,
adc
etcmovc
sont 2 uops chacun, avec une latence 2c . (adc eax, [edi+edx]
est toujours non-laminé sur Haswell, et est émis en tant que 3 uops à domaine fondu). Broadwell et les versions ultérieures autorisent les uops à 3 entrées pour plus que juste FMA (Haswell), en faisantadc
etcmovc
(et quelques autres choses) des instructions en mono-uop, comme si elles utilisaient la DMLA depuis longtemps. (C’est une des raisons pour lesquelles AMD s’est bien débrouillé avec les tests de performance GMP de précision étendue depuis longtemps.) Quoi qu’il en soit, la boucle interne de Haswell devrait être de 12 uops (+1 en synchronisation de pile à l’occasion), avec un goulot d’étranglement frontal de ~ 3c par iter dans le meilleur des cas, en ignorant les uops de synchronisation de pile.L'utilisation
pop
sans équilibrage à l'push
intérieur d'une boucle signifie que la boucle ne peut pas être exécutée à partir du LSD (détecteur de flux de boucle) et doit être relue à chaque fois à partir du cache uop dans l'IDQ. Au contraire, c’est une bonne chose sur Skylake, puisqu’une boucle de 9 ou 10 UOP n’émet pas de façon optimale à 4 UPS à chaque cycle . Cela fait probablement partie des raisons pour lesquelles remplacerlodsd
parpop
tellement aidé. (Le LSD ne peut pas verrouiller les uops car cela ne laisserait pas de place pour insérer une pile de synchronisation .) (BTW, une mise à jour au microcode désactive complètement le LSD sur Skylake et Skylake-X afin de corriger un erratum. ci-dessus avant d’obtenir cette mise à jour.)Je l’ai profilée sur Haswell et ai constaté qu’elle fonctionnait en 381,31 milliards de cycles d’horloge (quelle que soit la fréquence du processeur, car elle utilise uniquement le cache L1D, pas la mémoire). Le débit de sortie front-end était de 3,72 UOP par domaine fondu, contre 3,70 pour Skylake. (Mais bien sûr, le nombre d’instructions par cycle est passé de 2,87 à 2,42, parce que
adc
etcmov
sont 2 oups sur Haswell.)push
remplacerstosd
ne vous aiderait probablement pas autant, caradc [esp + edx]
cela déclencherait une synchronisation de pile à chaque fois. Et cela coûterait un octet, lastd
situationlodsd
va dans l'autre sens. (mov [edi], eax
/lea edi, [edi+4]
remplacerstosd
est une victoire, passant de 32,909Mcycles pour 100Miters à 31,954Mcycles pour 100Miters. Il semble questosd
décodage en 3 uops, avec les adresses de magasin / données de magasin non micro-fusionnées, doncpush
+ pile-sync uops pourrait encore être plus rapide questosd
)La performance réelle de ~ 322,47 milliards de cycles pour les itérations 1G de 114 membres équivaut à 2,824 cycles par itération de la boucle interne , pour la version rapide 105B sur Skylake. (Voir la
ocperf.py
sortie ci-dessous). C'est plus lent que prévu par l'analyse statique, mais j'ignorais les frais généraux de la boucle externe et de toutes les opérations de synchronisation de pile.Perf corrige
branches
etbranch-misses
montre que la boucle interne se trompe une fois par boucle externe (à la dernière itération, si elle n’est pas prise). Cela représente également une partie du temps supplémentaire.Je pourrais économiser du code en faisant en sorte que la boucle la plus interne ait une latence de 3 cycles pour le chemin critique, en utilisant
mov esi,eax
/sub eax,ebp
/cmovc eax, esi
/cmc
(2 + 2 + 3 + 1 = 8B) au lieu delea esi, [eax - 1000000000]
/cmp ebp,eax
/cmovc
(6 + 2 + 3 = 11B ). Lecmov
/stosd
est hors du chemin critique. (L'incrémentation-edi destosd
peut être exécutée séparément du magasin, chaque itération bifurque ainsi d'une chaîne de dépendance courte.) Il enregistrait un autre 1B en modifiant l'instruction d'initialisation ebp delea ebp, [ecx-1]
enmov ebp,eax
, mais j'ai découvert qu'avoir la mauvaiseebp
n'a pas changé le résultat. Cela laisserait un membre exactement = 1000000000 au lieu d’emballer et de produire un report, mais cette erreur se propage plus lentement que nous grandissons, de sorte que cela ne change pas les 1k premiers chiffres du résultat final. De plus, je pense que l'erreur peut se corriger d'elle-même lorsque nous ajoutons, car il y a de la place dans un membre pour la retenir sans débordement. Même 1G + 1G ne dépasse pas un entier de 32 bits, il finira par percoler vers le haut ou être tronqué.La version de latence 3c est 1 uop supplémentaire, ainsi le front-end peut la publier à un cycle par 2,75c sur Skylake, à peine légèrement plus rapide que le back-end ne peut l'exécuter. (Sur Haswell, ce sera 13 uops au total puisqu'il utilise toujours
adc
etcmov
, et un goulot d'étranglement sur le front-end à 3,25 centimes par iter).En pratique, Skylake ralentit le facteur 1,18 (3,34 cycles par membre) au lieu de 3 / 2,5 = 1,2 que je prédisais pour remplacer le goulot d'étranglement du front-end par le goulot d'étranglement dû au fait de regarder la boucle interne sans synchronisation de pile. uops. Comme les piles de synchronisation de pile ne font que mal à la version rapide (goulot d’étranglement au lieu de la latence), il n’en faut pas beaucoup pour l’expliquer. par exemple 3 / 2,54 = 1,18.
Un autre facteur est que la version de latence 3c peut détecter l’erreur lorsqu’elle quitte la boucle interne alors que le chemin critique est en cours d’exécution (car le serveur frontal peut avoir une longueur d’avance sur le back-end, ce qui permet à une exécution hors d’ordre d’exécuter la boucle. ainsi, la peine effective d’erreur de pronostic est plus faible. La perte de ces cycles frontaux permet au back-end de se rattraper.
Si ce n'était pas le cas, nous pourrions peut-être accélérer la
cmc
version 3c en utilisant une branche dans la boucle externe au lieu du traitement sans branche du décalage carry_out -> edx et esp. Une prédiction de branche + une exécution spéculative pour une dépendance de contrôle au lieu d'une dépendance de données pourrait permettre à la prochaine itération de démarrer l'exécution de laadc
boucle alors que des uops de la boucle interne précédente étaient encore en vol. Dans la version sans embranchement, les adresses de charge de la boucle interne ont une dépendance de données sur CF à partiradc
du dernier membre.Les goulots d'étranglement de la version de boucle interne de latence 2c sur le front-end, donc le back-end continue à peu près. Si le code de la boucle externe était à latence élevée, le serveur frontal pourrait aller de l'avant en émettant des uops dès la prochaine itération de la boucle interne. (Mais dans ce cas, la boucle externe contient beaucoup d' ILP et aucune charge de latence élevée. Par conséquent, le back-end n'a pas beaucoup de retard leurs entrées deviennent prêtes).
( +- x %)
est l'écart-type sur les 4 exécutions pour ce compte. Intéressant qu'il exécute un tel nombre rond d'instructions. Ces 924 milliards ne sont pas une coïncidence. Je suppose que la boucle externe exécute un total de 924 instructions.uops_issued
est un compte de domaine fondu (pertinent pour la bande passante d'émission front-end), alors qu'iluops_executed
s'agit d'un compte de domaine non fusionné (nombre d'UP envoyés aux ports d'exécution). La micro-fusion regroupe 2 Uops à domaine non fusionné dans un UOP à domaine fusionné, mais mov-élimination signifie que certains Uops à domaine fusionné ne nécessitent aucun port d'exécution. Voir la question liée pour en savoir plus sur le comptage des uops et des domaines fusionnés et non fusionnés. (Voir également les tableaux d'instructions et le guide uarch d'Agner Fog , ainsi que d'autres liens utiles dans le wiki des balises SO x86 ).D'une autre série, mesurant différentes choses: les erreurs de cache L1D sont totalement insignifiantes, comme prévu pour la lecture / écriture des deux mêmes tampons 456B. La branche de la boucle interne se trompe une fois par boucle externe (lorsqu'il n'est pas nécessaire de quitter la boucle). (Le temps total est plus long parce que l'ordinateur n'était pas totalement inactif. L'autre cœur logique a probablement été actif de temps en temps, et des interruptions supplémentaires ont eu lieu (la fréquence mesurée par l'utilisateur étant plus basse que 4,400 GHz). Ou plusieurs cœurs étaient actifs la plupart du temps, réduisant le turbo max. Je ne savais pas
cpu_clk_unhalted.one_thread_active
si la concurrence HT posait problème.)Mon code peut bien fonctionner en moins de cycles sur Ryzen, ce qui peut générer 5 uops par cycle (ou 6 lorsque certaines d'entre elles sont des instructions 2-uop, comme les fichiers AVX 256b sur Ryzen). Je ne suis pas sûr de ce que son front-end ferait avec
stosd
, qui est de 3 oups sur Ryzen (identique à Intel). Je pense que les autres instructions de la boucle interne ont la même latence que Skylake et que toutes les réponses sont individuelles. (Y comprisadc eax, [edi+edx]
, ce qui est un avantage sur Skylake).Cela pourrait probablement être beaucoup plus petit, mais peut-être 9 fois plus lent si je stockais les nombres sous la forme d'un chiffre décimal par octet . Générer un report avec
cmp
et s’ajuster aveccmov
fonctionnerait de la même façon, mais réalisez 1 / 9e du travail. Deux chiffres décimaux par octet (base 100, pas un BCD 4 bits avec une vitesse lenteDAA
) fonctionneraient également, etdiv r8
/add ax, 0x3030
convertit un octet 0-99 en deux chiffres ASCII dans l'ordre d'impression. Mais 1 chiffre par octet n'est pas nécessairediv
, il suffit de boucler et d'ajouter 0x30. Si je stocke les octets dans l’ordre d’impression, cela rendrait la deuxième boucle très simple.L'utilisation de 18 ou 19 chiffres décimaux par entier 64 bits (en mode 64 bits) lui permettrait de fonctionner environ deux fois plus vite, mais coûterait une taille de code significative pour tous les préfixes REX et pour les constantes 64 bits. Les membres 32 bits en mode 64 bits empêchent d'utiliser
pop eax
au lieu delodsd
. Je pouvais toujours éviter les préfixes REX en utilisantesp
un registre de travail non-pointeur (en échangeant l'utilisation deesi
etesp
) au lieu de l'utiliser enr8d
tant que 8ème registre.Si vous créez une version à fonction appelable, la conversion au format 64 bits et son utilisation
r8d
peuvent être moins onéreuses que la sauvegarde / restaurationrsp
. De plus, 64 bits ne peuvent pas utiliser ledec r32
codage sur un octet (puisqu'il s'agit d'un préfixe REX). Mais la plupart du temps, j'ai fini par utiliserdec bl
2 octets. (Parce que j'ai une constante dans les octets supérieurs deebx
et que je ne l'utilise qu'en dehors des boucles internes, ce qui fonctionne car l'octet de poids faible de la constante est0x00
.)Version haute performance
Pour obtenir des performances maximales (sans code-golf), vous souhaiterez dérouler la boucle interne de manière à ce qu'elle fonctionne au maximum en 22 itérations, ce qui est un modèle pris / non pris suffisamment court pour que les prédicteurs de branche fonctionnent correctement. Dans mes expériences,
mov cl, 22
avant une.inner: dec cl/jnz .inner
boucle, il y avait très peu de prédictions erronées (comme 0,05%, bien moins d'un par cycle complet de la boucle interne), maismov cl,23
de 0,35 à 0,6 fois par boucle interne.46
est particulièrement mauvais, avec une prévision erronée d’environ 1,28 fois par boucle interne (128 millions de fois pour 100 itérations de boucle externe).114
erroné, exactement une fois par boucle intérieure, comme dans la boucle de Fibonacci.Je suis devenu curieux et j'ai essayé, déroulant la boucle intérieure de 6 avec un
%rep 6
(parce que cela divise 114 uniformément). Cela a pour la plupart éliminé les échecs de branche. J'ai faitedx
négatif et utilisé comme compensation pour lesmov
magasins, doncadc eax,[edi]
je pouvais rester micro-fusionné. (Et donc je pourrais éviterstosd
). J'ai tiré lelea
pour mettre à jouredi
hors du%rep
bloc, donc il ne fait qu'un pointeur-mise à jour pour 6 magasins.Je me suis également débarrassé de tout ce qui concerne les registres partiels dans la boucle externe, bien que je ne pense pas que cela soit significatif. Il aurait peut-être été utile que la fin de la boucle externe ne soit pas dépendante de l'ADC final, de sorte que certaines des boucles internes puissent être démarrées. Le code de la boucle extérieure pourrait probablement être optimisé un peu plus, car
neg edx
c’était la dernière chose que j’ai faite, après avoir remplacéxchg
par seulement 2mov
instructions (puisque j’en avais déjà une) et réorganisé les chaînes dep avec suppression du code 8 bits. enregistrer des choses.C’est la source NASM de la boucle de Fibonacci. C'est un remplacement instantané de cette section de la version d'origine.
Performance:
C'est pour la même Fib (1G), produisant la même sortie en 62,3 secondes au lieu de 73 secondes. (273,146 G, contre 322,467 G. Puisque tout se trouve dans le cache N1, les cycles d’horloge de base sont tout ce que nous devons examiner.)
Notez le
uops_issued
nombre total beaucoup plus bas , bien en dessous duuops_executed
nombre. Cela signifie que beaucoup d'entre eux étaient micro-fusionnés: 1 uop dans le domaine fusionné (issue / ROB), mais 2 uops dans le domaine non fusionné (planificateur / unités d'exécution)). Et ces quelques-uns ont été éliminés à l'étape d'émission / de changement de nom (comme lamov
copie de registre ou laxor
réduction à zéro, qui doivent être émises mais n'ont pas besoin d'une unité d'exécution). Les uops éliminés déséquilibreraient le compte dans l'autre sens.branch-misses
est descendu à ~ 400k, à partir de 1G, donc le déroulement a fonctionné.resource_stalls.any
est important maintenant, ce qui signifie que le front-end n'est plus le goulot d'étranglement: au lieu de cela, le back-end prend du retard et limite le front-end.idq_uops_not_delivered.core
ne compte que les cycles où le front-end n’a pas livré de bonus, mais que le back-end n’a pas été bloqué. C'est bon et bas, indiquant peu de goulots d'étranglement au début.Fait amusant: la version en python passe plus de la moitié de son temps à être divisée par 10 au lieu d’ajouter. (Remplacer par
a/=10
para>>=64
accélère de plus d'un facteur 2, mais modifie le résultat car troncature binaire! = Troncature décimale.)Ma version asm est bien sûr optimisée spécifiquement pour cette taille de problème, avec la boucle itération-compte codée en dur. Même déplacer un nombre de précision arbitraire le copiera, mais ma version peut simplement lire à partir d'un décalage pour les deux itérations suivantes, même pour ignorer cela.
J'ai profilé la version de python (python2.7 64 bits sur Arch Linux):
Les nombres entre parenthèses indiquent combien de temps le compteur de perf était échantillonné. Quand on regarde plus de pions que le matériel ne supporte, perf tourne entre les pions différents et extrapole. C'est tout à fait bien pour une longue période de la même tâche.
Si je courais
perf
après avoir paramétré sysctlkernel.perf_event_paranoid = 0
(ou enperf
tant que root), cela se mesurerait4.400GHz
.cycles:u
ne compte pas le temps passé dans les interruptions (ou les appels système), mais uniquement les cycles de l'espace utilisateur. Mon bureau était presque totalement inactif, mais c'est typique.la source
Haskell,
8361 octetsLes sorties ( F 1000000000 , F 1000000001 ). Sur mon ordinateur portable, il imprime correctement le paren gauche et les 1000 premiers chiffres en 133 secondes, en utilisant 1,35 Go de mémoire.
Comment ça fonctionne
La récurrence de Fibonacci peut être résolue à l'aide de l'exponentiation matricielle:
[ F i - 1 , F i ; F i , F i + 1 ] = [0, 1; 1, 1] i ,
d'où proviennent ces identités:
[ 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 .
La
p
fonction calcule ( F i + j , F i + j + 1 ) donnée ( F i , F i + 1 ) et ( F j , F j + 1 ). En écrivantf n
pour ( F i , F i + 1 ), nous avonsp (f i) (f j)
=f (i + j)
.Ensuite,
(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)
,et on branche
f 1
=(1,1)
.la source
Mathematica, 15
34octetsFibonacci
lui-même prend ~ 6s sur mon ordinateur. Et 95 (+/- 5) s pour le frontend pour l'afficher.Les 1000 premiers chiffres (34 octets):
⌊Fibonacci@1*^9/1*^208986640⌋&
Plus long mais plus rapide
ToString@Fibonacci@1*^9~StringTake~1000&
:la source
div
). J'ai arrêté parce que les gens auraient probablement fini de se pencher sur cette question au moment où j'avais une fonction bien jouée au golf qui faisait tout ce travail. Mais apparemment, la force brute peut fonctionner, comme le montrent certaines réponses.Python 2, 70 octets
Cela a fonctionné en 18 minutes et 31 secondes sur mon ordinateur portable, produisant les 1000 chiffres corrects suivis de
74100118580
(les chiffres suivants corrects sont74248787892
).la source
div
boucle pour faire 9 chiffres décimaux par morceau. Effectuer les ajouts avec cmp / cmov et 2xADD au lieu de ADC.Haskell , 78 octets
Essayez-le en ligne!
A pris 48 secondes sur TIO. Même formule récursive que ma réponse Python , mais sans tronquer.
La constante
2143923439
est10**9-1
inversée en binaire et avec un extra à la fin. Itérer entre ses chiffres binaires en sens inverse simule l’itération entre les chiffres binaires de10**9-1
. Il semble plus court de coder ceci que de le calculer.la source
Haskell ,
202184174173170 170168164162 octetsEssayez-le en ligne!
Explication
Cela utilise un moyen assez rapide pour calculer les nombres de fibonacci. La fonction
l
prend deux nombres de Fibonacci et calcule les nombres de Fibonacci 10 plus tard, alors quef
prend la n ième et n + 1 ième nombres de Fibonacci et calcule la 2n + 20 e et 2n + 21 ième nombres de Fibonacci. Je les enchaîne plutôt au hasard pour obtenir 1 milliard et saisir les 1000 premiers chiffres.la source
Haskell, 81 octets
Explication
f n
calcule récursivement len
nombre th de fibonacci en utilisant la récurrence de la réponse de xnor avec élimination de la sous-expression commune. Contrairement aux autres solutions publiées, qui utilisent des multiplications O (log (n)), nous avons une récursion O (log (n)) - profondeur avec un facteur de ramification de 2, pour une complexité de multiplications O (n).Cependant, tout n'est pas perdu! Comme presque tous les appels se trouveront au bas de l’arbre de récursivité, nous pouvons utiliser l’arithmétique native rapide dans la mesure du possible et éviter de nombreuses manipulations d’énormes bignums. Il crache une réponse en quelques minutes sur ma boîte.
la source
T-SQL,
422 414453 octets (vérifié, en compétition maintenant!)EDIT 2 : changé en , gagné quelques octets mais vitesse accrue pour compléter à 1 milliard! Terminé en 45 heures 29 minutes , vérifie par rapport à la chaîne donnée et affiche 8 caractères supplémentaires (qui peuvent ou non être corrects en raison d'erreurs d'arrondi).
INT BIGINT
DECIMAL(37,0)
T-SQL n'a pas de support natif pour les "nombres énormes", il a donc fallu lancer mon propre additionneur de nombres énormes à base de texte en utilisant des chaînes de 1008 caractères:
Voici la version formatée avec des commentaires:
En gros, je manipule manuellement des chaînes remplies de zéros de 1008 caractères représentant mes deux variables de Fibonacci,
@a
et@
.Je les additionne
8 1836 chiffres à la fois, en enlevant les 36 derniers chiffres, en les convertissant en un type numérique gérable (DECIMAL(37,0)
), en les additionnant, puis en les écrasant dans une autre longue chaîne@c
. Je «tourne» ensuite@a
,@
en déplaçant les 36 derniers chiffres vers l'avant et en répétant le processus. 28 rotations * 36 chiffres couvrent la totalité des 1008. Je dois "porter celui-ci" manuellement.Une fois que notre nombre commence à dépasser la longueur de ma chaîne, je "décale à gauche" et nous commençons à perdre de la précision, mais l'erreur se situe bien dans mes caractères supplémentaires.
J'ai essayé d'utiliser une table SQL contenant des INT et des BIGINT, avec une logique similaire, et elle était considérablement plus lente. Bizarre.
la source
PARI / GP, 45 octets
D'une certaine manière
\p1000
n'est pas suffisant. Cela ne fonctionne pas avec les systèmes 32 bits. La division finale consiste à éviter le point décimal de la notation scientifique.la source
Pari / GP , 15 + 5 = 20 octets
Exécuter avec l'option de ligne de commande
-s1g
pour allouer 1 Go de mémoire.la source
Ruby, 63 octets
mec, je suis mauvais au golf ruby; mais la classe BigInt fait des merveilles pour ce genre de choses. Nous utilisons le même algorithme que Anders Kaseorg.
la source