Java est-il beaucoup plus difficile à «ajuster» pour les performances par rapport à C / C ++? [fermé]


11

La «magie» de la JVM empêche-t-elle l'influence d'un programmeur sur les micro-optimisations en Java? J'ai récemment lu en C ++ parfois l'ordre des données des membres peut fournir des optimisations (accordées, dans l'environnement de microsecondes) et je présumais que les mains d'un programmeur sont liées quand il s'agit de réduire les performances de Java?

J'apprécie qu'un algorithme décent offre des gains de vitesse plus importants, mais une fois que vous avez le bon algorithme, Java est-il plus difficile à modifier en raison du contrôle JVM?

Sinon, les gens pourraient-ils donner des exemples des astuces que vous pouvez utiliser en Java (en plus des simples drapeaux de compilation).


14
Le principe de base de toute optimisation Java est le suivant: la JVM l'a probablement déjà fait mieux que vous. L'optimisation implique principalement de suivre des pratiques de programmation sensées et d'éviter les choses habituelles comme la concaténation de chaînes dans une boucle.
Robert Harvey

3
Le principe de la micro-optimisation dans toutes les langues est que le compilateur l'a déjà fait mieux que vous. L'autre principe de la micro-optimisation dans toutes les langues est que jeter plus de matériel dessus est moins cher que le temps de micro-optimisation du programmeur. Le programmeur doit avoir tendance à faire évoluer les problèmes (algorithmes sous-optimaux), mais la micro-optimisation est une perte de temps. Parfois, la micro-optimisation est logique sur les systèmes embarqués où vous ne pouvez pas jeter plus de matériel dessus, mais Android utilisant Java, et une implémentation plutôt médiocre de celui-ci, montre que la plupart d'entre eux ont déjà suffisamment de matériel.
Jan Hudec

1
pour "Java performance tricks", il vaut la peine d'étudier sont les suivants: Effective Java , Angelika Langer Links - Java Performance and performance related articles by Brian Goetz in Java theory and practice and Threading Lightly series listed here
gnat

2
Soyez extrêmement prudent sur les trucs et astuces - la JVM, les systèmes d'exploitation et le matériel évoluent - vous feriez mieux d'apprendre la méthodologie de réglage des performances et d'appliquer des améliorations pour votre environnement particulier :-)
Martijn Verburg

Dans certains cas, une machine virtuelle peut effectuer des optimisations au moment de l'exécution qui ne sont pas réalisables au moment de la compilation. L'utilisation de la mémoire gérée peut améliorer les performances, mais elle aura également souvent une empreinte mémoire plus élevée. La mémoire inutilisée est libérée lorsque cela est pratique, plutôt que le plus tôt possible.
Brian

Réponses:


5

Bien sûr, au niveau de la micro-optimisation, la JVM fera certaines choses sur lesquelles vous aurez peu de contrôle par rapport au C et au C ++ en particulier.

D'un autre côté, la variété des comportements du compilateur avec C et C ++ en particulier aura un impact négatif beaucoup plus important sur votre capacité à faire des micro-optimisations de toute sorte de manière vaguement portable (même entre les révisions du compilateur).

Cela dépend du type de projet que vous modifiez, des environnements que vous ciblez, etc. Et en fin de compte, cela n'a pas vraiment d'importance car vous obtenez de quelques ordres de grandeur de meilleurs résultats de toutes façons optimisations algorithmiques / structure de données / conception de programme.


Cela peut avoir beaucoup d'importance lorsque vous constatez que votre application ne se déploie pas sur plusieurs cœurs
James

@james - vous voulez élaborer?
Telastyn


1
@James, la mise à l'échelle à travers les cœurs a très peu à voir avec le langage d'implémentation (sauf Python!), Et, plus à voir avec l'architecture d'application.
James Anderson

29

Les micro-optimisations ne valent presque jamais le temps, et presque toutes les faciles sont effectuées automatiquement par les compilateurs et les runtimes.

Il existe cependant un domaine d'optimisation important où C ++ et Java sont fondamentalement différents, à savoir l'accès à la mémoire en bloc. C ++ dispose d'une gestion manuelle de la mémoire, ce qui signifie que vous pouvez optimiser la disposition des données de l'application et les modèles d'accès pour utiliser pleinement les caches. C'est assez difficile, quelque peu spécifique au matériel que vous utilisez (donc les gains de performances peuvent disparaître sur différents matériels), mais si cela est fait correctement, cela peut conduire à des performances absolument à couper le souffle. Bien sûr, vous payez pour cela avec le potentiel de toutes sortes de bugs horribles.

Avec un langage récupéré comme Java, ce type d'optimisations ne peut pas être fait dans le code. Certains peuvent être effectués par le runtime (automatiquement ou via la configuration, voir ci-dessous), et certains ne sont tout simplement pas possibles (le prix à payer pour être protégé contre les bogues de gestion de la mémoire).

Sinon, les gens pourraient-ils donner des exemples des astuces que vous pouvez utiliser en Java (en plus des simples drapeaux de compilation).

Les drapeaux du compilateur ne sont pas pertinents en Java car le compilateur Java ne fait presque aucune optimisation; le runtime le fait.

Et en effet, les runtimes Java ont une multitude de paramètres qui peuvent être modifiés, en particulier concernant le garbage collector. Il n'y a rien de "simple" dans ces options - les valeurs par défaut sont bonnes pour la plupart des applications, et pour obtenir de meilleures performances, vous devez comprendre exactement ce que font les options et le comportement de votre application.


1
+1: essentiellement ce que j'écrivais dans ma réponse, peut-être une meilleure formulation.
Klaim

1
+1: Très bons points, expliqués de façon très concise: "C'est assez difficile ... mais si c'est bien fait, cela peut conduire à des performances absolument à couper le souffle. Bien sûr, vous payez pour cela avec le potentiel de toutes sortes de bugs horribles . "
Giorgio

1
@MartinBa: C'est plus que vous payez pour optimiser la gestion de la mémoire. Si vous n'essayez pas d'optimiser la gestion de la mémoire, la gestion de la mémoire C ++ n'est pas si difficile (évitez-la entièrement via STL ou rendez-la relativement facile en utilisant RAII). Bien sûr, l'implémentation de RAII en C ++ nécessite plus de lignes de code que de ne rien faire en Java (c'est-à-dire parce que Java le gère pour vous).
Brian

3
@Martin Ba: Essentiellement oui. Pointeurs pendants, débordements de tampon, pointeurs non initialisés, erreurs dans l'arithmétique des pointeurs, tout ce qui n'existe tout simplement pas sans gestion manuelle de la mémoire. Et l'optimisation de l'accès à la mémoire nécessite à peu près beaucoup de gestion manuelle de la mémoire.
Michael Borgwardt

1
Il y a deux ou trois choses que vous pouvez faire en Java. L'un est le pool d'objets, qui maximise les chances de localisation de la mémoire des objets (contrairement au C ++ où il peut garantir la localisation de la mémoire).
RokL

5

[...] (accordé, dans l'environnement microseconde) [...]

Les micro-secondes s'additionnent si nous bouclons des millions à des milliards de choses. Une session vtune / micro-optimisation personnelle en C ++ (pas d'améliorations algorithmiques):

T-Rex (12.3 million facets):
Initial Time: 32.2372797 seconds
Multithreading: 7.4896073 seconds
4.9201039 seconds
4.6946372 seconds
3.261677 seconds
2.6988536 seconds
SIMD: 1.7831 seconds
4-valence patch optimization: 1.25007 seconds
0.978046 seconds
0.970057 seconds
0.911041 seconds

Tout en dehors du "multithreading", "SIMD" (écrit à la main pour battre le compilateur), et l'optimisation du patch à 4 valences étaient des optimisations de mémoire au niveau micro. De plus, le code d'origine à partir des temps initiaux de 32 secondes était déjà un peu optimisé (complexité algorithmique théoriquement optimale) et il s'agit d'une session récente. La version originale bien avant cette récente session a pris plus de 5 minutes à traiter.

L'optimisation de l'efficacité de la mémoire peut souvent aider de plusieurs fois à des ordres de grandeur dans un contexte à un seul thread, et plus dans des contextes multithreads (les avantages d'un représentant de mémoire efficace se multiplient souvent avec plusieurs threads dans le mélange).

Sur l'importance de la micro-optimisation

Je suis un peu agité par cette idée que les micro-optimisations sont une perte de temps. Je conviens que c'est un bon conseil général, mais tout le monde ne le fait pas incorrectement en se basant sur des intuitions et des superstitions plutôt que sur des mesures. Fait correctement, il ne produit pas nécessairement un micro impact. Si nous prenons le propre Embree d'Intel (noyau de lancer de rayons) et testons uniquement le BVH scalaire simple qu'ils ont écrit (pas le paquet de rayons qui est exponentiellement plus difficile à battre), puis essayons de battre les performances de cette structure de données, cela peut être un plus une expérience humiliante même pour un vétéran habitué au profilage et au réglage du code pendant des décennies. Et tout cela grâce aux micro-optimisations appliquées. Leur solution peut traiter plus de cent millions de rayons par seconde lorsque j'ai vu des professionnels de l'industrie du raytracing qui peuvent '

Il n'y a aucun moyen de prendre une implémentation simple d'un BVH avec seulement une focalisation algorithmique et d'en tirer plus de cent millions d'intersections de rayons primaires par seconde contre tout compilateur d'optimisation (même le propre ICC d'Intel). Un simple n'obtient souvent même pas un million de rayons par seconde. Il faut des solutions de qualité professionnelle pour obtenir souvent même quelques millions de rayons par seconde. Il faut une micro-optimisation au niveau Intel pour obtenir plus de cent millions de rayons par seconde.

Des algorithmes

Je pense que la micro-optimisation n'est pas importante tant que les performances ne sont pas importantes au niveau des minutes à secondes, par exemple, ou des heures à minutes. Si nous prenons un algorithme horrible comme le tri à bulles et que nous l'utilisons sur une entrée de masse comme exemple, puis le comparons à une implémentation même de base du tri par fusion, le premier peut prendre des mois à traiter, le dernier peut-être 12 minutes, par conséquent de la complexité quadratique vs linéaireithmique.

La différence entre les mois et les minutes va probablement amener la plupart des gens, même ceux qui ne travaillent pas dans des domaines critiques pour les performances, à considérer le temps d'exécution comme inacceptable s'il nécessite que les utilisateurs attendent des mois pour obtenir un résultat.

Pendant ce temps, si nous comparons le tri par fusion simple et non micro-optimisé au tri rapide (qui n'est pas du tout supérieur sur le plan algorithmique au tri par fusion, et ne propose que des améliorations au niveau micro pour la localité de référence), le tri rapide micro-optimisé pourrait se terminer dans 15 secondes au lieu de 12 minutes. Faire patienter 12 minutes pourrait être parfaitement acceptable (type de pause-café).

Je pense que cette différence est probablement négligeable pour la plupart des gens entre, disons, 12 minutes et 15 secondes, et c'est pourquoi la micro-optimisation est souvent considérée comme inutile car elle ne ressemble souvent qu'à la différence entre les minutes et les secondes, et non les minutes et les mois. L'autre raison pour laquelle je pense que cela est inutile est qu'il est souvent appliqué à des zones qui n'ont pas d'importance: une petite zone qui n'est même pas bouclée et critique, ce qui donne une différence discutable de 1% (qui peut très bien être simplement du bruit). Mais pour les personnes qui se soucient de ces types de différences de temps et qui sont prêtes à mesurer et à bien faire, je pense qu'il vaut la peine de prêter attention au moins aux concepts de base de la hiérarchie de la mémoire (en particulier les niveaux supérieurs relatifs aux défauts de page et aux échecs de cache) .

Java laisse beaucoup de place à de bonnes micro-optimisations

Ouf, désolé - avec ce genre de diatribe de côté:

La «magie» de la JVM empêche-t-elle l'influence d'un programmeur sur les micro-optimisations en Java?

Un peu mais pas autant que les gens pourraient penser si vous le faites correctement. Par exemple, si vous effectuez un traitement d'image, en code natif avec SIMD manuscrit, multithreading et optimisations de mémoire (modèles d'accès et éventuellement même représentation en fonction de l'algorithme de traitement d'image), il est facile de croiser des centaines de millions de pixels par seconde pendant 32- pixels RGBA (canaux couleur 8 bits) et parfois même des milliards par seconde.

Il est impossible de se rapprocher de Java si vous dites que vous avez créé un Pixelobjet (cela seul ferait gonfler la taille d'un pixel de 4 octets à 16 sur 64 bits).

Mais vous pourriez être en mesure de vous rapprocher beaucoup plus si vous évitiez l' Pixelobjet, utilisiez un tableau d'octets et modélisiez un Imageobjet. Java est encore assez compétent si vous commencez à utiliser des tableaux de données anciennes et simples. J'ai déjà essayé ce genre de choses en Java et j'ai été très impressionné à condition que vous ne créiez pas un tas de petits objets minuscules partout qui soient 4 fois plus gros que la normale (ex: utilisez intau lieu de Integer) et que vous commenciez à modéliser des interfaces en vrac comme un Imageinterface, pas Pixelinterface. Je me risquerais même à dire que Java peut rivaliser avec les performances C ++ si vous faites une boucle sur de vieilles données simples et non sur des objets (énormes tableaux de float, par exemple, non Float).

Peut-être encore plus important que les tailles de mémoire est qu'un tableau de intgarantit une représentation contiguë. Un tableau de Integerne fonctionne pas. La contiguïté est souvent essentielle pour la localité de référence, car elle signifie que plusieurs éléments (ex: 16 ints) peuvent tous s'insérer dans une seule ligne de cache et être potentiellement accessibles ensemble avant l'expulsion avec des modèles d'accès à la mémoire efficaces. Pendant ce temps, un seul Integerpeut être bloqué quelque part dans la mémoire, la mémoire environnante n'étant pas pertinente, uniquement pour que cette région de mémoire soit chargée dans une ligne de cache uniquement pour utiliser un seul entier avant l'expulsion, par opposition à 16 entiers. Même si nous avons été merveilleusement chanceux et entourésIntegersétaient tous les uns à côté des autres en mémoire, nous ne pouvons insérer que 4 dans une ligne de cache accessible avant l'expulsion car elle Integerest 4 fois plus grande, et c'est dans le meilleur des cas.

Et il y a beaucoup de micro-optimisations à réaliser car nous sommes unifiés sous la même architecture / hiérarchie de mémoire. Peu importe la langue que vous utilisez, les modèles d'accès à la mémoire importent, des concepts comme le tuilage / blocage de boucle peuvent généralement être appliqués beaucoup plus souvent en C ou C ++, mais ils bénéficient tout autant à Java.

J'ai récemment lu en C ++ parfois l'ordre des données des membres peut fournir des optimisations [...]

L'ordre des membres des données n'a généralement pas d'importance en Java, mais c'est surtout une bonne chose. En C et C ++, la préservation de l'ordre des membres des données est souvent importante pour des raisons ABI afin que les compilateurs ne s'en occupent pas. Les développeurs humains qui y travaillent doivent faire attention à faire des choses comme organiser leurs membres de données dans l'ordre décroissant (du plus grand au plus petit) pour éviter de gaspiller de la mémoire lors du remplissage. Avec Java, le JIT peut apparemment réorganiser les membres pour vous à la volée afin d'assurer un alignement correct tout en minimisant le remplissage, donc à condition que ce soit le cas, il automatise quelque chose que les programmeurs C et C ++ moyens peuvent souvent mal faire et finissent par gaspiller de la mémoire de cette façon ( ce qui ne fait pas que gaspiller de la mémoire, mais souvent une perte de vitesse en augmentant inutilement la foulée entre les structures AoS et en provoquant plus de ratés de cache). Il' C'est une chose très robotique de réorganiser les champs pour minimiser le rembourrage, donc idéalement, les humains ne s'en occupent pas. La seule fois où l'agencement des champs peut avoir une importance qui nécessite qu'un humain connaisse l'arrangement optimal est si l'objet est plus grand que 64 octets et que nous organisons les champs en fonction du modèle d'accès (pas de remplissage optimal) - auquel cas il pourrait être une entreprise plus humaine (nécessite la compréhension des chemins critiques, dont certains sont des informations qu'un compilateur ne peut pas anticiper sans savoir ce que les utilisateurs feront du logiciel).

Sinon, les gens pourraient-ils donner des exemples des astuces que vous pouvez utiliser en Java (en plus des simples drapeaux de compilation).

La plus grande différence pour moi en termes de mentalité d'optimisation entre Java et C ++ est que C ++ pourrait vous permettre d'utiliser un peu (minuscule) les objets plus que Java dans un scénario critique en termes de performances. Par exemple, C ++ peut encapsuler un entier dans une classe sans aucune surcharge (référencée partout). Java doit avoir cette surcharge de style de pointeur de métadonnées + alignement par objet, c'est pourquoi il Booleanest plus grand que boolean(mais en échange, il offre des avantages uniformes de réflexion et la possibilité de remplacer toute fonction non marquée comme finalpour chaque UDT).

Il est un peu plus facile en C ++ de contrôler la contiguïté des dispositions de mémoire sur des champs non homogènes (ex: entrelacement flottants et entiers dans un tableau via une structure / classe), car la localité spatiale est souvent perdue (ou du moins le contrôle est perdu) en Java lors de l'allocation d'objets via le GC.

... mais souvent les solutions les plus performantes les séparent de toute façon et utilisent un modèle d'accès SoA sur des tableaux contigus d'anciennes données simples. Donc, pour les domaines qui nécessitent des performances optimales, les stratégies pour optimiser la disposition de la mémoire entre Java et C ++ sont souvent les mêmes, et vous obligeront souvent à démolir ces minuscules interfaces orientées objet au profit d'interfaces de style collection qui peuvent faire des choses comme hot / division de champ froid, représentants SoA, etc. Les représentants AoSoA non homogènes semblent plutôt impossibles en Java (sauf si vous venez d'utiliser un tableau brut d'octets ou quelque chose comme ça), mais ce sont pour de rares cas où les deuxles modèles d'accès séquentiel et aléatoire doivent être rapides tout en ayant simultanément un mélange de types de champs pour les champs chauds. Pour moi, la majeure partie de la différence de stratégie d'optimisation (au niveau général) entre ces deux est théorique si vous atteignez des performances de pointe.

Les différences varient un peu plus si vous recherchez simplement de "bonnes" performances - ne pas pouvoir faire autant avec de petits objets comme Integervs intpeut être un peu plus d'un PITA, en particulier avec la façon dont il interagit avec les génériques . Il est un peu plus difficile de créer une seule structure de données générique en tant que cible d'optimisation centrale en Java qui fonctionne pour int, floatetc., tout en évitant les UDT plus grandes et coûteuses, mais souvent les zones les plus critiques en termes de performances nécessiteront de rouler à la main vos propres structures de données réglé pour un but très spécifique de toute façon, donc ce n'est ennuyeux que pour le code qui recherche de bonnes performances mais pas des performances de pointe.

Overhead d'objet

Notez que la surcharge des objets Java (métadonnées et perte de localité spatiale et perte temporaire de localité temporelle après un cycle GC initial) est souvent importante pour les choses qui sont vraiment petites (comme intvs Integer) qui sont stockées par millions dans une structure de données qui est largement contiguë et accessible en boucles très serrées. Il semble y avoir beaucoup de sensibilité à ce sujet, donc je dois préciser que vous ne voulez pas vous soucier de la surcharge des objets pour les gros objets comme les images, juste des objets vraiment minuscules comme un seul pixel.

Si quelqu'un doute de cette partie, je suggérerais de faire un point de référence entre résumer un million au hasard intscontre un million au hasard Integerset le faire à plusieurs reprises (le Integersremaniement en mémoire après un premier cycle de GC).

Astuce ultime: des conceptions d'interface qui laissent la place à l'optimisation

Donc, l'astuce Java ultime telle que je la vois si vous avez affaire à un endroit qui gère une lourde charge sur de petits objets (ex: a Pixel, un vecteur à 4 vecteurs, une matrice 4x4, un Particle, peut-être même un Accounts'il ne dispose que de quelques petits champs) est d'éviter d'utiliser des objets pour ces petites choses et d'utiliser des tableaux (éventuellement enchaînés) de vieilles données simples. Les objets deviennent alors des interfaces de collecte comme Image, ParticleSystem, Accounts, une collection de matrices ou des vecteurs, etc. individuels sont accessibles par index, par exemple Ceci est aussi l' une des astuces de conception ultime en C et C ++, puisque même sans que les frais généraux d'objets de base et mémoire disjointe, la modélisation de l'interface au niveau d'une seule particule empêche les solutions les plus efficaces.


1
Étant donné que de mauvaises performances en vrac pourraient en fait avoir une chance décente d'écraser des performances de pointe dans les domaines critiques, je ne pense pas que l'on puisse complètement ignorer l'avantage d'avoir de bonnes performances facilement. Et l'astuce de transformer un tableau de structures en une structure de tableaux se décompose quelque peu lorsque toutes (ou presque toutes) les valeurs comprenant l'une des structures d'origine seront accessibles en même temps. BTW: Je vois que vous dénichez beaucoup de messages anciens et ajoutez votre propre bonne réponse, parfois même la bonne réponse ;-)
Deduplicator

1
@Deduplicator J'espère que je ne dérange pas les gens en se cognant trop! Celui-ci a été un peu minable - peut-être que je devrais l'améliorer un peu. SoA vs AoS est souvent difficile pour moi (accès séquentiel vs aléatoire). Je sais rarement à l'avance lequel utiliser, car il y a souvent un mélange d'accès séquentiel et aléatoire dans mon cas. La leçon précieuse que j'ai souvent apprise est de concevoir des interfaces qui laissent assez de place pour jouer avec la représentation des données - des interfaces un peu plus volumineuses qui ont de gros algorithmes de transformation lorsque cela est possible (parfois impossible avec des minuscules accès aléatoires ici et là).

1
Eh bien, je l'ai remarqué seulement parce que les choses sont vraiment lentes. Et j'ai pris mon temps avec chacun.
Déduplicateur

Je me demande vraiment pourquoi user204677est parti. Une si bonne réponse.
oligofren

3

Il y a une zone médiane entre la micro-optimisation, d'une part, et le bon choix d'algorithme, d'autre part.

C'est le domaine des accélérations à facteur constant, et il peut donner des ordres de grandeur.
Pour ce faire, il faut interrompre des fractions entières du temps d'exécution, comme 30%, puis 20% de ce qui reste, puis 50%, et ainsi de suite pendant plusieurs itérations, jusqu'à ce qu'il ne reste presque plus rien.

Vous ne voyez pas cela dans les petits programmes de style démo. Où vous le voyez, c'est dans de gros programmes sérieux avec beaucoup de structures de données de classe, où la pile d'appels est généralement profonde de plusieurs couches. Un bon moyen de trouver les opportunités d'accélération consiste à examiner des échantillons aléatoires de l'état du programme.

Généralement, les accélérations se composent de choses comme:

  • minimiser les appels à newen regroupant et en réutilisant d'anciens objets,

  • reconnaître les choses qui sont faites qui sont en quelque sorte là pour la généralité, plutôt que d'être réellement nécessaires,

  • réviser la structure des données en utilisant différentes classes de collecte qui ont le même comportement big-O mais tirent parti des modèles d'accès réellement utilisés,

  • enregistrer les données qui ont été acquises par des appels de fonction au lieu de ré-appeler la fonction, (C'est une tendance naturelle et amusante des programmeurs de supposer que les fonctions ayant des noms plus courts s'exécutent plus rapidement.)

  • tolérer une certaine incohérence entre les structures de données redondantes, au lieu d'essayer de les garder entièrement cohérentes avec les événements de notification,

  • etc.

Mais bien sûr, rien de tout cela ne devrait être fait sans qu'il soit d'abord démontré qu'il y a des problèmes en prélevant des échantillons.


2

Java (pour autant que je sache) ne vous donne aucun contrôle sur les emplacements des variables en mémoire, vous avez donc plus de mal à éviter des choses comme le faux partage et l'alignement des variables (vous pouvez compléter une classe avec plusieurs membres inutilisés). Une autre chose dont je ne pense pas que vous puissiez tirer parti est des instructions telles que mmpause, mais ces choses sont spécifiques au CPU et donc si vous pensez que vous en avez besoin, Java n'est peut-être pas le langage à utiliser.

Il existe la classe Unsafe qui vous donne la flexibilité de C / C ++ mais aussi avec le danger de C / C ++.

Cela peut vous aider à regarder le code assembleur que la JVM génère pour votre code

Pour en savoir plus sur une application Java qui examine ce genre de détails, consultez le code Disruptor publié par LMAX


2

Il est très difficile de répondre à cette question, car cela dépend des implémentations du langage.

En général, il y a très peu de place pour de telles «micro-optimisations» de nos jours. La raison principale est que les compilateurs profitent de telles optimisations lors de la compilation. Par exemple, il n'y a pas de différence de performances entre les opérateurs pré-incrément et post-incrément dans les situations où leur sémantique est identique. Un autre exemple serait par exemple une boucle comme celle-ci for(int i=0; i<vec.size(); i++)où l'on pourrait argumenter qu'au lieu d'appeler lesize()fonction membre lors de chaque itération il serait préférable d'obtenir la taille du vecteur avant la boucle puis de comparer par rapport à cette variable unique et d'éviter ainsi la fonction d'un appel par itération. Cependant, il existe des cas dans lesquels un compilateur détectera ce cas idiot et mettra en cache le résultat. Cependant, cela n'est possible que lorsque la fonction n'a pas d'effets secondaires et que le compilateur peut être sûr que la taille du vecteur reste constante pendant la boucle, de sorte qu'elle ne s'applique qu'à des cas assez triviaux.


Quant au deuxième cas, je ne pense pas que le compilateur puisse l'optimiser dans un avenir prévisible. Détecter qu'il est sûr d'optimiser vec.size () dépend de prouver que la taille si le vecteur / perdu ne change pas à l'intérieur de la boucle, ce qui, à mon avis, est indécidable en raison du problème d'arrêt.
Lie Ryan

@LieRyan J'ai vu plusieurs cas (simples) dans lesquels le compilateur a généré un fichier binaire exactement identique si le résultat a été "mis en cache" manuellement et si size () a été appelé. J'ai écrit du code et il s'avère que le comportement dépend fortement de la façon dont le programme fonctionne. Il y a des cas dans lesquels le compilateur peut garantir qu'il n'y a aucune possibilité que la taille du vecteur change pendant la boucle, et puis il y a des cas où il ne peut pas le garantir, tout comme le problème d'arrêt comme vous l'avez mentionné. Pour l'instant, je ne peux pas vérifier ma réclamation (le démontage C ++ est une douleur), j'ai donc modifié la réponse
zxcdw

2
@Lie Ryan: beaucoup de choses indécidables dans le cas général sont parfaitement décidables pour des cas spécifiques mais courants, et c'est vraiment tout ce dont vous avez besoin ici.
Michael Borgwardt

@LieRyan Si vous n'appelez que des constméthodes sur ce vecteur, je suis sûr que de nombreux compilateurs d'optimisation le comprendront.
K.Steff

en C #, et je pense que je lis aussi en Java, si vous ne cachez pas la taille, le compilateur sait qu'il peut supprimer les vérifications pour voir si vous allez en dehors des limites du tableau, et si vous faites la taille du cache, il doit faire les vérifications , qui coûtent généralement plus cher que ce que vous économisez en mettant en cache. Essayer de déjouer les optimiseurs est rarement un bon plan.
Kate Gregory

1

les gens pourraient-ils donner des exemples des astuces que vous pouvez utiliser en Java (en plus des simples drapeaux de compilation).

Outre les améliorations des algorithmes, assurez-vous de tenir compte de la hiérarchie de la mémoire et de la façon dont le processeur s'en sert. Il y a de gros avantages à réduire les latences d'accès à la mémoire, une fois que vous comprenez comment la langue en question alloue la mémoire à ses types de données et objets.

Exemple Java pour accéder à un tableau de 1000 x 1000 pouces

Considérez l'exemple de code ci-dessous - il accède à la même zone de mémoire (un tableau 1000x1000 d'entiers), mais dans un ordre différent. Sur mon mac mini (Core i7, 2,7 GHz), la sortie est la suivante, montrant que la traversée du tableau par lignes fait plus que doubler les performances (moyenne sur 100 tours chacune).

Processing columns by rows*** took 4 ms (avg)
Processing rows by columns*** took 10 ms (avg) 

Cela est dû au fait que le tableau est stocké de telle sorte que les colonnes consécutives (c'est-à-dire les valeurs int) sont placées adjacentes en mémoire, contrairement aux lignes consécutives. Pour que le processeur utilise réellement les données, elles doivent être transférées dans ses caches. Le transfert de mémoire se fait par un bloc d'octets, appelé ligne de cache - le chargement d'une ligne de cache directement depuis la mémoire introduit des latences et diminue ainsi les performances d'un programme.

Pour le Core i7 (pont de sable), une ligne de cache contient 64 octets, donc chaque accès à la mémoire récupère 64 octets. Étant donné que le premier test accède à la mémoire dans une séquence prévisible, le processeur prélèvera les données avant qu'elles ne soient réellement consommées par le programme. Globalement, cela se traduit par moins de latence sur les accès mémoire et améliore ainsi les performances.

Code d'échantillon:

  package test;

  import java.lang.*;

  public class PerfTest {
    public static void main(String[] args) {
      int[][] numbers = new int[1000][1000];
      long startTime;
      long stopTime;
      long elapsedAvg;
      int tries;
      int maxTries = 100;

      // process columns by rows 
      System.out.print("Processing columns by rows");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int r = 0; r < 1000; r++) {
         for(int c = 0; c < 1000; c++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     

      // process rows by columns
      System.out.print("Processing rows by columns");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int c = 0; c < 1000; c++) {
         for(int r = 0; r < 1000; r++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     
    }
  }

1

La JVM peut interférer et souvent, et le compilateur JIT peut changer considérablement entre les versions.Certaines micro-optimisations sont impossibles en Java en raison de limitations linguistiques, telles que l'hyper-threading friendly ou la dernière collection SIMD des processeurs Intel.

Il est recommandé de lire un blog très informatif sur le sujet, rédigé par l'un des auteurs de Disruptor :

Il faut toujours se demander pourquoi s'embêter à utiliser Java si vous voulez des micro-optimisations, il existe de nombreuses méthodes alternatives pour accélérer une fonction comme utiliser JNA ou JNI pour passer sur une bibliothèque native.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.