web-dev-qa-db-fra.com

Pourquoi serait-il possible que Java soit plus rapide que C ++?

Parfois Java surpasse C++ dans les benchmarks. Bien sûr, parfois C++ surpasse.

Voir les liens suivants:

Mais comment est-ce encore possible? Cela me fait penser que le bytecode interprété pourrait être plus rapide qu'un langage compilé.

Quelqu'un peut-il expliquer? Merci!

81
Deets McGeets

Tout d'abord, la plupart des machines virtuelles Java incluent un compilateur, donc le "bytecode interprété" est en fait assez rare (au moins dans le code de référence - ce n'est pas aussi rare dans la vie réelle, où votre code est généralement plus que quelques boucles triviales qui se répètent très souvent). ).

Deuxièmement, bon nombre des repères impliqués semblent être assez biaisés (que ce soit par intention ou par incompétence, je ne peux pas vraiment le dire). Par exemple, il y a quelques années, j'ai regardé une partie du code source lié à l'un des liens que vous avez publiés. Il avait un code comme celui-ci:

  init0 = (int*)calloc(max_x,sizeof(int));
  init1 = (int*)calloc(max_x,sizeof(int));
  init2 = (int*)calloc(max_x,sizeof(int));
  for (x=0; x<max_x; x++) {
    init2[x] = 0;
    init1[x] = 0;
    init0[x] = 0;
  }

Puisque calloc fournit une mémoire déjà mise à zéro, utiliser la boucle for pour la remettre à zéro est évidemment inutile. Cela a été suivi (si la mémoire sert) en remplissant la mémoire avec d'autres données de toute façon (et aucune dépendance à sa remise à zéro), de sorte que toute la mise à zéro était complètement inutile de toute façon. Le remplacement du code ci-dessus par un simple malloc (comme toute personne sensée aurait pu commencer par) a amélioré la vitesse de la version C++ suffisamment pour battre la version Java (par une marge assez large, si mémoire sert).

Considérez (pour un autre exemple) le benchmark methcall utilisé dans l'entrée de blog de votre dernier lien. Malgré le nom (et à quoi les choses pourraient ressembler), la version C++ de ceci ne mesure pas vraiment la surcharge des appels de méthode. La partie du code qui s'avère critique se trouve dans la classe Toggle:

class Toggle {
public:
    Toggle(bool start_state) : state(start_state) { }
    virtual ~Toggle() {  }
    bool value() {
        return(state);
    }
    virtual Toggle& activate() {
        state = !state;
        return(*this);
    }
    bool state;
};

La partie critique s'avère être le state = !state;. Considérez ce qui se passe lorsque nous modifions le code pour coder l'état en tant que int au lieu d'un bool:

class Toggle {
    enum names{ bfalse = -1, btrue = 1};
    const static names values[2];
    int state;

public:
    Toggle(bool start_state) : state(values[start_state]) 
    { }
    virtual ~Toggle() {  }
    bool value() {  return state==btrue;    }

    virtual Toggle& activate() {
        state = -state;
        return(*this);
    }
};

Ce changement mineur améliore la vitesse globale d'environ marge 5: 1. Même si le benchmark était destiné à mesurer le temps d'appel de la méthode, en réalité, la plupart de ce qu'il mesurait était le temps de conversion entre int et bool. Je conviendrais certainement que l'inefficacité montrée par l'original est regrettable - mais étant donné la rareté avec laquelle cela semble se produire dans le code réel et la facilité avec laquelle il peut être corrigé quand/si cela se produit, j'ai du mal à penser de cela comme signifiant beaucoup.

Dans le cas où quelqu'un décide de relancer les benchmarks impliqués, je dois également ajouter qu'il y a une modification presque aussi triviale à la version Java qui produit (ou au moins à un moment donné - je n'ai pas réexécuté les tests avec une JVM récente pour confirmer qu'ils le font encore) une amélioration assez substantielle de la version Java également. La version Java a un NthToggle :: activate () qui ressemble à ceci:

public Toggle activate() {
this.counter += 1;
if (this.counter >= this.count_max) {
    this.state = !this.state;
    this.counter = 0;
}
return(this);
}

Changer cela pour appeler la fonction de base au lieu de manipuler this.state Donne une amélioration de vitesse assez substantielle (mais pas suffisante pour suivre la version C++ modifiée).

Ainsi, nous nous retrouvons avec une fausse hypothèse sur les codes d'octets interprétés par rapport à certains des pires repères (que j'ai) jamais vus. Ni donne un résultat significatif.

Ma propre expérience est que, avec des programmeurs également expérimentés accordant une attention égale à l'optimisation, C++ battra Java plus souvent qu'autrement - mais (au moins entre ces deux), le langage fera rarement autant de différence que les programmeurs et conception. Les repères cités nous en disent plus sur la (in) compétence/(dés) honnêteté de leurs auteurs que sur les langues qu'ils prétendent comparer.

[Edit: Comme impliqué à un endroit ci-dessus mais jamais déclaré aussi directement que je devrais probablement l'avoir fait, les résultats que je cite sont ceux que j'ai obtenus lorsque j'ai testé cela il y a ~ 5 ans, en utilisant les implémentations C++ et Java qui étaient courant à ce moment-là. Je n'ai pas réexécuté les tests avec les implémentations actuelles. Un coup d'œil, cependant, indique que le code n'a pas été corrigé, donc tout ce qui aurait changé serait la capacité du compilateur à couvrir les problèmes dans le code.]

Cependant, si nous ignorons les exemples Java, il est réellement possible pour le code interprété de s'exécuter plus rapidement que le code compilé (bien que difficile et quelque peu inhabituel ).

Cela se produit généralement de la manière suivante: le code en cours d'interprétation est beaucoup plus compact que le code machine, ou il s'exécute sur un processeur doté d'un cache de données plus important que le cache de code.

Dans un tel cas, un petit interprète (par exemple, l'interpréteur interne d'une implémentation de Forth) peut être en mesure de tenir entièrement dans le cache de code, et le programme qu'il interprète tient entièrement dans le cache de données. Le cache est généralement plus rapide que la mémoire principale d'un facteur d'au moins 10, et souvent beaucoup plus (un facteur de 100 n'est plus particulièrement rare).

Donc, si le cache est plus rapide que la mémoire principale d'un facteur N et qu'il faut moins de N instructions de code machine pour implémenter chaque code d'octet, le code d'octet devrait gagner (je simplifie, mais je pense que l'idée générale devrait toujours être évident).

108
Jerry Coffin

Roulé à la main C/C++ fait par un expert avec un temps illimité va être au moins aussi rapide ou plus rapide que Java. En fin de compte, Java lui-même est écrit en C/C++ afin que vous puissiez bien sûr tout faire Java le fait si vous êtes prêt à faire suffisamment d'efforts d'ingénierie).

En pratique cependant, Java s'exécute souvent très rapidement pour les raisons suivantes:

  • compilation JIT - bien que Java sont stockées en bytecode, ceci est (généralement) compilé en code natif par le compilateur JIT au démarrage du programme. Une fois compilé, il est du code natif pur - donc théoriquement, on peut s'attendre à ce qu'il fonctionne aussi bien que C/C++ compilé une fois que le programme a fonctionné assez longtemps (c'est-à-dire après que toute la compilation JIT a été effectuée)
  • Collecte des ordures in Java est extrêmement rapide et efficace - le GC Hotspot est probablement la meilleure implémentation GC complète au monde. C'est le résultat de nombreuses années-hommes des efforts d'experts de Sun et d'autres sociétés. À peu près n'importe quel système de gestion de mémoire complexe que vous vous lancez en C/C++ sera pire. Bien sûr, vous pouvez écrire des schémas de gestion de mémoire de base assez rapides/légers en C/C++, mais ils ont gagné " t être presque aussi polyvalent qu’un système GC complet. Comme la plupart des systèmes modernes nécessitent une gestion complexe de la mémoire, Java présente donc un gros avantage pour les situations réelles).
  • Meilleur ciblage de la plateforme - en retardant la compilation au démarrage de l'application (compilation JIT, etc.), le compilateur Java peut profiter du fait qu'il connaît le processeur exact sur lequel il s'exécute. Cela peut permettre des optimisations très avantageuses que vous ne pourriez pas faire dans du code C/C++ précompilé qui doit cibler un jeu d'instructions de processeur "au plus petit dénominateur commun".
  • Statistiques d'exécution - parce que la compilation JIT est effectuée au moment de l'exécution, elle peut recueillir des statistiques pendant l'exécution du programme qui permettent de meilleures optimisations (par exemple, connaître la probabilité qu'une branche particulière soit prise). Cela peut permettre à Java JIT de produire un meilleur code que les compilateurs C/C++ (qui doivent "deviner" la branche la plus probable à l'avance, hypothèse qui peut souvent être erronée).
  • Très bonnes bibliothèques - le Java runtime contient un hôte de bibliothèques très bien écrit avec de bonnes performances (en particulier pour les applications côté serveur). Souvent, ce sont mieux que vous ne pourriez écrivez-vous ou obtenez facilement pour C/C++.

En même temps, C/C++ présente également certains avantages:

  • Plus de temps pour faire des optimisations avancées - La compilation C/C++ est effectuée une fois, et peut donc passer beaucoup de temps à faire des optimisations avancées si vous la configurez pour le faire. Il n'y a aucune raison théorique pour laquelle Java ne pourrait pas faire la même chose, mais en pratique, vous voulez Java pour compiler du code JIT relativement rapidement, donc le compilateur JIT a tendance à se concentrer sur des optimisations "plus simples".
  • Instructions qui ne sont pas exprimables en bytecode - tandis que Java bytecode est entièrement à usage général, il y a encore certaines choses que vous pouvez faire à un niveau bas que vous ne pouvez pas faire en bytecode (l'arithmétique des pointeurs non vérifiés est un bon exemple!). En (ab) en utilisant ce genre de trucs, vous pouvez obtenir des avantages en termes de performances
  • Moins de contraintes de "sécurité" - Java fait un travail supplémentaire pour s'assurer que les programmes sont sûrs et fiables. Les exemples sont les contrôles de limites sur les tableaux, certaines garanties de concurrence, les contrôles de pointeurs nuls , tapez safety sur les transtypages, etc. En les évitant en C/C++, vous pouvez obtenir des gains de performances (même si cela peut être une mauvaise idée!)

Global:

  • Java et C/C++ peuvent atteindre des vitesses similaires
  • C/C++ a probablement le léger Edge dans des circonstances extrêmes (il n'est pas surprenant que les développeurs de jeux AAA le préfèrent encore, par exemple)
  • En pratique, cela dépendra de l'équilibre des différents facteurs énumérés ci-dessus pour votre application particulière.
112
mikera

Le Java runtime isnt interprète le bytecode. Il utilise plutôt ce qu'on appelle Just In Time Compilation =. Fondamentalement, pendant que le programme est exécuté, il prend le bytecode et le convertit en code natif optimisé pour le CPU particulier.

19
GrandmasterB

Toutes choses étant égales par ailleurs, vous pourriez dire: non, Java ne devrait jamais être plus rapide. Vous pouvez toujours implémenter Java en C++ à partir de gratter et ainsi obtenir des performances au moins aussi bonnes. En pratique cependant:

  • JIT compile le code sur la machine de l'utilisateur final, lui permettant d'optimiser pour le CPU exact qu'ils exécutent. Bien qu'il y ait des frais généraux ici pour la compilation, cela pourrait bien porter ses fruits pour les applications intensives. Souvent, les programmes réels ne sont pas compilés pour le processeur que vous utilisez.
  • Le compilateur Java peut être mieux à optimiser automatiquement les choses qu'un compilateur C++. Ou peut-être pas, mais dans le monde réel, les choses ne sont pas toujours parfaites.
  • Le comportement des performances peut varier en raison d'autres facteurs, tels que la récupération de place. En C++, vous appelez généralement le destructeur immédiatement lorsque vous avez terminé avec un objet. En Java, vous libérez simplement la référence, retardant la destruction réelle. C'est un autre exemple de différence qui n'est ni ici ni là, en termes de performances. Bien sûr, vous pouvez affirmer que vous pouvez implémenter GC en C++ et en finir avec, mais la réalité est que peu de gens le font/veulent/peuvent.

En passant, cela me rappelle le débat sur le C dans les années 80/90. Tout le monde se demandait "C peut-il être aussi rapide que l'assemblage?". Fondamentalement, la réponse était: non sur le papier, mais en réalité, le compilateur C a créé un code plus efficace que 90% des programmeurs d'assemblage (enfin, une fois qu'il a mûri un peu).

19
Daniel B

Mais l'allocation n'est que la moitié de la gestion de la mémoire - la désallocation est l'autre moitié. Il s'avère que pour la plupart des objets, le coût direct de la récupération de place est de - zéro. En effet, un collecteur de copie n'a pas besoin de visiter ou de copier des objets morts, seulement des objets vivants. Les objets qui deviennent des ordures peu de temps après l'allocation ne contribuent donc pas à la charge de travail du cycle de collecte.

...

Les machines virtuelles Java sont étonnamment bonnes pour comprendre des choses que nous supposions que seul le développeur pouvait savoir. En laissant la JVM choisir entre l'allocation de pile et l'allocation de tas au cas par cas, nous pouvons obtenir les avantages de performance de l'allocation de pile sans que le programmeur agonise sur l'allocation sur la pile ou sur le tas.

http://www.ibm.com/developerworks/Java/library/j-jtp09275/index.html

10
Landei

Alors qu'un programme Java Java complètement optimisé battra rarement un programme C++ complètement optimisé, des différences dans des choses comme la gestion de la mémoire peuvent rendre beaucoup d'algorithmes idiomatiquement implémentés dans Java plus rapide que les mêmes algorithmes implémentés idiomatiquement en C++.

Comme l'a souligné @Jerry Coffin, il existe de nombreux cas où de simples modifications peuvent rendre le code beaucoup plus rapide - mais souvent, il peut prendre trop de réglages impurs dans une langue ou dans l'autre pour que l'amélioration des performances en vaille la peine. C'est probablement ce que vous verriez dans un bon benchmark qui montre Java faisant mieux que C++.

De plus, bien que ce ne soit généralement pas si important, il existe une optimisation des performances qu'un langage JIT comme Java peut faire que C++ ne peut pas. Le Java runtime peut inclure des améliorations après que le code a été compilé, ce qui signifie que le JIT peut potentiellement produire du code optimisé pour tirer parti de nouvelles (ou au moins différentes) fonctionnalités CPU Pour cette raison, un binaire de 10 ans Java peut potentiellement surpasser un binaire C++ de 10 ans.

Enfin, une sécurité de type complète dans son ensemble peut, dans de très rares cas, offrir des améliorations de performances extrêmes. Singularité , un OS expérimental écrit presque entièrement dans un langage basé sur C #, a une communication interprocessus et un multitâche beaucoup plus rapides du fait qu'il n'y a pas besoin de limites de processus matériel ou de commutateurs de contexte coûteux.

5
Rei Miyasaka

Publié par Tim Holloway sur JavaRanch:

Voici un exemple primitif: à l'époque où les machines fonctionnaient selon des cycles déterminés mathématiquement, une instruction de branche avait généralement 2 synchronisations différentes. Un pour quand la branche a été prise, un pour quand la branche n'a pas été prise. Habituellement, le cas sans succursale était plus rapide. De toute évidence, cela signifiait que vous pouviez optimiser la logique en fonction de la connaissance du cas le plus courant (sous réserve que ce que nous "savons" ne soit pas toujours ce qui est réellement le cas).

La recompilation JIT va encore plus loin. Il surveille l'utilisation réelle en temps réel et inverse la logique en fonction du cas le plus courant. Et retournez-le à nouveau si la charge de travail change. Le code compilé statiquement ne peut pas faire cela. Voilà comment Java peut parfois surpasser le code Assembly/C/C++ réglé à la main.

Source: http://www.coderanch.com/t/547458/Performance/Java/Ahead-Time-vs-Just-time

5
Thiago Negri

En effet, la dernière étape de génération du code machine se déroule de manière transparente à l'intérieur la JVM lors de l'exécution de votre programme Java, au lieu d'être explicite lors de la construction de votre programme C++).

Vous devriez considérer le fait que les machines virtuelles Java modernes passent beaucoup de temps à compiler le code d'octets à la volée en code machine natif pour le rendre aussi rapide que possible. Cela permet à la JVM d'effectuer toutes sortes de trucs de compilation qui peuvent être encore meilleurs en connaissant les données de profilage du programme en cours d'exécution.

Une telle chose que d'inclure automatiquement un getter, de sorte qu'un JUMP-RETURN ne soit pas nécessaire pour obtenir simplement une valeur, accélère les choses.

Cependant, la chose qui a vraiment permis des programmes rapides est un meilleur nettoyage par la suite. Le mécanisme de récupération de place dans Java est plus rapide que le manuel sans malloc en C. De nombreuses implémentations modernes sans malloc utilisent un récupérateur de place en dessous.

4
user1249

Réponse courte - ce n'est pas le cas. Oubliez ça, le sujet est aussi vieux que le feu ou la roue. Java ou .NET n'est pas et ne sera pas plus rapide que C/C++. Il est assez rapide pour la plupart des tâches où vous n'avez pas du tout besoin de penser à l'optimisation. Comme les formulaires et le traitement SQL, mais c'est là que ça se termine.

Pour les benchmarks ou les petites applications écrites par des développeurs incompétents, oui, le résultat final sera que Java/.NET va probablement être proche et peut-être encore plus rapide.

En réalité, des choses simples comme allouer de la mémoire sur la pile ou simplement utiliser des memzones tueront simplement Java/.NET sur place.

Le monde de la collecte des ordures utilise une sorte de memzone avec toute la comptabilité. Ajouter une memzone à C et C sera plus rapide sur place. Surtout pour les benchmarks Java vs C "code haute performance", qui vont comme ceci:

for(...)
{
alloc_memory//Allocating heap in a loop is verrry good, in't it?
zero_memory//Extra zeroing, we really need it in our performance code
do_stuff//something like memory[i]++
realloc//This is lovely speedup
strlen//loop through all memory, because storing string length is soo getting old
free//Java will do that outside out timing loop, but oh well, we're comparing apples to oranges here
}//loop 100000 times

Essayez d'utiliser des variables basées sur la pile en C/C++ (ou nouveau placement), elles se traduisent par sub esp, 0xff, c'est une seule instruction x86, battez-la avec Java - vous ne pouvez pas ...

La plupart du temps, je vois ces bancs où Java contre C++ sont comparés, cela me fait aller comme, avec des stratégies d'allocation de mémoire incorrectes, des conteneurs auto-croissants sans réserves, plusieurs nouveaux. Ce n'est pas même proche du code C/C++ orienté performances.

Aussi une bonne lecture: https://days2011.scala-lang.org/sites/days2011/files/ws3-1-Hundt.pdf

4
Coder

La réalité est qu'ils ne sont que des assembleurs de haut niveau qui font exactement ce que le programmeur leur dit, exactement comment le programmeur leur dit dans l'ordre exact que le programmeur leur dit. Les différences de performances sont si faibles qu'elles sont sans conséquence pour toutes les applications pratiques.

Le langage n'est pas "lent", le programmeur a écrit un programme lent. Il est très rare qu'un programme écrit de la meilleure façon dans une langue soit supérieur (à n'importe quel but pratique) à un programme faisant la même chose en utilisant la meilleure façon de la langue alternative, à moins que l'auteur de l'étude ne cherche à broyer sa hache particulière.

Évidemment, si vous allez dans un cas Edge rare comme les systèmes embarqués durs en temps réel, le choix de la langue peut faire la différence, mais à quelle fréquence est-ce le cas? et dans ces cas, à quelle fréquence le bon choix n'est-il pas aveuglément évident?.

2
mattnz

Voir les liens suivants ... Mais comment est-ce encore possible? Cela me fait penser que le bytecode interprété pourrait être plus rapide qu'un langage compilé.

  1. Ces articles de blog fournissent-ils des preuves fiables?
  2. Ces articles de blog fournissent-ils des preuves définitives?
  3. Ces articles de blog fournissent-ils même des preuves de "bytecode interprété"?

Keith Lea vous dit qu'il y a des "défauts évidents" mais ne fait rien sur ces "défauts évidents". En 2005, ces anciennes tâches ont été abandonnées et remplacées par les tâches maintenant affichées dans le jeu de benchmarks .

Keith Lea vous dit qu'il "a pris le code de référence pour C++ et Java de la désormais obsolète Great Computer Language Shootout et a exécuté les tests" mais en fait il ne montre que les mesures de 14 sur 25 de ces tests obsolètes .

Keith Lea vous dit maintenant qu'il n'essayait pas de prouver quoi que ce soit avec le blog sept ans auparavant, mais à l'époque, il a dit: "J'en avais marre d'entendre les gens dire Java était lent, quand je sais c'est assez rapide ... "ce qui suggère à l'époque qu'il essayait de prouver quelque chose.

Christian Felde vous dit "Je n'ai pas créé le code, j'ai simplement relancé les tests." comme si cela le dégageait de toute responsabilité dans sa décision de publier les mesures des tâches et des programmes sélectionnés par Keith Lea.

Les mesures de même 25 minuscules programmes minuscules fournissent-elles des preuves définitives?

Ces mesures concernent des programmes exécutés en "mode mixte" Java non interprété Java - "N'oubliez pas comment fonctionne HotSpot.") Vous pouvez savoir facilement comment Java exécute le "bytecode interprété", car vous pouvez forcer Java à uniquement interpréter le bytecode - il suffit de chronométrer certains programmes Java exécutés avec et sans l'option -Xint.

2
igouy