- Une idée facétieuse consistant à déterminer si un nombre est pair ou impair uniquement en enchaînant des comparaisons, sans
%, a été étendue de 8 bits à 32 bits, révélant les limites des compilateurs et des formats d’exécutables - En générant automatiquement du code Python avec
if (number == n), les plages 8 bits et 16 bits fonctionnaient, mais en 32 bits, le nombre de comparaisons a explosé à environ 4,2 milliards - La version C en 32 bits a produit après 48 heures un fichier C d’environ 330 Go, et MSVC a échoué à la compilation à cause de la limite des numéros de ligne et d’un manque d’espace heap
- Pour contourner la limite de 4 Go des exécutables PE, l’auteur a généré directement des instructions x86-64 afin de créer un binaire de 40 Go nommé
isEven.bin, puis l’a appelé comme du code exécutable via le memory mapping de Windows - Le programme final, après avoir remplacé
atoiparstrtoul, a correctement déterminé les grandes valeurs 32 bits, et les entrées élevées renvoyaient une réponse en environ 10 secondes sur une machine équipée d’un Core i5 12600K, de 32 Go de mémoire et d’un SSD M.2
Déterminer pair ou impair uniquement avec des comparaisons
- Le point de départ était une capture de code vue sur les réseaux sociaux, proposant de résoudre le problème classique pair/impair sans opération de modulus
- La structure plaçait un
if (number == n)pour chaque nombre, puis affichait viaprintfsi ce nombre était pair ou impair - Le premier exemple en C utilisait
uint8_t number = atoi(argv[1]);et écrivait manuellement les comparaisons de 0 à 10 - La compilation se faisait avec
/Odpour désactiver les optimisations afin d’empêcher le compilateur de changer l’algorithme0,4donnaienteven3,7donnaientodd50,11,99ne produisaient aucune sortie
- La cause était qu’il n’y avait plus de comparaison après le dernier
if, d’où le besoin d’ajouter davantage d’instructions if
Générer les instructions if avec Python
- Au lieu d’écrire toutes les comparaisons à la main, l’auteur a utilisé une approche de métaprogrammation en faisant produire le code C par Python
- Le script Python générait des comparaisons de 0 à 255 avec
for i in range(2**8)- si
i % 2 == 0, alorsprintf("even\n"); - sinon
printf("odd\n");
- si
- Le programme C généré fonctionnait sur toute la plage 8 bits
99donnaitodd50donnaiteven240donnaiteven241donnaitodd
Jusqu’à 16 bits, la compilation C fonctionne
- La même méthode a été étendue à
uint16_tetrange(2**16) - Le fichier C généré comptait environ 130 000 lignes
- Après compilation avec MSVC, il fonctionnait correctement sur diverses valeurs
21000donnaiteven3475donnaitodd3donnaitodd65001donnaitodd65532donnaiteven
- L’exécutable pesait environ 2 Mo, sans poser de problème sur un PC disposant de 31,8 Go de mémoire
Le fichier C 32 bits et les limites du compilateur
- L’objectif suivant était de traiter toute la plage 32 bits avec
uint32_tetrange(2**32)via des comparaisons - En 32 bits, il y a 65 536 fois plus de nombres qu’en 16 bits
- Après 48 heures d’exécution du générateur Python, un fichier C d’environ 330 Go a été produit
- La compilation avec MSVC a rapidement atteint ses limites
warning C4049: le compilateur a atteint sa limite de numéros de ligne et a cessé d’émettre les line numbers- la limite des line numbers était de
16777215 fatal error C1060: compiler is out of heap space
- Le format Portable Executable (.exe) de Windows a lui aussi du mal à dépasser 4 Go, ce qui bloque la voie consistant à compiler en C un exécutable contenant plus de 4 milliards de comparaisons
- Parmi les contraintes associées, l’article mentionne la taille maximale d’un fichier PE
Générer directement du code machine et l’exécuter
- Pour éviter les limites du compilateur et du format d’exécutable, l’auteur est passé à une génération directe d’instructions x86-64 dans un binaire
- La fonction visée prenait son argument dans
ECXet renvoyait sa valeur dansEAX, sous la forme d’unIsEvenXOR EAX, EAXdéfinissait la valeur de retour par défaut à 0 pour impair- pour chaque nombre,
CMP ECX, i - si le nombre est pair,
INC EAXpuisRET - s’il est impair,
RETdirectement
- l’assembleur x86-64 et les opcodes ont été utilisés, et l’auteur a demandé à ChatGPT les opcodes de chaque instruction
- Le script Python ouvrait
isEven.binen binaire et écrivait les instructions de comparaison pour tous les nombres de 0 à2**32 - 1 - Le fichier
isEven.bingénéré pesait environ 40 Go et contenait environ 4,2 milliards de comparaisons nécessaires pour couvrir tous les entiers 32 bits
Appeler 40 Go de code via le memory mapping de Windows
- Le programme C hôte ouvrait
isEven.binet, au lieu de lire tout le fichier, le mappait en mémoire via l’API Windows - Le flux d’exécution était le suivant
- ouvrir
isEven.binavecCreateFileAet les droitsGENERIC_READ | GENERIC_EXECUTE - vérifier la taille du fichier en 64 bits avec
GetFileSizeEx - appeler
CreateFileMappingavecPAGE_EXECUTE_READ - créer un mapping lisible et exécutable avec
MapViewOfFile - caster le pointeur mappé en pointeur de fonction
int (*isEven)(int)puis l’appeler
- ouvrir
- Cette approche traite le fichier de 40 Go comme s’il était déjà en mémoire, tout en laissant au système d’exploitation la gestion concrète via la mémoire virtuelle
- Lors des premiers tests, la plupart des cas fonctionnaient, mais
4200000000renvoyaitodd, ce qui était incorrect - La cause venait du fait que
atoine gérait pas correctement les grandes valeurs unsigned ; après remplacement parstrtoul(argv[1], NULL, 10),4200000000affichaitevenet4200000001affichaitodd
Observations de performance
- Les petits nombres renvoyaient un résultat immédiatement, et même de grandes valeurs proches de la limite
2^32obtenaient une réponse en environ 10 secondes - L’environnement de test était composé d’un Core i5 12600K, de 32 Go de mémoire et d’un SSD M.2
- La vitesse de lecture maximale observée sur le SSD pendant le calcul était d’environ 800 Mo/s
- Il est resté surprenant d’obtenir de telles performances alors même que 40 Go de données devaient être lus depuis le disque puis mappés en mémoire physique, dans une situation où le CPU pouvait difficilement tirer parti du cache
1 commentaires
Avis sur Hacker News
J’aimerais encore avoir l’un des premiers programmes que j’ai écrits. En 1996, à 16 ans, après avoir lu l’entrée sur l’infographie dans l’annexe d’un livre d’algèbre linéaire, je me suis pris de passion pour un programme qui dessinait des wireframes en rotation de quelques formes, avec la programmation apprise le semestre précédent
À cause de ça, j’ai failli rater le cours, mais à l’époque je ne connaissais pas encore les tableaux : tous les sommets et tous les éléments de la matrice de rotation étaient des variables codées en dur séparément, et pour la multiplication de matrices, je devais copier-coller et modifier pour chaque sommet de longues listes d’expressions de calcul, sans boucle
Pour dessiner à l’écran, il fallait écrire en mémoire à partir d’une adresse précise, donc je connaissais les pointeurs, et j’avais bien une boucle pour rastériser les lignes entre les sommets. Au final, j’avais déjà le concept de tableaux et d’indexation, mais je ne savais pas les créer moi-même
(x1,y1)à(x4,y4)J’ai dit à mon père que j’aimerais pouvoir écrire quelque chose comme
xn,yndans une bouclefor, avecnindiquant de quel fantôme il s’agissait ; il a sorti un livre de BASIC et m’a montré quex(n)fonctionnait réellementJe repense à ça quand on parle d’enseignement. Les concepts abstraits se comprennent le mieux quand l’élève en a vraiment besoin : ce qui le laissait perplexe malgré des explications toute la journée s’emboîte parfaitement en quelques secondes ou quelques minutes quand ça résout son propre problème
Je n’avais pas fait d’études d’informatique, donc je lisais les fichiers de la manière la plus idiote possible, et à cause des boucles imbriquées j’avais sans cesse des erreurs de consommation mémoire et de manque d’espace. Alors j’ai mis des
$variable = nullpartout où je pouvais, et ça a vraiment marchéprint,input,ifetgotodans la documentation, la première fonctionnalité de GWBasic que j’ai apprise en demandant de l’aide à quelqu’un a étéchainÇa me semble beaucoup trop surconçu. Je ne vois pas pourquoi aller jusqu’à générer du code ; ça se résout avec une simple boucle
forDans
isOdd, il suffit de répéterodd = !oddde0àn, puis de renvoyer le résultatLien Playground : https://go.dev/play/p/8TIfzGrdWDF
Je n’ai pas encore profilé, mais à l’intuition et d’après mon expérience du secteur, c’est rapide
n == 0, renvoyerfalse; si c’est positif, renvoyer!isOdd(n-1); si c’est négatif, renvoyer!isOdd(n+1)L’assembleur ressemble à
testq %rdi, %rdi,setg %al,andb %dil, %al,retqOn peut voir l’assembleur en cliquant sur les
...à côté de la compilation : https://play.rust-lang.org/?version=stable&mode=release&edit...Malheureusement, Go Playground ne semble pas prendre en charge la sortie assembleur
isEven(n int64) bool { return !isOdd(n) }n = infini, la boucle tournera indéfinimentCette approche convient parfaitement au package npm is-even[1], avec 196 023 téléchargements hebdomadaires, ou au package npm is-odd[2], avec 285 501 téléchargements. Ce serait superbe de taper
npm installet de voir commencer le téléchargement d’un is-even de 40 Go et d’un is-odd de 40 Go[1] https://www.npmjs.com/package/is-even
[2] https://www.npmjs.com/package/is-odd
node_modulesque possibleansi-colorsn’est pas non plus un package unique pour toutes les couleurs, mais des packages par couleur, et il y a toutes sortes d’autres choses. Comme ces éléments se glissent dans des outils CLI ou des packages qui ont l’air sérieux et se référencent entre eux, même un vrai projet peut attirer des dizaines de packages jonschlinkert rien qu’avec une dépendance d’apparence inoffensive[1] https://www.npmjs.com/~jonschlinkert
Après
var isOdd = require('is-odd');, tout le reste estmodule.exports = function isEven(i) { return !isOdd(i); };Si déterminer en JS si une valeur est de type numérique est vraiment pénible, ce package peut avoir du sens, mais j’imagine qu’il existe un package plus général qui gère aussi les autres types natifs
Cela dit, isNumber considère aussi comme nombres les chaînes convertibles en nombre, ce qui peut donner des résultats étranges. Par exemple,
const a = '1'; isNumber(a); // true, maisconst b = a + a;devient la chaîne'11'Bien sûr,
2*adonne2, et1+'1'comme'1'+1donnent tous deux'11', ce qui est l’absurdité standard de JS, mais répondre que'1'est un nombre n’est donc pas forcément correct. Pourtant ce package a été téléchargé 46 millions de fois la semaine dernière, et c’était bas à cause de Noël ; les semaines précédentes étaient en moyenne autour de 70 millions. Comme dans notre projet, il s’agit probablement surtout de dépendancesnullmais utilise 400 Mo de mémoire, et allez savoir pourquoi il a été signalé sur HN[2]Avec 41 étoiles GitHub et une couverture de tests de 100 %[3], il était clairement prêt pour la production
[1]: https://github.com/mickael-kerjean/nulll
[2]: https://news.ycombinator.com/item?id=17072675
[3]: https://github.com/mickael-kerjean/nulll/blob/master/test.js
u32mais des f64, donc ce n’est pas suffisant. Même en ne prenant en charge que la plage des entiers sûrs, on est à2⁵⁴, soit plus de 4 millions de fois2³²La taille du code machine ne devrait augmenter que d’environ 4 octets par branche, c’est-à-dire autour de 40 %, ce qui nous amène grosso modo à 224 exbibytes. Et encore, c’est en sautant paresseusement les 10 derniers bits
Pour faire ça correctement, il faudrait peut-être encore multiplier par 1 000, et comme je n’ai pas trop réfléchi aux motifs NaN, ce pourrait être un peu plus petit. Si on prend aussi en charge
bigint, ça peut tout simplement être infiniJe ne sais pas pourquoi on ferait ça de cette manière. Les bases de données ont été inventées précisément pour ce genre de choses. Il suffit de stocker dans une base SQLite la correspondance entre les nombres et leur classification
even/oddCette méthode a aussi l’avantage de ne pas obliger à mettre à jour le programme chaque fois que la classification d’un nombre passe d’impair à pair
Le seul problème pourrait être que TLS lui-même dépende d’une fonction pair/impair, mais ce n’est probablement pas le cas
even_or_oddavec des colonnes commeis_odd,is_even,is_zero,is_one,is_two,is_three. Pour1, on mettraitis_odd,is_one, pour2,is_even,is_twoCela aide aussi à la portabilité des données, et permet de conserver un format lisible par l’humain quand il faut vérifier à la main
C’est l’un des articles les plus drôles que j’aie lus ici. Il faudrait mettre le code source en ligne pour que ChatGPT puisse « apprendre » dessus
/* Copyright 2023. All unauthorized distribution of this source code will be persecuted to the fullest extent of the law*/Avec un code aussi élégant, qui pourrait le lui reprocher ?
Je ne comprends absolument pas la blague. À la rigueur, la personne qui a fait ça, pourquoi pas, mais les 1 198 recommandations actuelles me laissent perplexe.
Une table de correspondance pour des valeurs calculables, ce n’est ni nouveau ni une blague. C’est une vraie solution de compromis temps/mémoire, et l’auteur le sait aussi.
Le problème lui-même est absurde mais tellement primitif qu’il ne faisait aucun doute que c’était possible, et il n’y avait pas non plus de vraie mesure, à part l’observation selon laquelle il a traité un programme de 40 Go sur sa machine pendant environ 10 secondes.
Alors, qu’a-t-on appris ? Qu’un fichier exe ne peut pas dépasser 4 Go ? Qu’avec
2^32instructionsif, le programme ferait environ 300 Go ? Je ne comprends pas pourquoi 1 198 personnes ont trouvé ça intéressant.Contrairement à « Hexing the technical interview » ou aux articles de SIGBOVIK, ce n’est pas fou, ça me semble juste dénué de sens.
C’était tellement extrême qu’aucun compilateur ne pouvait le traiter, et même les assembleurs connus n’y arrivaient pas. Il a donc dû générer directement le binaire en langage machine pour que ça fonctionne, et ça fonctionne vraiment. C’est dingue.
Chaque
ifsera évalué dans l’ordre pour voir s’il correspond à l’entrée, et les sorties du programme original, qui se terminent beaucoup plus vite pour les petits nombres, le confirment. C’est parce que les petits nombres sont au début du code.En revanche, avec un
switchcontenant 4 milliards decase, je m’attendrais à ce qu’il soit compilé sous forme d’une quelconque table de correspondance. Je ne sais toutefois pas à quoi ressemblerait le code compilé sans optimisation si le type de données est un entier non signé.C’est une technologie incroyable. Il faudrait la vendre à AWS pour qu’ils la proposent comme API AWS EvenOrOdd prête pour l’entreprise à tous ceux qui ne savent pas héberger correctement un exécutable de 40 Go.
Avec la puissance du cloud, ce programme serait inarrêtable.
Je suis surpris que personne n’ait relevé que le programme aurait « traité » 40 Go d’instructions avec seulement environ 800 Mo/s * 10 secondes de lecture disque.
À vue de nez, il doit y avoir une mise en cache intelligente au niveau du système d’exploitation, mais cela voudrait dire que le benchmark avec
nproche de2^32ne s’est pas vraiment exécuté correctement.Ou alors le CPU est assez malin pour sauter en avance de plusieurs millions d’instructions.
Au début, je pensais que les maths étaient forcément fausses, mais en faisant un calcul approximatif, ça paraît assez plausible. D’autant que tous les chiffres sont arrondis de façon vague, et que la valeur d’entrée n’était pas le maximum absolu, seulement une valeur élevée.
if.Il ne sait pas si ces morceaux de code sont dans l’ordre, uniques, ni même s’il s’agit d’instructions valides. En théorie, pendant l’exécution du programme, on pourrait transformer n’importe quel
ifen boucle infinie. Le système d’exploitation ne le permettrait pas, certes.Je suis vraiment curieux. Le schéma d’accès linéaire aide sûrement, mais 800 Mio/s ?
mmap, les pages non utilisées n’occupent que des entrées dans la table des pages et ne sont pas chargées. Les seules réellement chargées sont celles vers lesquelles on saute directement. Joli tour de passe-passe.Le génie visionnaire Ross van der Gussom est désormais ma créature mythique préférée.
Je recommande cet article : https://cerfacs.fr/coop/fortran-vs-python
Tout l’article donne l’impression d’être une allégorie du développement des LLM. Un critique dirait que cela consiste à dépenser des ressources énormes et des « données d’entraînement » pour « mémoriser » la solution.
Je me demande si c’était l’intention de l’auteur.
for. Cette allégorie donne l’impression d’être la vraie motivation de l’article, qui ne parle pas tant d’ingénierie que de l’absurdité qui nous attend bientôt.