Quatre petits programmes, écrits une fois par langage et vérifiés comme imprimant les mêmes octets, chronométrés sur la même machine au repos — avec les contrôles qui disent lesquelles des différences sont réelles.
Ils ont été choisis pour quatre coûts différents. Le résultat le plus utile de cette page est qu'ils ne s'accordent sur aucun classement.
| Programme | Taille | Ce qu'il sollicite |
|---|---|---|
fib(35) | 9,2 M d'appels récursifs | Coût d'appel et arithmétique entière. Rien n'est alloué, donc la stratégie mémoire d'un langage ne peut ni l'aider ni le pénaliser. |
loops | 100 M d'itérations | Une boucle while, un modulo, une addition. Le corps se compile en la même poignée d'instructions dans tout langage qui compile. |
objects | 10 M d'allocations | Un record à deux champs construit et détruit à chaque itération. C'est là que comptage de références, ramasse-miettes et allocation sur la pile donnent des réponses différentes. |
lists | 1 M d'éléments à travers map / filter / fold | Chaque langage utilise son propre type de liste et ses propres fonctions d'ordre supérieur. C'est le moins comparable des quatre, et la colonne où cela compte le plus. |
Chaque chiffre est le minimum des exécutions moins le temps de démarrage propre à ce runtime, pour que l'amorçage d'une JVM ne soit pas imputé à son arithmétique. Le minimum plutôt que la médiane : une machine perturbée produit une longue queue à droite et aucune à gauche.
| Langage | fib(35) | loops | objects | lists | démarrage |
|---|---|---|---|---|---|
| C | 9.1 | 43.1 | 10.1 | 3.9 | 0.2 |
| C++ | 9.3 | 43.8 | 10.2 | 5.8 | 0.3 |
| Rust | 16.1 | 40.5 | 9.3 | 3.5 | 0.4 |
| Keal | 20.4 | 44.2 | 10.0 | 13.4 | 0.3 |
| Go | 22.7 | 46.2 | 11.1 | 30.9 | 0.8 |
| Java | 15.3 | 43.9 | 14.2 | 97.3 | 14.9 |
| Kotlin | 13.1 | 42.6 | 13.7 | 78.7 | 23.2 |
| Python | 449.0 | 4 684 | 1 465 | 94.8 | 6.2 |
fib(35) — 9,2 M d'appels récursifs
loops — 100 M d'itérations
objects — 10 M d'allocations
lists — 1 M d'éléments à travers map / filter / fold
De combien la médiane dépasse le minimum, en pourcentage. Sous 10% environ, un chiffre est acquis ; bien au-dessus, deux langages séparés de quelques pour cent ne sont pas départagés par ces données, et la page ne prétend pas le contraire.
| Langage | fib(35) | loops | objects | lists |
|---|---|---|---|---|
| C | 9% | 7% | 8% | 19% |
| C++ | 6% | 7% | 7% | 13% |
| Rust | 7% | 8% | 5% | 17% |
| Keal | 7% | 3% | 7% | 4% |
| Go | 3% | 5% | 5% | 89% |
| Java | 6% | 9% | 6% | 11% |
| Kotlin | 11% | 10% | 12% | 8% |
| Python | 8% | 3% | 5% | 7% |
| Langage | Version | Compilation |
|---|---|---|
| C | gcc (Ubuntu 15.2.0-16ubuntu1) 15.2.0 | -O2 -std=c11 |
| C++ | g++ (Ubuntu 15.2.0-16ubuntu1) 15.2.0 | -O2 -std=c++17 |
| Rust | rustc 1.98.0 (88d9e12ae 2026-08-18) | -C opt-level=2 |
| Keal | keal 1.2.0 | keal build |
| Go | go version go1.25.1 linux/arm64 | go build |
| Java | openjdk version "25.0.4" 2026-07-21 | javac, default JVM |
| Kotlin | kotlinc-jvm 2.4.10 (JRE 25.0.4+7-1-26.04-Ubuntu); jars run on openjdk version "25.0.4" 2026-07-21 | -include-runtime, jars run on the PATH java |
| Python | CPython 3.14.4 | stock build |
Le JDK et l'interpréteur ci-dessus ont été mis en place pour cette mesure, ils ne sont pas ceux de la machine. Les deux installations sont keg-only : le java par défaut de ce Mac est en 23 et son python3 par défaut est le 3.9.6 d'Apple. Reproduire avec les valeurs par défaut de la machine donne d'autres chiffres — à lui seul, CPython 3.9 exécute fib 2,2 fois plus lentement que 3.14 ici, soit davantage que tout écart entre les machines sur cette ligne.
Chaque chiffre est le minimum des exécutions moins le temps de démarrage propre à ce runtime, pour que l'amorçage d'une JVM ne soit pas imputé à son arithmétique. Le minimum plutôt que la médiane : une machine perturbée produit une longue queue à droite et aucune à gauche.
| Langage | fib(35) | loops | objects | lists | démarrage |
|---|---|---|---|---|---|
| C | 16.4 | 32.5 | 6.0 | 2.5 | 1.8 |
| C++ | 16.4 | 32.8 | 6.0 | 2.9 | 2.0 |
| Rust | 16.2 | 33.4 | 9.1 | 2.9 | 1.9 |
| Keal | 20.9 | 45.4 | 9.5 | 10.4 | 1.7 |
| Go | 20.4 | 46.0 | 9.7 | 7.3 | 2.2 |
| Java | 14.2 | 42.7 | 12.5 | 48.7 | 18.9 |
| Kotlin | 17.0 | 41.8 | 13.0 | 41.5 | 26.3 |
| Python | 605.3 | 5 860 | 1 688 | 106.6 | 15.6 |
fib(35) — 9,2 M d'appels récursifs
loops — 100 M d'itérations
objects — 10 M d'allocations
lists — 1 M d'éléments à travers map / filter / fold
De combien la médiane dépasse le minimum, en pourcentage. Sous 10% environ, un chiffre est acquis ; bien au-dessus, deux langages séparés de quelques pour cent ne sont pas départagés par ces données, et la page ne prétend pas le contraire.
| Langage | fib(35) | loops | objects | lists |
|---|---|---|---|---|
| C | 8% | 8% | 13% | 12% |
| C++ | 7% | 11% | 5% | 7% |
| Rust | 7% | 11% | 6% | 16% |
| Keal | 9% | 2% | 5% | 10% |
| Go | 8% | 5% | 12% | 27% |
| Java | 21% | 8% | 8% | 10% |
| Kotlin | 10% | 14% | 7% | 12% |
| Python | 8% | 8% | 11% | 21% |
| Langage | Version | Compilation |
|---|---|---|
| C | Apple clang version 21.0.0 (clang-2100.1.1.101) | -O2 -std=c11 |
| C++ | Apple clang version 21.0.0 (clang-2100.1.1.101) | -O2 -std=c++17 |
| Rust | rustc 1.98.0 (88d9e12ae 2026-08-18) | -C opt-level=2 |
| Keal | keal 1.2.0 | keal build |
| Go | go version go1.27.0 darwin/arm64 | go build |
| Java | openjdk version "25.0.4.1" 2026-08-18 | javac, default JVM |
| Kotlin | kotlinc-jvm 2.4.10 (JRE 25.0.4.1); jars run on openjdk version "25.0.4.1" 2026-08-18 | -include-runtime, jars run on the PATH java |
| Python | CPython 3.14.7 | stock build |
Toutes les chaînes d'outils ci-dessus ont été installées pour cette mesure : la machine n'en avait aucune, pas même un Python fonctionnel. Rust est sur l'ABI GNU et non MSVC, ce que ses options indiquent — le C est ici MinGW, et un Rust MSVC aurait introduit un second runtime C dans la même colonne de rapports. keal build emprunte ce même gcc.
Chaque chiffre est le minimum des exécutions moins le temps de démarrage propre à ce runtime, pour que l'amorçage d'une JVM ne soit pas imputé à son arithmétique. Le minimum plutôt que la médiane : une machine perturbée produit une longue queue à droite et aucune à gauche.
| Langage | fib(35) | loops | objects | lists | démarrage |
|---|---|---|---|---|---|
| C | 11.1 | 92.0 | 19.3 | 7.7 | 4.9 |
| C++ | 11.4 | 91.7 | 19.6 | 10.8 | 5.0 |
| Rust | 21.6 | 66.7 | 20.3 | 6.6 | 5.9 |
| Keal | 45.3 | 108.2 | 18.4 | 17.6 | 6.2 |
| Go | 41.4 | 83.5 | 19.6 | 23.6 | 6.9 |
| Java | 26.5 | 108.8 | 32.4 | 101.7 | 46.9 |
| Kotlin | 24.7 | 108.0 | 33.1 | 85.6 | 63.4 |
| Python | 1 130 | 9 278 | 2 896 | 235.4 | 21.4 |
fib(35) — 9,2 M d'appels récursifs
loops — 100 M d'itérations
objects — 10 M d'allocations
lists — 1 M d'éléments à travers map / filter / fold
De combien la médiane dépasse le minimum, en pourcentage. Sous 10% environ, un chiffre est acquis ; bien au-dessus, deux langages séparés de quelques pour cent ne sont pas départagés par ces données, et la page ne prétend pas le contraire.
| Langage | fib(35) | loops | objects | lists |
|---|---|---|---|---|
| C | 6% | 1% | 3% | 9% |
| C++ | 5% | 1% | 4% | 5% |
| Rust | 1% | 2% | 4% | 7% |
| Keal | 1% | 1% | 3% | 4% |
| Go | 2% | 2% | 4% | 19% |
| Java | 5% | 3% | 4% | 5% |
| Kotlin | 5% | 1% | 2% | 3% |
| Python | 1% | 3% | 4% | 2% |
| Langage | Version | Compilation |
|---|---|---|
| C | gcc.EXE (MinGW-W64 x86_64-ucrt-posix-seh, built by Brecht Sanders, r1) 16.2.0 | -O2 -std=c11 |
| C++ | g++.EXE (MinGW-W64 x86_64-ucrt-posix-seh, built by Brecht Sanders, r1) 16.2.0 | -O2 -std=c++17 |
| Rust | rustc 1.98.1 (48a229cea 2026-09-01) | -C opt-level=2, x86_64-pc-windows-gnu |
| Keal | keal 1.2.0 | keal build (gcc, MinGW-w64 UCRT) |
| Go | go version go1.27.1 windows/amd64 | go build |
| Java | openjdk version "25.0.4.1" 2026-08-18 LTS | javac, default JVM |
| Kotlin | kotlinc-jvm 2.4.10 (JRE 25.0.4.1+1-LTS) | -include-runtime, default JVM |
| Python | CPython 3.12.10 | stock build |
Là où deux machines ne s'accordent pas sur un rapport, le désaccord est le résultat — le chiffre était une propriété d'une machine et non du langage. Lisez le dénominateur d'abord : plusieurs lignes qui bougent ensemble et dans le même sens signent un changement de la référence C, et non cinq langages changeant à la fois.
fib(35)
| Langage | Linux · aarch64 | macOS · Apple M4 | Windows · x86_64 |
|---|---|---|---|
| C | 1.0× | 1.0× | 1.0× |
| C++ | 1.0× | 1.0× | 1.0× |
| Rust | 1.8× | 1.0× | 1.9× |
| Keal | 2.2× | 1.3× | 4.1× |
| Go | 2.5× | 1.2× | 3.7× |
| Java | 1.7× | 0.9× | 2.4× |
| Kotlin | 1.4× | 1.0× | 2.2× |
| Python | 49× | 37× | 102× |
loops
| Langage | Linux · aarch64 | macOS · Apple M4 | Windows · x86_64 |
|---|---|---|---|
| C | 1.0× | 1.0× | 1.0× |
| C++ | 1.0× | 1.0× | 1.0× |
| Rust | 0.9× | 1.0× | 0.7× |
| Keal | 1.0× | 1.4× | 1.2× |
| Go | 1.1× | 1.4× | 0.9× |
| Java | 1.0× | 1.3× | 1.2× |
| Kotlin | 1.0× | 1.3× | 1.2× |
| Python | 109× | 180× | 101× |
objects
| Langage | Linux · aarch64 | macOS · Apple M4 | Windows · x86_64 |
|---|---|---|---|
| C | 1.0× | 1.0× | 1.0× |
| C++ | 1.0× | 1.0× | 1.0× |
| Rust | 0.9× | 1.5× | 1.1× |
| Keal | 1.0× | 1.6× | 1.0× |
| Go | 1.1× | 1.6× | 1.0× |
| Java | 1.4× | 2.1× | 1.7× |
| Kotlin | 1.4× | 2.2× | 1.7× |
| Python | 145× | 281× | 150× |
lists
| Langage | Linux · aarch64 | macOS · Apple M4 | Windows · x86_64 |
|---|---|---|---|
| C | 1.0× | 1.0× | 1.0× |
| C++ | 1.5× | 1.2× | 1.4× |
| Rust | 0.9× | 1.2× | 0.9× |
| Keal | 3.4× | 4.2× | 2.3× |
| Go | 7.9× | 2.9× | 3.1× |
| Java | 25× | 19× | 13× |
| Kotlin | 20× | 17× | 11× |
| Python | 24× | 43× | 31× |
Un banc d'essai est un instrument, et un instrument que personne n'a étalonné rapporte ses propres défauts comme des résultats. Chacun de ces contrôles pouvait revenir négatif.
Chaque implémentation est exécutée et sa dernière ligne comparée à la référence Keal avant tout chronométrage. Un programme plus rapide qui imprime un autre nombre n'est pas un programme plus rapide.
Dix fois le travail doit coûter dix fois le temps. Vérifié en reconstruisant à ×10 et en rechronométrant plutôt qu'en lisant l'assembleur — bien que la branche arrière de la boucle y ait aussi été confirmée. Dix fois le travail est revenu à 9,0–9,6 fois le temps.
L'ensemble est mesuré deux fois : une fois groupé, chaque réplique d'une configuration d'affilée, une fois entrelacé dans un ordre tiré au sort. L'écart entre les deux plans est ensuite confronté à la dispersion propre à cette configuration à l'intérieur d'un seul plan. 1 sur 96 l'ont franchi — et avec un seuil aussi grossier appliqué 32 fois par machine, un franchissement marginal ou deux est ce que le hasard seul produit, si bien que le compte est rapporté plutôt qu'interprété.
Corrélation de rang entre le moment d'une exécution dans la séquence entrelacée et sa durée. Une machine qui chauffe, ou un cache qui se remplit, apparaîtrait comme un signe constant d'une configuration à l'autre. La médiane vaut 0,19, bien en deçà du seuil que neuf répliques rendent significatif.
La première exécution d'un binaire le lit depuis le disque. Chaque configuration est exécutée une fois et jetée avant la mesure, pour qu'aucun langage ne paie le fait d'être passé en premier.
Le tableau de dispersion n'est pas décoratif. Là où la médiane d'une configuration dépasse largement son minimum, sa position face à un voisin proche n'est pas tranchée par ces données, et la page le dit au lieu de désigner un gagnant.
lists n'est pas comparable à l'identique. Le List<Int> de Keal, le vector de C++ et le Vec de Rust contiennent des entiers 64 bits non boxés ; Java et Kotlin boxent chaque élément dans un Long. Cette différence explique l'essentiel de ce que la JVM paie dans cette colonne, et c'est une propriété des bibliothèques plutôt que des compilateurs.
Chaque mesure est un processus entier, exécuté une fois. Pas de boucle de chauffe dans le programme, pas de mesure en régime établi. C'est ce que coûte l'exécution du programme, ce qui avantage les compilateurs anticipés et sous-estime ce qu'un JIT fait dans un serveur de longue durée.
Quatre programmes sont quatre programmes. Ils ont été choisis pour quatre coûts différents, mais aucun ensemble aussi petit ne prédit une charge réelle, et aucun ne touche aux entrées-sorties, aux chaînes ni à la concurrence.
Le rapport à C emporte le compilateur C avec lui. Un rapport divise par ce qu'a produit le compilateur C local : il absorbe la vitesse du matériel, pas les décisions d'optimisation de la référence elle-même. fib est l'endroit où cela se voit. Sur la machine Linux, gcc en -O2 déroule la récursion sur plusieurs niveaux — 244 instructions dans le corps de la fonction contre 30 sans inlining — et la même source, le même compilateur et le même drapeau s'exécutent alors en 11,8 ms contre 20,3 ms. Ce facteur 1,7 est de l'ordre de l'écart entre les références C des machines sur ce programme. objects est la seconde colonne dans ce cas, et elle pointe en sens inverse : face à la machine sous clang, six lignes sur sept montent ensemble d'environ 1,6× parce que c'est là que la référence C est la rapide. Entre les deux machines sous gcc, cette même colonne reste dans le peloton — c'est ce qui dit que l'effet appartient au compilateur et non au matériel. Ces deux rapports se comparent donc à l'intérieur d'une machine et non d'un compilateur C à l'autre, et toute ligne qui bouge d'une machine à l'autre doit être lue contre son dénominateur avant d'être lue comme un fait sur le langage.
La soustraction du démarrage pèse le plus là où il y en a le plus. Chaque chiffre est diminué du temps de hello-world de sa propre machine, ce qui garde l'amorçage d'une JVM hors de son arithmétique. Mais cet amorçage se compte en dizaines de millisecondes là où celui d'un binaire natif tient sous deux : les lignes JVM portent donc de loin la plus grosse correction — et elle bouge. Sur la machine macOS, aligner le JDK a déplacé le démarrage de Java de 8 ms, soit 8 ms sur ses quatre chiffres et un dixième de son lists. Comparer une ligne JVM d'une machine à l'autre, c'est comparer deux corrections de démarrage autant que deux runtimes.
Les machines ne partagent pas leur chaîne d'outils. Un rapport à C absorbe la vitesse du matériel. Il n'absorbe ni un gcc différent, ni un Go différent, ni une JVM différente, et chaque machine rapporte ce qu'elle avait. Là où le runtime est lui-même l'objet de la mesure — les lignes Java et Kotlin — les machines s'alignent sur une version majeure de JDK, faute de quoi un écart entre deux machines confondrait le système d'exploitation et la machine virtuelle. Partout ailleurs la version est déclarée plutôt qu'imposée, et le tableau des chaînes d'outils sous chaque machine est cette divulgation.
Un rapport est une propriété d'une machine tant qu'une seconde ne l'a pas confirmé. La première machine ici n'a pas reproduit un rapport que ce projet avait publié, d'un facteur deux, pour exactement cette raison. Tant que le tableau ci-dessous n'a qu'une colonne, lisez chaque chiffre comme mesuré et non comme vrai.
Les programmes sont dans bench/ports/ et le harnais qui les chronomètre est bench/ports/run.py. Il construit ce pour quoi il trouve une chaîne d'outils, nomme ce qu'il saute, et imprime une entrée prête à être ajoutée à la liste des machines. Chaque langage utilise des entiers signés 64 bits et, quand il a le choix, le niveau d'optimisation que keal build passe lui-même à cc.