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.
la source
Réponses:
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:
Quelques autres facteurs liés à la mémoire, mais pas au cache:
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.
la source
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
new
etdelete
C ++, 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.
la source
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 unList
.List<int>
utilise unint[]
, pasObject[]
comme Java. Voir stackoverflow.com/questions/116988/…vector<N>
emplacement où, dans le cas particulier devector<4>
, mon implémentation SIMD codée à la main devrait être utiliséeCe 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
virtual
lorsque 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
inline
faire 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.la source
Connaissez-vous le document de recherche Google sur ce sujet?
De la conclusion:
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".
la source
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:
Donc, selon le langage avec lequel vous comparez le C ++, vous pouvez obtenir ou non la même réponse.
En C ++, vous avez:
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:
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.
la source
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 ++.
la source
"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
ArrayIndexOutOfBoundsExceptions
etNullPointerExceptions
). Le ramasse-miettes est une autre surcharge importante.Il y a une comparaison assez détaillée ici .
la source
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.
la source
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.
la source
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é.
la 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.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 .
la source