[...] (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 Pixel
objet (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' Pixel
objet, utilisiez un tableau d'octets et modélisiez un Image
objet. 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 int
au lieu de Integer
) et que vous commenciez à modéliser des interfaces en vrac comme un Image
interface, pas Pixel
interface. 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 int
garantit une représentation contiguë. Un tableau de Integer
ne 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 Integer
peut ê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 Integer
est 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 Boolean
est 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 final
pour 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 Integer
vs int
peut ê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
, float
etc., 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 int
vs 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 ints
contre un million au hasard Integers
et le faire à plusieurs reprises (le Integers
remaniement 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 Account
s'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.