Comment comparer équitablement la vitesse d'exécution de C#, C++, Java et Go

· Mis à jour le: · · Benchmark, Performances, C#, C++, Java, Go

« C++, c’est censé être rapide. » « Go est léger en production. » « Java devient vraiment rapide sur les longues durées. » « C# aussi est étonnamment costaud, grâce au JIT de .NET. »

On entend ce genre de discours tout le temps. Mais la pire chose à faire ici est de prendre des chiffres mesurés par des personnes différentes, dans des environnements différents, de les aligner côte à côte et d’en tirer un verdict définitif sur la supériorité d’un langage.

C# et Java sont fortement influencés par le JIT et le warm-up, tandis que C++ et Go sont normalement compilés à l’avance. La présence et les caractéristiques du GC diffèrent également. Les différences d’implémentation de la bibliothèque standard et des bibliothèques de l’écosystème comptent aussi beaucoup. Et même sur une même machine, les résultats fluctuent facilement selon les réglages d’alimentation, la chaleur, l’activité en arrière-plan et les biais des données d’entrée. C’est un monde plutôt terre à terre.

Dans cet article, nous détaillons comment mesurer C# / C++ / Java / Go de la manière la plus équitable possible. Pour donner tout de suite la conclusion : le plus important est de ne pas essayer de trancher « quel langage est le plus rapide » avec un seul chiffre.

Le sujet principal de cet article est, à proprement parler, comment structurer la comparaison. Aligner des chiffres dépendants de l’environnement qui n’ont l’air que plausibles vire à la voyance ; nous ne publierons donc pas ici de classement mesuré. Nous nous concentrons plutôt sur comment concevoir la comparaison pour qu’elle ait réellement de la valeur.

D’abord la conclusion

Dans une comparaison de vitesse entre C# / C++ / Java / Go, voici les sept points qui comptent vraiment.

  1. Décider d’abord quelle vitesse on veut comparer Que ce soit le temps de démarrage, le throughput en régime stable, la latence p95 ou l’efficacité mémoire, la manière de mesurer change.

  2. Ne jamais conclure à partir d’un seul benchmark Le calcul CPU, l’allocation mémoire, le traitement parallèle et le temps de démarrage font chacun apparaître des langages et runtimes différents comme les plus forts.

  3. Séparer cold et warm pour C# et Java Mélanger une comparaison incluant la première exécution avec une comparaison en régime stable après warm-up fausse toute la discussion.

  4. Mesurer avec le même algorithme, la même entrée et la même vérification de correction « Ce n’était pas une implémentation plus rapide, c’était juste un problème différent » est un grand classique des erreurs de benchmark.

  5. Séparer les microbenchmarks internes à un langage des benchmarks end-to-end transversaux Le harnais dédié de chaque langage est pratique, mais la comparaison entre langages est plus rigoureuse lorsqu’elle est pilotée par un lanceur commun externe.

  6. Regarder la médiane et la distribution, pas seulement la moyenne Une seule pause GC ou un seul traitement d’arrière-plan qui tombe sur une exécution suffit à casser la moyenne.

  7. Conserver les conditions, pas seulement les chiffres Un résultat de benchmark est autant un enregistrement des conditions expérimentales qu’un enregistrement de vitesse. Un résultat sans conditions documentées devient très pénible par la suite.

Ce qu’il faut décider en premier

Si l’on se contente du mot « rapide » tout court, ça tourne généralement mal. Commencez par décider ce que vous appelez rapide.

Même pour un seul et même programme, ce que l’on veut observer peut varier considérablement.

1. Voulez-vous observer le temps de démarrage ?

Pour les outils en ligne de commande, les batchs de courte durée et les outils auxiliaires qui démarrent une fois et se terminent aussitôt, le cold start et le démarrage du processus comptent. Sur cet axe, les résultats changent radicalement selon que l’on inclut ou non le coût d’initialisation du JIT et du chargement des classes.

2. Voulez-vous observer le throughput en fonctionnement prolongé ?

Pour les serveurs, les processus résidents, les workers et les traitements de conversion de longue durée, c’est le throughput en régime stable (steady-state) qui compte. Dans ce cas, être lent uniquement sur la première exécution n’est pas l’important ; la vraie question est la stabilité et le niveau atteint après le warm-up.

3. Voulez-vous observer la tail latency ?

Pour les API, les UI et les traitements proches du temps réel, le p95 / p99 peut compter plus que la moyenne. Même si la moyenne est rapide, des blocages occasionnels et importants sont pénibles du point de vue de l’expérience utilisateur et du SLA.

4. Voulez-vous inclure l’efficacité mémoire ?

Se limiter au temps CPU sans regarder le RSS maximal, le volume d’allocation, le nombre de GC et les pauses GC conduit à mal juger le poids réel en exploitation. « Rapide mais gourmand en mémoire » et « un peu plus lent mais stable et léger » peuvent s’inverser selon l’usage.

En résumé, la question à trancher en premier est

Ce que cette comparaison doit répondre, ce n’est pas quel langage est rapide, mais quel workload, dans quelles conditions, sur quel indicateur, peut être traité le plus vite.

Si l’on commence à collecter des chiffres tant que ce point reste flou, rien ne tiendra debout à la fin.

Pourquoi comparer des langages est difficile

Mélanger JIT et AOT revient à faire une expérience différente

C# et Java sont normalement soumis à l’influence du JIT. C++ et Go, eux, sont normalement compilés à l’avance.

Cela signifie que mesurer la première exécution revient à mesurer non seulement la vitesse du programme lui-même, mais aussi le démarrage du runtime, le chargement des classes et la préparation du JIT. À l’inverse, si l’on ne regarde que des exécutions pleinement chauffées, la comparaison devient jusqu’où va l’optimisation en régime stable.

Les deux ont un sens. Mais ils ne veulent pas dire la même chose.

Les différences d’implémentation dépassent souvent les différences de langage

Même pour un même « tri »,

  • l’un utilise la bibliothèque standard
  • l’autre est une implémentation maison
  • l’un fait des copies superflues
  • l’un régénère l’entrée à chaque fois

Cela seul change considérablement les résultats.

Par ailleurs, dès qu’on aborde le JSON, la compression, la cryptographie ou les expressions régulières, les différences d’implémentation de bibliothèque pèsent bien plus que le langage lui-même. Sans expliciter ce que l’on mesure, ce qui devait être une « comparaison de langages » devient une « comparaison de bibliothèques ».

C++ a le piège où l’optimisation fait disparaître le traitement

En particulier dans les microbenchmarks, si le compilateur décide que « ce résultat de calcul, personne ne l’utilise », il peut carrément supprimer le traitement. On se retrouve alors non pas devant du code rapide, mais devant du code qui ne fait tout simplement rien, ce qui donne une petite histoire de fantômes.

Ce problème a tendance à apparaître de façon particulièrement flagrante en C++ ; utiliser le résultat, imprimer un checksum, ou s’appuyer sur les fonctionnalités anti-optimisation du framework de benchmark est donc très important.

Le GC n’est ni un « désavantage » ni un « avantage », c’est une caractéristique

C#, Java et Go ont un GC. Réduire cela à « il y a un GC, donc c’est lent » est bien trop grossier.

En pratique, ce qui compte davantage, c’est

  • la façon de gérer un grand nombre d’objets de courte durée de vie
  • le paramétrage de la taille du tas
  • la fréquence et les pauses du GC
  • la disposition des objets en mémoire
  • les habitudes d’allocation des bibliothèques

À l’inverse, C++ permet un contrôle fin via la gestion manuelle et le RAII, mais cela veut aussi dire que les différences de conception et d’implémentation se voient davantage. Autrement dit, une différence de stratégie de gestion mémoire n’est pas, en soi, un jugement de valeur.

Ce qu’il ne faut pas faire dans une comparaison

1. Mélanger Debug et Release

C’est hors de question. Alignez toujours les cibles de comparaison sur des builds optimisés, représentatifs de la production.

2. Ne pas résoudre le même problème

Format d’entrée différent, sortie différente, gestion d’erreurs présente seulement d’un côté, politique de réutilisation mémoire différente. Laisser cela tel quel revient à mesurer des différences d’exigences, pas de vitesse.

3. Exécuter une seule fois et en tirer une conclusion

Une seule exécution, c’est surtout du bruit.

  • JIT
  • cache de pages
  • boost CPU
  • chaleur
  • tâches d’arrière-plan
  • GC
  • première lecture de fichier

Tout cela se mélange en une seule exécution.

4. Brouiller le warm-up

Lors de la mesure de C# et Java, rester flou sur le fait d’inclure ou non la première exécution, ou de ne regarder que l’après-warm-up, fait s’effondrer la discussion. Traitez cold et warm comme deux choses distinctes.

5. Ne pas vérifier la correction

Avant d’être « rapide », un benchmark doit d’abord « renvoyer le même résultat ». Vérifiez systématiquement que toutes les implémentations comparées produisent le même checksum ou la même sortie à partir de la même entrée.

6. Bâtir toute une vision du monde sur un seul microbenchmark

Gagner sur une boucle serrée (tight loop) ne signifie pas gagner sur un service réel dans son ensemble. À l’inverse, perdre sur le temps de démarrage n’empêche pas d’être largement assez fort sur les traitements de longue durée.

L’approche de base pour comparer C# / C++ / Java / Go

Ce point est particulièrement important. Nous recommandons une structure à deux couches.

1. Pour la mesure à l’intérieur d’un langage, utiliser le harnais adapté à ce langage

Chaque langage dispose d’outils de benchmark qui absorbent ses particularités propres.

  • C# : BenchmarkDotNet
  • Java : JMH
  • Go : go test -bench et benchstat
  • C++ : Google Benchmark

Ces outils prennent en charge, dans une mesure raisonnable, les particularités du runtime, le traitement statistique et les pièges classiques de la mesure. Ils sont très efficaces pour la comparaison au sein d’un langage et pour creuser une implémentation.

2. Pour la comparaison entre langages, placer un lanceur commun à l’extérieur

En revanche, aligner directement les résultats de BenchmarkDotNet en C# et les résultats de JMH en Java est un peu dangereux. Les harnais eux-mêmes suivent des conventions différentes.

C’est pourquoi, pour un travail transversal aux langages, nous recommandons de transformer chaque implémentation en un exécutable appelable avec le même contrat de ligne de commande, puis de tous les piloter de l’extérieur dans des conditions identiques.

Par exemple, préparez dans chaque langage un exécutable de cette forme.

bench --scenario sort_int32 --dataset data/sort_10m.bin --mode warm
bench --scenario group_words --dataset data/words_100mb.txt --mode cold
bench --scenario parallel_hash --dataset data/blob_1gb.bin --threads 8

Puis, côté lanceur commun,

  • randomiser l’ordre d’exécution
  • séparer cold / warm
  • transmettre le même jeu de données
  • vérifier le checksum
  • relever le temps wall-clock et la mémoire
  • conserver les données brutes en CSV / JSON

Cela permet de traiter beaucoup plus facilement, comme des sujets séparés, les bonnes pratiques propres à chaque langage et l’équité entre langages.

Exemple concret : quels scénarios de benchmark prévoir

Quand on demande de comparer C# / C++ / Java / Go, notre recommandation est : si vous ne pouvez en faire qu’un seul, choisissez un scénario CPU simple, difficile à mal interpréter ; si vous pouvez en faire plusieurs, prévoyez 3 à 4 workloads de nature différente.

Sélection recommandée

1. sort_int32_10m

Objectif : observer le CPU + la bande passante mémoire + l’usage de l’espace temporaire

  • Entrée : 10 millions de valeurs int32 générées avec une seed fixe
  • Traitement : trier le tableau et renvoyer un checksum
  • Point d’attention : restaurer la même entrée non triée à chaque itération

Ce scénario est relativement facile à comprendre. Cependant, il inclut les différences d’implémentation du tri standard : c’est donc une comparaison incluant la bibliothèque standard, plutôt qu’une comparaison du langage lui-même.

2. hash_group_count

Objectif : observer les tables de hachage, le traitement de chaînes, les allocations et les tendances du GC

  • Entrée : un jeu de données texte fixe
  • Traitement : compter les occurrences de chaque mot
  • Sortie : les N premiers résultats plus un checksum

C’est proche d’un traitement réel, mais les différences d’implémentation des bibliothèques de chaînes et des map comptent aussi beaucoup. En contrepartie, c’est une comparaison plus proche de la réalité.

3. parallel_sha256

Objectif : observer le parallélisme, l’ordonnanceur, les pools de workers et les habitudes de synchronisation

  • Entrée : une séquence de blocs binaires de taille fixe
  • Traitement : les hacher sur N threads et renvoyer un checksum final
  • Conditions : faire varier le nombre de threads par paliers, par exemple 1 / 2 / 4 / 8

Comparé à une simple boucle serrée, cela rend beaucoup plus visible la façon dont ça scale en exécution parallèle.

4. startup_noop ou startup_parse_small

Objectif : observer le temps de démarrage

  • noop : démarrer et se terminer immédiatement
  • parse_small : traiter une seule petite entrée puis se terminer

Ici, le coût du JIT et de l’initialisation de C# / Java se voit facilement, et le tableau diffère pas mal de C++ / Go. Inversement, même si un écart apparaît ici, c’est une question distincte de qui gagne sur les traitements de longue durée.

Qu’en est-il des benchmarks JSON ou HTTP ?

Le JSON et le HTTP sont proches d’un usage réel, ils ont donc bien sûr du sens. Mais dans ce cas, il s’agit plutôt d’une comparaison incluant les bibliothèques, les frameworks et l’écosystème, plus qu’une comparaison de langages.

Ce n’est pas mauvais en soi. En pratique, c’est même souvent la question la plus importante. Mais dans un article ou un rapport, il vaut mieux préciser explicitement :

Ceci n’est pas une comparaison de langages, mais une comparaison d’implémentations types incluant les principales bibliothèques.

pour limiter les malentendus.

Conditions à aligner selon le langage

C++

  • Aligner sur des builds optimisés
  • Fixer le compilateur
  • Fixer l’implémentation de la bibliothèque standard
  • Documenter les conditions telles que -O3 / /O2, LTO, PGO
  • Faire attention à ce que le résultat ne soit pas supprimé par l’optimisation
  • Se méfier d’un comportement indéfini qui ferait paraître le code plus rapide qu’il ne l’est

C++ offre beaucoup de liberté, ce qui fait que les différences de conditions se répercutent directement dans les résultats. C’est pourquoi il est très important de préciser avec quel compilateur, quels flags, et quelle STL la mesure a été faite.

C#

  • Aligner sur des builds Release
  • Fixer la version de .NET
  • Enregistrer les conditions telles que Server GC / Workstation GC
  • Documenter la présence de Tiered Compilation, ReadyToRun, Native AOT
  • Séparer cold et warm

Pour C#, les différences de configuration de .NET changent la façon dont les résultats se présentent. En particulier, le C# compilé en JIT et le C# en Native AOT sont deux axes différents, bien qu’il s’agisse tous deux de « C# ». Si on les mélange, ce qu’on compare n’est plus le langage mais le mode de distribution.

Java

  • Fixer le vendeur et la version du JDK
  • Documenter le GC
  • Fixer le warm-up / la mesure / le nombre de forks
  • Enregistrer la taille du tas et les options de la JVM
  • Séparer le cold start du steady-state

Java profite facilement du JIT, mais en contrepartie, l’apparence de la première exécution varie beaucoup. C’est pourquoi séparer la comparaison des processus de courte durée de la comparaison en fonctionnement prolongé est indispensable.

Go

  • Fixer la version de Go
  • Fixer GOMAXPROCS
  • Documenter CGO_ENABLED
  • Si l’on touche à GOGC, toujours l’enregistrer
  • Conserver, si possible, une sortie au format benchmark

Go est relativement facile à manipuler, mais dans les benchmarks parallèles, l’impact de GOMAXPROCS est important. De plus, utiliser ou non cgo change complètement la donne, donc pensez à toujours le documenter parmi les conditions.

Comment aligner l’environnement d’exécution

Quel que soit le langage, une comparaison sans environnement aligné revient surtout à comparer des environnements.

Ce qu’il faut aligner

  • Même CPU / mémoire / stockage
  • Même version d’OS
  • Mêmes conditions d’alimentation
  • Conditions proches de la même température ambiante
  • Mêmes données d’entrée
  • Même priorité de processus
  • Mêmes conditions de nombre de cœurs
  • Mêmes conditions conteneur ou bare-metal

Ce qui pèse particulièrement

Réglages d’alimentation et fréquence CPU

Sur un ordinateur portable, le simple fait d’être sur secteur ou sur batterie change complètement le monde. Si le CPU governor ou le mode d’alimentation ne sont pas alignés, les résultats de comparaison fluctuent considérablement.

Pour la façon d’aligner les conditions d’alimentation, les notifications, le bruit d’arrière-plan, la chaleur et l’ordre d’exécution sous Windows, nous détaillons tout cela dans un autre article, Comment comparer la vitesse d’exécution de différentes versions d’un programme sous Windows. Si vous mesurez sous Windows, ce point compte énormément.

La chaleur

Si seules les premières exécutions sont rapides et que le reste ralentit, soupçonnez la chaleur et le throttling. Plutôt que d’exécuter tout A puis tout B, alterner comme A / B / A / B réduit le biais.

L’activité en arrière-plan

Mises à jour, indexation, synchronisation, antivirus, navigateur, outils de discussion. Ce sont des détails peu glorieux, mais ils interfèrent régulièrement.

Que mesurer

Pour une comparaison de langages, nous recommandons de regarder séparément au moins ces quatre points.

1. Le temps wall-clock

C’est le temps réel que l’utilisateur attend. C’est le premier indicateur à regarder.

2. Le temps CPU

C’est « combien de CPU a réellement été consommé ». Si seul le temps wall-clock est plus rapide alors que le temps CPU reste inchangé, l’écart vient peut-être de l’attente ou des E/S.

3. Mémoire / allocations

  • RSS maximal
  • volume total d’allocation
  • nombre d’allocations
  • nombre de GC
  • pauses GC

Regarder cela révèle le coût caché derrière la vitesse.

4. La distribution

  • médiane
  • p95 / p99
  • min / max
  • écart-type et dispersion

Si l’on ne parle qu’en moyennes, on ne voit jamais la vraie nature des exécutions qui déraillent occasionnellement.

Procédure d’exécution recommandée

Le déroulé qui fonctionne bien en pratique suit à peu près cet ordre.

1. Décider du workload

D’abord, expliciter ce que l’on veut comparer.

  • temps de démarrage
  • throughput en régime stable
  • tail latency
  • efficacité mémoire
  • scalabilité parallèle

2. Fixer un jeu de données commun

Aligner les données d’entrée avec une seed fixe ou un fichier fixe. Si la génération de données fait partie de la mesure, elle doit elle aussi tourner dans les mêmes conditions pour chaque langage.

3. Faire passer les vérifications de correction en premier

Vérifier que toutes les implémentations renvoient le même résultat, sur de petites données comme sur de grandes. Leur faire émettre un checksum ou un hash facilite grandement cette vérification.

4. Fixer les conditions de build

Produire, pour chaque langage, des exécutables Release / optimisés, et enregistrer les versions et les flags.

5. Séparer cold et warm

C’est particulièrement important pour C# et Java.

  • cold : inclut le moment juste après le démarrage du processus
  • warm : l’état stable obtenu après plusieurs exécutions

Ces deux éléments sont plus propres lorsqu’ils ne sont pas mélangés dans le même tableau.

6. Alterner ou randomiser l’ordre d’exécution

Exemple :

cpp -> csharp -> java -> go
go -> java -> cpp -> csharp
csharp -> go -> java -> cpp
...

Cela réduit le biais dû à la chaleur et au bruit.

7. S’assurer d’un nombre d’itérations suffisant

Pour un microbenchmark léger, visez un nombre d’itérations élevé ; pour de l’end-to-end, il en faut au moins 10. Quand l’écart est faible et le nombre d’itérations bas, l’interprétation devient très hasardeuse.

8. Sauvegarder les données brutes

Conservez les données brutes de chaque exécution, pas seulement les agrégats. En les relisant plus tard, on peut y lire les valeurs aberrantes et les particularités du warm-up.

9. Profiler quand un écart apparaît

C’est seulement quand un écart apparaît que l’on commence à en creuser la cause.

  • profil CPU
  • profil d’allocation
  • logs GC
  • flame graph
  • traces côté OS

Arrivé à ce stade, on peut discuter de pourquoi cela se produit, et pas seulement de « rapide / lent ».

Comment lire les résultats

Même une fois les chiffres obtenus, mal les lire reste dangereux.

C# / Java sont lents seulement sur la première exécution

Soupçonnez le JIT, le chargement des classes, l’initialisation. Dans ce cas,

  • si le temps de démarrage compte, c’est un écart significatif
  • si le fonctionnement prolongé est le sujet principal, c’est un écart à mettre dans un tableau séparé

C++ est fort sur une boucle serrée (tight loop)

L’optimisation bas niveau, la disposition des objets et un surcoût minimal du runtime peuvent y contribuer. Mais se contenter de regarder cela et en conclure « donc c’est le plus rapide en service réel aussi » est un raccourci abusif.

Go paraît favorisé sur le temps de démarrage et la facilité de distribution

Le binaire unique, un démarrage relativement léger et un modèle de concurrence accessible peuvent y contribuer. Mais cela ne signifie pas qu’il soit avantagé sur tous les workloads CPU.

C# / Java rattrapent nettement, voire dépassent, en régime stable

L’optimisation du JIT peut y contribuer. Ce n’est pas une histoire rare non plus. C’est pourquoi il est important de ne pas mélanger la comparaison incluant le démarrage et la comparaison en régime stable.

Un écart important sur un traitement gourmand en allocations

Dans ce cas, plus que le nom du langage, ce qui compte souvent le plus, c’est

  • la disposition mémoire
  • la gestion des chaînes et des map
  • le comportement du GC
  • les copies superflues

Modèle d’enregistrement

Conserver au minimum ces champs pour chaque résultat de benchmark vous sera utile plus tard.

timestamp,language,scenario,run_kind,cold_or_warm,elapsed_ms,cpu_ms,max_rss_mb,alloc_bytes,gc_count,checksum
compiler_or_runtime,compiler_version,flags,os,cpu,threads,input_id,notes

Par exemple, run_kind peut se répartir ainsi.

  • micro
  • macro
  • startup
  • parallel

Pour cold_or_warm, il faut absolument préciser lequel des deux c’est.

  • cold
  • warm

Pour un benchmark, être interprétable plus tard compte souvent davantage que le fait de mesurer.

Résumé

Ce qui compte vraiment dans une comparaison de vitesse entre C# / C++ / Java / Go, c’est de prendre la question grossière « quel langage est le plus rapide » et de la transformer en une expérience de la forme « quel workload, dans quelles conditions, sur quel indicateur, comparer ».

Les points particulièrement difficiles à rater sont les suivants.

  • Séparer le temps de démarrage du régime stable
  • Mesurer avec le même algorithme, la même entrée, la même vérification de correction
  • Ne jamais conclure à partir d’un seul benchmark
  • Séparer le benchmark interne à un langage du benchmark transversal aux langages
  • Regarder la médiane et la distribution plutôt que la moyenne
  • Conserver les conditions et les données brutes

Et enfin, le plus important : ne pas trop chercher à trancher la victoire ou la défaite par le seul nom du langage. La performance réelle est déterminée par la combinaison du langage, du runtime, des bibliothèques, des conditions de build, des données, de l’OS et du matériel.

« C++ est rapide », « Java est costaud », « Go est léger », « C# aussi est bien assez rapide » — dans un certain sens, tout cela est vrai. Mais dès que l’on perd de vue dans quelles conditions on le dit, cela vire généralement à un pugilat dans le brouillard.

Aligner les conditions, utiliser plusieurs workloads, séparer cold / warm, et regarder jusqu’à la distribution. C’est peu glorieux, mais au final, c’est ce qui l’emporte.

Références

  • BenchmarkDotNet Getting Started https://benchmarkdotnet.org/articles/guides/getting-started.html

  • OpenJDK JMH Project https://openjdk.org/projects/code-tools/jmh/

  • JMH GitHub Repository / README https://github.com/openjdk/jmh

  • Go testing package https://pkg.go.dev/testing

  • Go benchstat https://pkg.go.dev/golang.org/x/perf/cmd/benchstat

  • Google Benchmark User Guide https://google.github.io/benchmark/user_guide.html

  • Comment comparer la vitesse d’exécution de différentes versions d’un programme sous Windows https://comcomponent.com/fr/blog/2026/03/16/002-windows-benchmark-comparing-program-versions/

Sujets connexes

Des pages plus faciles à comprendre en les lisant avec cet article.

Où en discuter

Concevoir des comparaisons de performances, aligner les conditions de mesure, interpréter les résultats et creuser les causes racines sont des sujets qui se marient bien avec les services suivants.

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Analyse des bugs et des causes

Isoler la cause des écarts de performance entre langages ou versions, localiser les goulots d'étranglement, et vérifier la validité de la procédure de mesure relèvent naturellement de l'enquête de bogue et de l'analyse des causes racines.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Lequel de C#, C++, Java et Go est le plus rapide ?
Un seul chiffre ne permet pas de trancher, car la performance réelle dépend d'une combinaison de langage, de runtime, de bibliothèques, de conditions de build, de données, d'OS et de matériel. L'essentiel est de transformer la question « quel langage est le plus rapide » en une expérience de la forme « quel workload, dans quelles conditions, avec quel indicateur, comparer ». Sur le temps de démarrage, le coût du JIT et de l'initialisation de C# et Java se voit facilement, mais en régime stable (steady-state), l'optimisation du JIT rattrape souvent une bonne partie de l'écart, voire l'inverse.
Pourquoi sépare-t-on le cold et le warm dans les benchmarks C# ou Java ?
C# et Java subissent normalement l'influence du JIT : mesurer la toute première exécution revient à mesurer non seulement la vitesse du programme lui-même, mais aussi le démarrage du runtime, le chargement des classes et la préparation du JIT. C++ et Go, à l'inverse, sont normalement compilés à l'avance. Le cold et le warm ont tous deux un sens, mais pas le même sens : il vaut donc mieux traiter séparément le cold, qui inclut le tout début du processus, et le warm, l'état stable obtenu après plusieurs exécutions, sans les mélanger dans un même tableau.
Comment concevoir un benchmark qui traverse plusieurs langages ?
Une structure à deux couches est recommandée. Pour la mesure à l'intérieur d'un langage, on utilise le harnais adapté à ce langage : BenchmarkDotNet (C#), JMH (Java), `go test -bench` et `benchstat` (Go), Google Benchmark (C++). Pour la comparaison entre langages, aligner directement les résultats de ces différents harnais est risqué, car chacun a ses propres conventions. Il est donc préférable de transformer chaque implémentation en un exécutable appelable via le même contrat de ligne de commande, puis de le piloter depuis un lanceur commun externe qui randomise l'ordre d'exécution, sépare cold et warm, utilise le même jeu de données, vérifie le checksum et conserve les données brutes.
À quoi faut-il faire attention dans un microbenchmark C++ ?
Il faut se méfier du piège où l'optimisation supprime purement et simplement le traitement. Si le compilateur décide que « personne n'utilise ce résultat de calcul », il peut éliminer le calcul lui-même : le code n'est alors pas rapide, il ne fait tout simplement plus rien. C'est pourquoi il est important d'utiliser le résultat, d'imprimer un checksum, ou de s'appuyer sur les fonctionnalités anti-optimisation du framework de benchmark. Par ailleurs, comme les différences de conditions se répercutent fortement en C++, il est essentiel de documenter précisément avec quel compilateur, quels flags (`-O3`/`O2`, LTO, PGO, etc.) et quelle STL la mesure a été faite.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog