Qu'est-ce qui confirme l'affirmation selon laquelle C ++ peut être plus rapide qu'une machine virtuelle Java ou CLR avec JIT? [fermé]

119

Un thème récurrent sur SE, j'ai remarqué dans de nombreuses questions est l'argument en cours selon lequel le C ++ est plus rapide et / ou plus efficace que les langages de niveau supérieur comme Java. Le contre-argument est que les machines JVM ou CLR modernes peuvent être tout aussi efficaces, grâce à JIT, etc., pour un nombre croissant de tâches, et que C ++ est encore plus efficace si vous savez ce que vous faites et pourquoi vous faites les choses d'une certaine manière. méritera des augmentations de performance. C'est évident et cela a du sens.

J'aimerais connaître une explication de base (s'il existe une telle chose ...) quant à savoir pourquoi et comment certaines tâches sont plus rapides en C ++ que la JVM ou le CLR? Est-ce simplement parce que C ++ est compilé en code machine alors que la JVM ou le CLR a toujours la surcharge de traitement de la compilation JIT au moment de l'exécution?

Lorsque j'essaie de rechercher le sujet, tout ce que je trouve, ce sont les mêmes arguments que ceux que j'ai exposés ci-dessus, sans aucune information détaillée permettant de comprendre exactement comment utiliser le C ++ pour le calcul haute performance.

Anonyme
la source
La performance dépend également de la complexité du programme.
Pandu
23
J'ajouterais: "Le C ++ est de plus en plus efficace si vous savez ce que vous faites et pourquoi vous ferez les choses d'une certaine manière qui mériteront des performances accrues." en disant que ce n'est pas seulement une question de connaissance, c'est une question de temps de développement. Ce n'est pas toujours efficace d'optimiser l'optimisation. C'est la raison pour laquelle des langages de niveau supérieur tels que Java et Python existent (entre autres raisons) - afin de réduire le temps qu'un programmeur doit consacrer à la programmation pour accomplir une tâche donnée aux dépens d'une optimisation hautement optimisée.
Joel Cornett
4
@ Joel Cornett: Je suis tout à fait d'accord. Je suis définitivement plus productif en Java qu'en C ++ et je ne considère le C ++ que lorsque j'ai besoin d'écrire du code très rapide. Par contre, j'ai constaté que le code C ++ mal écrit était très lent: le C ++ est moins utile entre les mains de programmeurs non qualifiés.
Giorgio
3
Toute sortie de compilation pouvant être produite par un JIT peut être produite par C ++, mais le code que C ++ peut générer ne doit pas nécessairement être produit par un JIT. Les fonctionnalités et les performances du C ++ constituent donc un sur-ensemble de celles de tout langage de niveau supérieur. CQFD
tylerl
1
@ Approbation Certes, techniquement, mais vous pouvez généralement compter les facteurs d’exécution pouvant affecter les performances d’un programme. Habituellement, sans utiliser plus de deux doigts. Donc, dans le pire des cas, vous envoyez plusieurs fichiers binaires ... sauf qu'il s'avère que vous n'avez même pas besoin de le faire car l'accélération potentielle est négligeable, ce qui explique pourquoi personne ne le dérange jamais.
Tylerl

Réponses:

200

Tout est question de mémoire (pas de JIT). L'avantage de JIT par rapport à C est principalement limité à l'optimisation des appels virtuels ou non virtuels via l'inline, ce que le processeur BTB travaille déjà dur.

Dans les machines modernes, l'accès à la RAM est très lent (par rapport à tout ce que le processeur fait), ce qui signifie que les applications qui utilisent le plus possible les caches (ce qui est plus facile lorsque moins de mémoire est utilisée) peuvent être cent fois plus rapides que celles qui utilisent ne pas De plus, Java utilise plus de mémoire que C ++ et rend plus difficile l'écriture d'applications exploitant pleinement le cache:

  • Il existe une surcharge de mémoire d'au moins 8 octets pour chaque objet et l'utilisation d'objets plutôt que de primitives est requise ou préférée dans de nombreux endroits (notamment les collections standard).
  • Les chaînes sont constituées de deux objets et ont une surcharge de 38 octets.
  • UTF-16 est utilisé en interne, ce qui signifie que chaque caractère ASCII nécessite deux octets au lieu d'un (la machine virtuelle Java Oracle a récemment introduit une optimizaion à éviter pour les chaînes ASCII pures).
  • Il n'y a pas de type de référence d'agrégat (c'est-à-dire des structures) et, à son tour, il n'y a pas de tableaux de types de référence d'agrégats. Un objet Java, ou un tableau d'objets Java, a une très faible localisation en cache L1 / L2 par rapport aux structures C et aux tableaux.
  • Les génériques Java utilisent l’effacement de type, qui a une localisation de cache faible par rapport à l’instanciation de type.
  • L'attribution d'objet est opaque et doit être effectuée séparément pour chaque objet. Il est donc impossible pour une application de disposer délibérément ses données de manière conviviale pour le cache tout en les considérant comme des données structurées.

Quelques autres facteurs liés à la mémoire, mais pas au cache:

  • Il n'y a pas d'allocation de pile, donc toutes les données non primitives avec lesquelles vous travaillez doivent être sur le tas et faire l'objet d'une récupération de place (certains JIT récents effectuent l'allocation de pile en arrière-plan dans certains cas).
  • Comme il n'y a pas de type de référence d'agrégat, il n'y a pas de passage de pile de types de référence d'agrégat. (Pensez à passer efficacement des arguments de vecteur)
  • Le ramassage des ordures peut endommager le contenu du cache L1 / L2, et les pauses d'arrêt du monde de GC altèrent l'interactivité.
  • La conversion entre types de données nécessite toujours une copie. vous ne pouvez pas prendre un pointeur sur un tas d'octets que vous avez obtenus à partir d'une socket et les interpréter comme un float.

Certaines de ces choses sont des compromis (ne pas avoir à faire de gestion de mémoire manuelle vaut la peine de perdre beaucoup de performances pour la plupart des gens), certaines sont probablement le résultat d'essayer de garder Java simple, et certaines sont des erreurs de conception (même si c'est peut-être seulement après coup , à savoir UTF-16 était un codage de longueur fixe lors de la création de Java, ce qui rend la décision de le choisir beaucoup plus compréhensible).

Il convient de noter que bon nombre de ces compromis sont très différents pour Java / JVM et pour C # / CIL. Le .NET CIL a des structures de type référence, une allocation / passage de pile, des tableaux de structures empaquetés et des génériques à instanciation de type.

Michael Borgwardt
la source
37
+1 - dans l'ensemble, c'est une bonne réponse. Cependant, je ne suis pas sûr que la puce "il n'y a pas d'allocation de pile" est tout à fait exacte. Les JIT Java échappent souvent à l'analyse pour permettre l'allocation de pile si possible. Peut-être faut-il dire que le langage Java ne permet pas au programmeur de décider quand un objet est alloué par pile ou par tas. De plus, si un collecteur de déchets de génération (utilisé par toutes les machines virtuelles modernes) est utilisé, "allocation de tas" signifie une chose complètement différente (avec des caractéristiques de performance complètement différentes) que dans un environnement C ++.
Daniel Pryden
5
Je penserais qu'il y a deux autres choses, mais je travaille surtout avec des choses d'un niveau beaucoup plus élevé, alors dites-moi si je me trompe. Vous ne pouvez pas vraiment écrire en C ++ sans développer une conscience plus générale de ce qui se passe réellement dans la mémoire et du fonctionnement du code machine alors que les langages de script ou de machine virtuelle abstiennent tout cela loin de votre attention. Vous avez également un contrôle beaucoup plus fin sur le fonctionnement des choses, alors que dans une VM ou un langage interprété, vous vous fiez à ce que les auteurs de bibliothèques de base ont optimisé pour un scénario trop spécifique.
Erik Reppen
18
+1 J'ajouterais une dernière chose (mais je ne souhaite pas soumettre de nouvelle réponse): l'indexation de tableaux en Java implique toujours la vérification des limites. Avec C et C ++, ce n'est pas le cas.
Riwalk
7
Il est intéressant de noter que l'allocation de segment de mémoire par Java est nettement plus rapide qu'une version naïve avec C ++ (en raison de la mise en pool interne et autres), mais l'allocation de mémoire en C ++ peut être nettement meilleure si vous savez ce que vous faites.
Brendan Long
10
@BrendanLong, true .. mais seulement si la mémoire est propre - une fois qu'une application est en cours d'exécution, l'allocation de mémoire sera ralentie en raison du besoin de GC qui ralentit considérablement les choses car elle doit libérer de la mémoire, exécuter les finaliseurs, puis compact. C'est un compromis qui profite aux points de repère, mais (IMHO) ralentit globalement les applications.
gbjbaanb
67

Est-ce simplement parce que C ++ est compilé en code assembleur / machine alors que Java / C # a toujours la charge de traitement de la compilation JIT au moment de l'exécution?

En partie, mais en général, en supposant un compilateur JIT à la pointe de la technologie absolument fantastique, le code C ++ correct a toujours tendance à mieux fonctionner que le code Java pour deux raisons principales:

1) Les modèles C de fournir de meilleures installations pour écrire du code qui est à la fois générique et efficace . Les modèles fournissent au programmeur C ++ une abstraction très utile qui ne nécessite aucun temps d’exécution. (Les modèles sont essentiellement des méthodes de dactylographie au moment de la compilation.) En revanche, le meilleur avantage des génériques Java est essentiellement les fonctions virtuelles. Les fonctions virtuelles ont toujours une surcharge d'exécution et ne peuvent généralement pas être en ligne.

En général, la plupart des langages, y compris Java, C # et même C, vous font choisir entre efficacité et généralité / abstraction. Les modèles C ++ vous donnent les deux (au prix de délais de compilation plus longs).

2) Le fait que la norme C ++ n’ait pas grand-chose à dire sur la structure binaire d’un programme C ++ compilé donne aux compilateurs C ++ beaucoup plus de marge de manœuvre qu’un compilateur Java, ce qui permet de meilleures optimisations (au prix de difficultés parfois plus grandes pour le débogage. ) En fait, la nature même de la spécification du langage Java impose une baisse de performance dans certains domaines. Par exemple, vous ne pouvez pas avoir un tableau d'objets contigu en Java. Vous ne pouvez avoir qu'un tableau contigu de pointeurs d' objet(références), ce qui signifie qu'itérer sur un tableau en Java implique toujours le coût de l'indirection. La sémantique des valeurs de C ++ permet toutefois d'activer des tableaux contigus. Une autre différence réside dans le fait que C ++ permet d'allouer des objets sur la pile, contrairement à Java. En pratique, puisque la plupart des programmes C ++ ont tendance à allouer des objets sur la pile, le coût de cette allocation est souvent proche de zéro.

Un domaine dans lequel C ++ peut être à la traîne par rapport à Java est celui où de nombreux petits objets doivent être alloués sur le tas. Dans ce cas, le système de récupération de place Java aura probablement de meilleures performances que les systèmes standard newet deleteC ++, car Java GC permet la désallocation en masse. Mais là encore, un programmeur C ++ peut compenser cela en utilisant un pool de mémoire ou un allocateur de brames, alors qu’un programmeur Java n’a aucun recours face à un modèle d’allocation de mémoire pour lequel le runtime Java n’est pas optimisé.

Consultez également cette excellente réponse pour plus d'informations sur ce sujet.

Charles Salvia
la source
6
Bonne réponse, mais un point mineur: "Les modèles C ++ vous donnent les deux (au prix de temps de compilation plus longs.)" J'ajouterais aussi au prix d'une plus grande taille de programme. Cela ne posera peut-être pas toujours un problème, mais si le développement s’applique aux appareils mobiles, c’est tout à fait possible.
Leo
9
@luiscubal: non, à cet égard, les génériques C # sont très semblables à Java (en ce sens que le même chemin de code "générique" est pris quel que soit le type passé.) L'astuce pour les modèles C ++ est que le code est instancié une fois pour toutes tous les types auxquels il est appliqué. Alors , std::vector<int>est un tableau dynamique conçu juste pour ints, et le compilateur est capable d'optimiser en conséquence. AC # List<int>est toujours juste un List.
Jalf
12
@jalf C # List<int>utilise un int[], pas Object[]comme Java. Voir stackoverflow.com/questions/116988/…
luiscubal
5
@luiscubal: votre terminologie n'est pas claire. Le JIT n'agit pas à ce que je considérerais comme du "temps de compilation". Vous avez raison, bien sûr, avec un compilateur JIT suffisamment intelligent et agressif, il n'y a en réalité aucune limite à ce qu'il pourrait faire. Mais C ++ requiert ce comportement. En outre, les modèles C ++ permettent au programmeur de spécifier des spécialisations explicites, permettant ainsi des optimisations explicites supplémentaires, le cas échéant. C # n'a pas d'équivalent pour cela. Par exemple, en C ++, je pourrais définir un vector<N>emplacement où, dans le cas particulier de vector<4>, mon implémentation SIMD codée à la main devrait être utilisée
jalf
5
@Leo: Le gonflement du code à travers les modèles était un problème il y a 15 ans. Avec la lourdeur de la modélisation et l'intégration, ainsi que les compilateurs de capacités choisis depuis (comme le pliage d'instances identiques), de nombreux codes deviennent de plus en plus petits grâce aux modèles.
sbi
46

Ce que les autres réponses (6 à ce jour) semblent avoir oublié de mentionner, mais ce que je considère très important pour répondre à cela, est l’une des philosophies de conception très basiques du C ++, qui a été formulée et utilisée par Stroustrup dès le premier jour:

Vous ne payez pas pour ce que vous n'utilisez pas.

Il y a d'autres principes de conception sous-jacents importants qui ont grandement façonné le C ++ (comme cela, vous ne devriez pas être forcé de passer à un paradigme spécifique), mais vous ne payez pas pour ce que vous n'utilisez pas est parmi les plus importants.


Dans son livre The Design and Evolution of C ++ (généralement appelé [D & E]), Stroustrup décrit le besoin dont il disposait qui l'avait amené à proposer le C ++ en premier lieu. Dans mes propres mots: Pour sa thèse de doctorat (quelque chose à voir avec les simulations de réseau, IIRC), il a implémenté un système dans SIMULA, qu’il aimait beaucoup, car le langage lui permettait d’exprimer ses pensées directement en code. Cependant, le programme qui en a résulté était trop lent et, pour obtenir un diplôme, il l'a réécrit dans BCPL, un prédécesseur de C. L'écriture du code dans BCPL a été qualifiée de pénible, mais le programme a été assez rapide pour être efficace. résultats, ce qui lui a permis de terminer son doctorat.

Après cela, il a voulu un langage qui permette de traduire les problèmes du monde réel en code aussi directement que possible, tout en permettant au code d'être très efficace.
À la suite de cela, il a créé ce qui allait devenir plus tard C ++.


Donc, l’objectif cité ci-dessus n’est pas simplement l’un des principes fondamentaux de la conception sous-jacente, c’est très proche de la raison d’être du C ++. Et on peut le trouver un peu partout dans le langage: les fonctions ne s’appliquent que virtuallorsque vous le souhaitez (car l’appel de fonctions virtuelles entraîne un léger surcoût) Les PODs ne sont initialisés automatiquement que lorsque vous le demandez explicitement, les exceptions ne vous coûtent que des performances jetez-les (alors que c’était un objectif de conception explicite de permettre que la configuration / le nettoyage des empilages soit très bon marché), aucun GC n’exécutant à tout moment, etc.

C ++ a explicitement choisi de ne pas vous donner certaines commodités ("dois-je rendre cette méthode virtuelle ici?") En échange de performances ("non, je ne le fais pas, et maintenant le compilateur peut le inlinefaire et optimiser pleinement le tout cela! ") et, ce qui n’est pas surprenant, cela a effectivement entraîné des gains de performances par rapport aux langues plus pratiques.

sbi
la source
4
Vous ne payez pas pour ce que vous n'utilisez pas. => et puis ils ont ajouté RTTI :(
Matthieu M.
11
@ Matthieu: Bien que je comprenne votre sentiment, je ne peux pas m'empêcher de remarquer que même cela a été ajouté avec soin en ce qui concerne les performances. RTTI est spécifié pour pouvoir être implémenté à l'aide de tables virtuelles, ce qui ajoute très peu de temps système si vous ne l'utilisez pas. Si vous n'utilisez pas le polymorphisme, il n'y a aucun coût. Est-ce que je manque quelque chose?
sbi
9
@ Matthieu: Bien sûr, il y a une raison. Mais cette raison est-elle rationnelle? D'après ce que je peux voir, le "coût de RTTI", s'il n'est pas utilisé, est un pointeur supplémentaire dans la table virtuelle de chaque classe polymorphe, indiquant un objet RTTI alloué de manière statique quelque part. À moins que vous ne vouliez programmer la puce dans mon grille-pain, comment cela pourrait-il être pertinent?
sbi
4
@Aaronaught: Je ne sais pas quoi répondre à cela. Avez-vous simplement écarté ma réponse parce qu'elle souligne la philosophie sous-jacente qui a amené Stroustrup et al. À ajouter des fonctionnalités de manière à permettre la performance, plutôt que de répertorier ces méthodes et fonctionnalités individuellement?
sbi
9
@Aaronaught: Vous avez ma sympathie.
sbi
29

Connaissez-vous le document de recherche Google sur ce sujet?

De la conclusion:

Nous constatons qu'en ce qui concerne les performances, le C ++ l'emporte largement. Cependant, cela nécessitait également les efforts de réglage les plus importants, dont beaucoup étaient effectués à un niveau de sophistication qui ne serait pas disponible pour le programmeur moyen.

Ceci est au moins partiellement une explication, dans le sens de "parce que les compilateurs C ++ du monde réel produisent un code plus rapide que les compilateurs Java par des mesures empiriques".

Doc Brown
la source
4
Outre les différences d’utilisation de la mémoire et du cache, l’un des plus importants est le nombre d’optimisations effectuées. Comparez le nombre d'optimisations effectuées par GCC / LLVM (et probablement Visual C ++ / ICC) par rapport au compilateur Java HotSpot: bien plus encore, en particulier en ce qui concerne les boucles, l'élimination des branches redondantes et l'allocation des registres. Les compilateurs JIT n'ont généralement pas le temps pour ces optimisations agressives, même s'ils pouvaient les implémenter mieux en utilisant les informations d'exécution disponibles.
Gratian Lup
2
@GratianLup: Je me demande si cela est (toujours) vrai avec LTO.
Deduplicator
2
@GratianLup: N'oublions pas l'optimisation guidée par le profil pour C ++ ...
Déduplicateur
23

Ce n’est pas une copie de vos questions, mais la réponse acceptée répond à la plupart de vos questions: Un examen moderne de Java

Pour résumer:

Fondamentalement, la sémantique de Java indique qu'il s'agit d'un langage plus lent que le C ++.

Donc, selon le langage avec lequel vous comparez le C ++, vous pouvez obtenir ou non la même réponse.

En C ++, vous avez:

  • Capacité de faire de la smart inline,
  • Génération de code générique ayant une forte localité (modèles)
  • données aussi petites et compactes que possible
  • possibilités d'éviter les indirections
  • comportement prévisible de la mémoire
  • optimisations du compilateur possibles uniquement en raison de l'utilisation d'abstractions de haut niveau (modèles)

Ce sont les caractéristiques ou les effets secondaires de la définition de langage qui le rendent théoriquement plus efficace en termes de mémoire et de vitesse que tout langage qui:

  • utiliser massivement l'indirection (langages "tout est une référence gérée / pointeur"): indirection signifie que le processeur doit sauter en mémoire pour obtenir les données nécessaires, ce qui augmente le nombre d'échecs de la cache du processeur, ce qui signifie un ralentissement du traitement - C utilise également des indirections a beaucoup même s'il peut avoir de petites données en C ++;
  • générer des objets de grande taille auxquels les membres sont accédés indirectement: ceci est une conséquence des références par défaut, les membres sont des pointeurs; ainsi, lorsque vous obtenez un membre, il est possible que les données ne soient pas proches du noyau de l'objet parent, ce qui provoque à nouveau des erreurs de cache.
  • utilisez un collecteur garbarge: cela rend simplement la prévisibilité des performances impossible (par conception).

La mise en ligne agressive du compilateur C ++ réduit ou élimine de nombreux indirections. La capacité à générer un petit ensemble de données compactes rend le cache convivial si vous ne répartissez pas ces données dans toute la mémoire plutôt que de les regrouper (les deux sont possibles, C ++ vous laisse juste choisir). RAII rend le comportement de la mémoire C ++ prévisible, éliminant ainsi de nombreux problèmes dans le cas de simulations en temps réel ou semi-temps réel, qui nécessitent une vitesse élevée. Les problèmes de localisation, en général, peuvent être résumés de la manière suivante: plus le programme / données est petit, plus son exécution est rapide. Le C ++ offre diverses manières de s’assurer que vos données se trouvent là où vous le souhaitez (dans un pool, un tableau ou autre) et qu’elles sont compactes.

Évidemment, il existe d’autres langages qui peuvent faire la même chose, mais ils sont tout simplement moins populaires parce qu’ils ne fournissent pas autant d’outils d’abstraction que le C ++, ils sont donc moins utiles dans de nombreux cas.

Klaim
la source
7

Il s’agit principalement de mémoire (comme l’a dit Michael Borgwardt) avec un peu d’inefficacité JIT ajoutée.

Une chose non mentionnée est le cache - pour utiliser pleinement le cache, vous avez besoin que vos données soient disposées de manière contiguë (c'est-à-dire toutes ensemble). Désormais, avec un système CPG, la mémoire est allouée sur le segment GC, ce qui est rapide, mais au fur et à mesure de l'utilisation de la mémoire, le CPG démarrera régulièrement, supprimera les blocs qui ne sont plus utilisés et compactera ensuite les éléments restants. Outre la lenteur évidente à rapprocher les blocs utilisés, cela signifie que les données que vous utilisez peuvent ne pas être collées ensemble. Si vous avez un tableau de 1000 éléments, à moins que vous ne les ayez tous alloués en une fois (puis mis à jour leur contenu plutôt que d'en supprimer et d'en créer de nouveaux - qui seront créés à la fin du tas), ceux-ci seront dispersés dans tout le tas, nécessitant ainsi plusieurs hits de mémoire pour tous les lire dans le cache du CPU. L'application AC / C ++ allouera probablement la mémoire pour ces éléments, puis vous mettrez à jour les blocs avec les données. (ok, il existe des structures de données comme une liste qui se comportent plus comme les allocations de mémoire du GC, mais les gens savent qu'elles sont plus lentes que les vecteurs).

Vous pouvez le constater dans les opérations en remplaçant simplement les objets StringBuilder par String ... Stringbuilders fonctionne en pré-allouant de la mémoire et en la remplissant. C'est un truc de performance connu des systèmes java / .NET.

N'oubliez pas que le paradigme «supprimer les anciennes et allouer de nouvelles copies» est très utilisé en Java / C #, simplement parce que les utilisateurs savent que les allocations de mémoire sont très rapides grâce au GC, et que le modèle de mémoire dispersée est utilisé partout ( bien sûr, donc toutes vos bibliothèques gaspillent de la mémoire et en utilisent beaucoup, aucune d’entre elles n’ayant l’avantage de la contiguïté. Blâmez le battage publicitaire autour de GC pour cela - ils vous ont dit que la mémoire était libre, lol.

Le GC lui-même est évidemment un autre grand succès: lorsqu’il est lancé, il doit non seulement balayer le tas, mais également libérer tous les blocs inutilisés, puis exécuter tous les finaliseurs (même si cela se faisait séparément). la prochaine fois que l'application sera arrêtée) (je ne sais pas si c'est toujours un tel succès, mais tous les documents que j'ai lus disent d'utiliser uniquement des finaliseurs si vraiment nécessaire), puis il doit déplacer ces blocs en position de sorte que le tas soit compacté et mettre à jour la référence au nouvel emplacement du bloc. Comme vous pouvez le constater, cela demande beaucoup de travail!

Les résultats parfaits pour la mémoire C ++ se résument à des allocations de mémoire - lorsque vous avez besoin d'un nouveau bloc, vous devez parcourir le tas à la recherche du prochain espace libre suffisamment grand, avec un tas très fragmenté, ce n'est pas aussi rapide qu'un GC. 'allouez simplement un autre bloc à la fin' mais je pense que ce n'est pas aussi lent que tout le travail effectué par le compactage du GC, et qu'il peut être atténué en utilisant plusieurs tas de blocs de taille fixe (également appelés pools de mémoire).

Il y a plus encore ... comme charger des assemblages hors du GAC qui nécessite une vérification de sécurité, des chemins de sonde (allumez sxstrace et regardez ce qui se passe!) Et une autre ingénierie générale qui semble être beaucoup plus populaire avec java / .net que C / C ++.

gbjbaanb
la source
2
Beaucoup de choses que vous écrivez ne sont pas vraies pour les éboueurs de génération modernes.
Michael Borgwardt
3
@ MichaelBorgwardt tels que? Je dis "le GC tourne régulièrement" et "ça compacte le tas". Le reste de ma réponse concerne l'utilisation de la mémoire par les structures de données d'application.
gbjbaanb
6

"Est-ce simplement parce que C ++ est compilé en code assembleur / machine alors que Java / C # a toujours la charge de traitement de la compilation JIT au moment de l'exécution?" En gros oui!

Une note rapide cependant, Java a plus de frais généraux que la compilation JIT. Par exemple, il effectue beaucoup plus de vérifications pour vous (c'est ainsi qu'il fait des choses comme ArrayIndexOutOfBoundsExceptionset NullPointerExceptions). Le ramasse-miettes est une autre surcharge importante.

Il y a une comparaison assez détaillée ici .

vaughandroid
la source
2

Gardez à l'esprit que ce qui suit ne fait que comparer la différence entre la compilation native et la compilation JIT, et ne couvre pas les spécificités d'un langage ou d'un framework particulier. Il peut y avoir des raisons légitimes de choisir une plate-forme particulière au-delà.

Lorsque nous affirmons que le code natif est plus rapide, nous parlons du cas d'utilisation typique du code natif compilé par rapport au code compilé JIT, où l'utilisation typique d'une application compilée JIT doit être exécutée par l'utilisateur, avec des résultats immédiats (par exemple, en attente sur le compilateur en premier). Dans ce cas, je ne pense pas que quiconque puisse prétendre de manière franche que le code compilé par JIT puisse correspondre ou battre le code natif.

Supposons que nous ayons un programme écrit dans un langage X et que nous puissions le compiler avec un compilateur natif, et encore avec un compilateur JIT. Chaque flux de travail comporte les mêmes étapes, qui peuvent être généralisées comme suit: (Code -> Représentation intermédiaire -> Code machine -> Exécution). La grande différence entre deux étapes est de savoir quelles étapes sont vues par l'utilisateur et lesquelles sont vues par le programmeur. Avec la compilation native, le programmeur voit tout sauf l'étape d'exécution, mais avec la solution JIT, la compilation en code machine est vue par l'utilisateur, en plus de l'exécution.

L'affirmation selon laquelle A est plus rapide que B fait référence au temps pris par le programme pour s'exécuter, tel que vu par l'utilisateur . Si nous supposons que les deux morceaux de code fonctionnent de manière identique à l'étape d'exécution, nous devons supposer que le flux de travail JIT est plus lent pour l'utilisateur, car il doit également voir le temps T de la compilation en code machine, où T> 0. , pour que le flux de travail JIT puisse fonctionner de la même manière que le flux de travail natif, nous devons réduire le temps d’exécution du code pour que le code Exécution + Compilation en code machine soit inférieur au seul stade d’exécution. du flux de travail natif. Cela signifie que nous devons optimiser le code mieux dans la compilation JIT que dans la compilation native.

Ceci est cependant plutôt infaisable, car pour effectuer les optimisations nécessaires afin d’accélérer l’exécution, nous devons passer plus de temps à l’étape de la compilation vers le code machine, de sorte que chaque fois que nous économisons du fait du code optimisé est perdu, on l'ajoute à la compilation. En d'autres termes, la "lenteur" d'une solution basée sur JIT n'est pas simplement due au temps ajouté pour la compilation JIT, mais au code produit par cette compilation, elle est plus lente qu'une solution native.

Je vais utiliser un exemple: attribution de registre. Comme l'accès mémoire est plusieurs milliers de fois plus lent que l'accès aux registres, nous souhaitons idéalement utiliser des registres dans la mesure du possible et disposer du moins d'accès mémoire possible, mais nous avons un nombre limité de registres et nous devons passer l'état à la mémoire lorsque nous en avons besoin. un registre. Si nous utilisons un algorithme d'allocation de registre qui prend 200 ms à calculer, nous économisons donc 2 ms de temps d'exécution. Nous n'utilisons pas le temps de la meilleure façon possible pour un compilateur JIT. Des solutions telles que l'algorithme de Chaitin, qui peut produire un code hautement optimisé, ne conviennent pas.

Le rôle du compilateur JIT est de trouver le meilleur équilibre entre le temps de compilation et la qualité du code produit, avec toutefois un fort parti pris pour le temps de compilation rapide, car vous ne voulez pas laisser l’utilisateur en attente. Les performances du code en cours d'exécution sont plus lentes dans le cas de JIT, car le compilateur natif n'est pas lié (beaucoup) par le temps dans l'optimisation du code, il est donc libre d'utiliser les meilleurs algorithmes. La possibilité que la compilation globale + l'exécution pour un compilateur JIT puisse battre uniquement le temps d'exécution pour le code compilé en mode natif est effectivement 0.

Mais nos machines virtuelles ne se limitent pas à la compilation JIT. Ils utilisent des techniques de compilation rapides, la mise en cache, le remplacement à chaud et l'optimisation adaptative. Modifions donc notre affirmation selon laquelle la performance correspond à ce que voit l'utilisateur et limitons-la au temps nécessaire à l'exécution du programme (supposons que nous avons compilé AOT). Nous pouvons effectivement rendre le code d'exécution équivalent au compilateur natif (ou peut-être mieux?). Une grande revendication pour les VM est qu’elles peuvent produire un code de meilleure qualité qu’un compilateur natif, car il a accès à plus d’informations - celles du processus en cours, telles que la fréquence d’exécution d’une fonction donnée. La VM peut ensuite appliquer des optimisations adaptatives au code essentiel via un échange à chaud.

Cet argument pose cependant un problème: il suppose que l'optimisation guidée par le profil, entre autres, est unique en son genre pour les ordinateurs virtuels, ce qui n'est pas vrai. Nous pouvons également l'appliquer à la compilation native - en compilant notre application avec le profilage activé, en enregistrant les informations, puis en recompilant l'application avec ce profil. Il est également intéressant de noter que le remplacement à chaud de code n’est pas une chose que seul un compilateur JIT peut faire, nous pouvons le faire pour du code natif - bien que les solutions basées sur JIT soient plus facilement disponibles et beaucoup plus simples pour le développeur. La grande question est donc la suivante: une machine virtuelle peut-elle nous fournir des informations que la compilation native ne peut pas générer, ce qui peut améliorer les performances de notre code?

Je ne peux pas le voir moi-même. Nous pouvons également appliquer la plupart des techniques d’une VM typique au code natif - bien que le processus soit plus complexe. De même, nous pouvons appliquer toutes les optimisations d'un compilateur natif à une machine virtuelle qui utilise la compilation AOT ou des optimisations adaptatives. La réalité est que la différence entre le code natif et celui exécuté dans une machine virtuelle n'est pas aussi grande qu'on le croit. Ils aboutissent finalement au même résultat, mais ils adoptent une approche différente pour y parvenir. La machine virtuelle utilise une approche itérative pour produire un code optimisé, où le compilateur natif l’attend dès le départ (et peut être améliorée avec une approche itérative).

Un programmeur C ++ pourrait faire valoir qu'il a besoin des optimisations dès le départ et qu'il ne devrait pas attendre qu'une machine virtuelle détermine comment les réaliser, voire pas du tout. C’est probablement un point valable avec notre technologie actuelle, car le niveau actuel d’optimisation dans nos VM est inférieur à ce que les compilateurs natifs peuvent offrir - mais cela ne sera peut-être pas toujours le cas si les solutions AOT de nos VM s’améliorent, etc.

Mark H
la source
0

Cet article résume un ensemble de billets de blog en essayant de comparer la vitesse de c ++ par rapport à c # et les problèmes que vous devez résoudre dans les deux langues pour obtenir du code haute performance. Le résumé est "votre bibliothèque est bien plus importante que tout, mais si vous êtes en c ++, vous pouvez surmonter cela." ou "les langues modernes ont de meilleures bibliothèques et obtiennent ainsi des résultats plus rapides avec un effort moindre" en fonction de votre orientation philosophique.

Jeff Gates
la source
0

Je pense que la vraie question ici n'est pas "qui est plus rapide?" mais "qui a le meilleur potentiel pour une performance supérieure?". Vu sous ces termes, C ++ l'emporte clairement - il est compilé en code natif, il n'y a pas de JIT, c'est un niveau d'abstraction plus bas, etc.

C'est loin de l'histoire complète.

Du fait que C ++ est compilé, toute optimisation du compilateur doit être effectuée au moment de la compilation et les optimisations du compilateur appropriées pour une machine peuvent être complètement fausses pour une autre. Il est également vrai que toute optimisation globale du compilateur peut favoriser certains algorithmes ou modèles de code par rapport à d’autres.

D'autre part, un programme JITted sera optimisé au moment de l'exécution, de sorte qu'il puisse tirer quelques astuces qu'un programme précompilé ne peut pas et peut effectuer des optimisations très spécifiques pour la machine sur laquelle il est réellement exécuté et le code qu'il est réellement exécuté. Une fois que vous avez dépassé les frais généraux du JIT, il est possible dans certains cas d’être plus rapide.

Dans les deux cas, une implémentation judicieuse de l'algorithme et d'autres instances du programmeur qui ne sont pas stupides constitueront probablement des facteurs bien plus significatifs. Cependant, par exemple, il est parfaitement possible d'écrire du code de chaîne complètement mort-mort en C ++ qui sera même encombré un langage de script interprété.

Maximus Minimus
la source
3
"Les optimisations de compilateur appropriées pour une machine peuvent être complètement fausses pour une autre" Eh bien, ce n'est pas vraiment à blâmer pour le langage. Le code réellement critique à la performance peut être compilé séparément pour chaque machine sur laquelle il fonctionnera, ce qui est une évidence si vous compilez localement à partir de source ( -march=native). - "c'est un niveau d'abstraction inférieur" n'est pas vraiment vrai. C ++ utilise des abstractions de niveau aussi élevé que Java (ou, en fait, plus élevées: programmation fonctionnelle? Métaprogrammation de modèles?), Il implémente les abstractions moins "proprement" que Java.
gauche autour du
"Un code réellement critique en termes de performances peut être compilé séparément pour chaque machine sur laquelle il s'exécutera, ce qui est une évidence si vous compilez localement à partir de la source".
Maximus Minimus
Pas nécessairement l'utilisateur final, mais uniquement la personne responsable de l'installation du programme. Sur les ordinateurs de bureau et les appareils mobiles, il s’agit généralement de l’utilisateur final, mais il ne s’agit pas des seules applications existantes, certainement pas des plus critiques en termes de performances. Et vous n'avez pas vraiment besoin d'être un programmeur pour construire un programme à partir du code source, s'il a des scripts de compilation correctement écrits, comme le font tous les bons projets de logiciels libres / ouverts.
leftaroundabout
1
En théorie, oui, un JIT peut faire plus d’astuces qu’un compilateur statique, mais en pratique (pour .NET du moins, je ne connais pas java non plus), il ne fait rien de tout cela. J'ai récemment procédé à une série de désassemblages de code .NET JIT. Il existe toutes sortes d'optimisations, telles que le retrait du code des boucles, l'élimination du code mort, etc., que le JIT .NET ne fait tout simplement pas. J'espère bien, mais l'équipe Windows de Microsoft tente de tuer .NET depuis des années. Je ne retiens donc pas mon souffle.
Orion Edwards Le
-1

La compilation JIT a en fait un impact négatif sur les performances. Si vous concevez un compilateur "parfait" et un compilateur JIT "parfait", la première option gagnera toujours en performance.

Java et C # sont tous deux interprétés dans des langages intermédiaires, puis compilés en code natif à l'exécution, ce qui réduit les performances.

Mais maintenant, la différence n’est pas si évidente pour C #: Microsoft CLR génère un code natif différent pour différents processeurs, ce qui le rend plus efficace pour la machine sur laquelle il tourne, ce qui n’est pas toujours le cas des compilateurs C ++.

PS C # est écrit très efficacement et n’a pas beaucoup de couches d’abstraction. Ce n'est pas vrai pour Java, qui n'est pas aussi efficace. Ainsi, dans ce cas, avec son grand CLR, les programmes C # affichent souvent de meilleures performances que les programmes C ++. Pour plus d'informations sur .Net et CLR, jetez un coup d'œil à "CLR via C #" de Jeffrey Richter .

SuperM
la source
8
Si l'EJ avait réellement un impact négatif sur la performance, elle ne serait sûrement pas utilisée?
Zavior
2
@Zavior - Je ne peux pas penser à une bonne réponse à votre question, mais je ne vois pas comment JIT ne peut pas augmenter les coûts de performance - le JIT est un processus supplémentaire à exécuter au moment de l'exécution qui nécessite des ressources qui ne sont pas utiles t être dépensé pour l'exécution du programme lui-même, alors qu'un langage entièrement compilé est «prêt à l'emploi».
Anonyme
3
JIT a un effet positif sur les performances, pas un effet négatif, si vous le mettez dans son contexte: il compile du code octet en code machine avant de l'exécuter. Les résultats peuvent également être mis en cache, ce qui lui permet de s'exécuter plus rapidement que le code octet équivalent interprété.
Casey Kuball
3
JIT (ou plutôt, l'approche bytecode) n'est pas utilisé pour la performance, mais pour la commodité. Au lieu de pré-construire des fichiers binaires pour chaque plate-forme (ou un sous-ensemble commun, qui est sous-optimal pour chacune d'entre elles), vous ne compilez qu'à moitié et laissez le compilateur JIT faire le reste. "Écrire une fois, déployer n'importe où" est la raison pour laquelle c'est fait de cette façon. La commodité peut être fait avec juste un interprète bytecode, mais JIT ne fait plus vite que l'interprète première (mais pas nécessairement assez rapide pour battre une solution pré-compilé, la compilation JIT ne prend du temps, et le résultat n'a pas toujours compenser pour ça).
tdammers
4
@Tdammmers, en réalité, il y a aussi un élément de performance. Voir java.sun.com/products/hotspot/whitepaper.html . Les optimisations peuvent inclure des ajustements dynamiques pour améliorer la prédiction de branche et les accès au cache, l’inline dynamique, la déprovialisation, la désactivation de la vérification des limites et le déroulement de la boucle. L'affirmation est que, dans de nombreux cas, ceux-ci peuvent plus que payer le coût de l'EJ.
Charles E. Grant