« 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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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 -benchetbenchstat - 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
int32gé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édiatementparse_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.
micromacrostartupparallel
Pour cold_or_warm, il faut absolument préciser lequel des deux c’est.
coldwarm
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
testingpackage https://pkg.go.dev/testing -
Go
benchstathttps://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 associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Compatibilité descendante des interfaces DLL et COM — Tableau de décision : quels changements cassent les appelants
Quels changements apportés à une DLL ou à un composant COM cassent réellement leurs appelants ? Nous détaillons les trois niveaux de comp...
Checklist pour gérer les processus enfants en toute sécurité dans une application Windows
Pour gérer en toute sécurité les processus enfants dans une application Windows, l'important n'est pas l'API de lancement mais la concept...
Pièges de la mémoire partagée et bonnes pratiques concrètes
Nous passons en revue les pièges de la mémoire partagée en usage réel, ainsi que la conception qui réduit le taux d'incidents : synchroni...
Appeler une DLL C# Native AOT depuis C/C++
Nous faisons le point sur la publication d'une bibliothèque de classes C# en DLL native avec Native AOT, et sur l'appel depuis C/C++ des ...
Versionner le schéma de base de données d'une application métier — pratiques de migration pour éviter que « chaque client ait une base différente »
Guide pratique pour versionner le schéma de base de données d'applications métier dont les bases sont dispersées chez chaque client. Impl...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Conseil technique et revue de conception
Concevoir des comparaisons de performances, aligner les conditions de mesure, et savoir lire correctement le warm-up et les statistiques sont des sujets qui se marient bien avec le conseil technique et la revue de conception.
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.
Liens publics