Eden Space
Ma question est donc de savoir si tout cela peut être vrai, et si oui, pourquoi l'allocation de tas de Java est-elle beaucoup plus rapide?
J'étudie un peu le fonctionnement du Java GC car c'est très intéressant pour moi. J'essaie toujours d'élargir ma collection de stratégies d'allocation de mémoire en C et C ++ (intéressé à essayer d'implémenter quelque chose de similaire en C), et c'est un moyen très, très rapide d'allouer beaucoup d'objets de manière éclatée à partir d'un perspective pratique mais principalement due au multithreading.
La façon dont fonctionne l'allocation Java GC consiste à utiliser une stratégie d'allocation extrêmement bon marché pour allouer initialement des objets à l'espace "Eden". D'après ce que je peux dire, il utilise un allocateur de pool séquentiel.
C'est beaucoup plus rapide juste en termes d'algorithme et de réduction des défauts de page obligatoires que pour un usage général malloc
en C ou par défaut, en jetant operator new
en C ++.
Mais les allocateurs séquentiels ont une faiblesse flagrante: ils peuvent allouer des morceaux de taille variable, mais ils ne peuvent pas libérer de morceaux individuels. Ils allouent simplement de façon séquentielle droite avec un remplissage pour l'alignement et ne peuvent purger que toute la mémoire qu'ils ont allouée en même temps. Ils sont généralement utiles en C et C ++ pour construire des structures de données qui ne nécessitent que des insertions et pas de suppression d'éléments, comme un arbre de recherche qui ne doit être construit qu'une seule fois lorsqu'un programme démarre puis est recherché à plusieurs reprises ou seulement de nouvelles clés ont été ajoutées ( aucune clé retirée).
Ils peuvent également être utilisés même pour les structures de données qui permettent de supprimer des éléments, mais ces éléments ne seront pas réellement libérés de la mémoire car nous ne pouvons pas les désallouer individuellement. Une telle structure utilisant un allocateur séquentiel consommerait simplement de plus en plus de mémoire, sauf si elle avait une passe différée où les données étaient copiées sur une nouvelle copie compactée à l'aide d'un allocateur séquentiel séparé (et c'est parfois une technique très efficace si un allocateur fixe gagnait pas pour une raison quelconque - allouez simplement séquentiellement une nouvelle copie de la structure de données et videz toute la mémoire de l'ancienne).
Collection
Comme dans l'exemple de structure de données / pool séquentiel ci-dessus, ce serait un énorme problème si Java GC n'allouait que de cette façon, même s'il est super rapide pour une allocation en rafale de nombreux morceaux individuels. Il ne serait pas en mesure de libérer quoi que ce soit jusqu'à ce que le logiciel soit arrêté, auquel cas il pourrait libérer (purger) tous les pools de mémoire en une seule fois.
Ainsi, au lieu de cela, après un seul cycle GC, un passage est effectué à travers des objets existants dans l'espace "Eden" (alloués séquentiellement), et ceux qui sont encore référencés sont ensuite alloués à l'aide d'un allocateur plus général capable de libérer des morceaux individuels. Ceux qui ne sont plus référencés seront simplement désalloués lors du processus de purge. Donc, fondamentalement, c'est "copier des objets hors de l'espace Eden s'ils sont encore référencés, puis purger".
Cela serait normalement assez cher, donc c'est fait dans un thread d'arrière-plan séparé pour éviter de bloquer de manière significative le thread qui a initialement alloué toute la mémoire.
Une fois la mémoire copiée hors de l'espace Eden et allouée à l'aide de ce schéma plus coûteux qui peut libérer des morceaux individuels après un cycle GC initial, les objets se déplacent vers une région de mémoire plus persistante. Ces morceaux individuels sont ensuite libérés dans les cycles GC suivants s'ils cessent d'être référencés.
La vitesse
Donc, en termes simples, la raison pour laquelle le GC Java pourrait très bien surpasser C ou C ++ lors de l'allocation de tas droite est parce qu'il utilise la stratégie d'allocation totalement dégénéralisée la moins chère dans le thread demandant l'allocation de mémoire. Ensuite, il enregistre le travail plus coûteux que nous aurions normalement besoin de faire lors de l'utilisation d'un allocateur plus général comme directement malloc
pour un autre thread.
Donc, conceptuellement, le GC doit en fait faire plus de travail dans l'ensemble, mais il le répartit sur tous les threads afin que le coût total ne soit pas payé d'avance par un seul thread. Il permet au thread allouant de la mémoire de le faire très bon marché, puis de reporter les vraies dépenses nécessaires pour faire les choses correctement afin que les objets individuels puissent réellement être libérés sur un autre thread. En C ou C ++ lorsque nous malloc
ou appelons operator new
, nous devons payer le coût intégral à l'avance dans le même thread.
C'est la principale différence, et pourquoi Java pourrait très bien surpasser le C ou le C ++ en utilisant uniquement des appels naïfs à malloc
ou operator new
pour allouer un tas de petits morceaux individuellement. Bien sûr, il y aura généralement des opérations atomiques et un verrouillage potentiel lorsque le cycle GC démarrera, mais il est probablement un peu optimisé.
Fondamentalement, l'explication simple se résume à payer un coût plus élevé dans un seul thread ( malloc
) par rapport à payer un coût moins cher dans un seul thread, puis à payer le coût plus lourd dans un autre qui peut fonctionner en parallèle ( GC
). En tant qu'inconvénient, cela signifie que vous avez besoin de deux indirections pour passer de la référence d'objet à l'objet comme requis pour permettre à l'allocateur de copier / déplacer la mémoire sans invalider les références d'objet existantes, et vous pouvez également perdre la localité spatiale une fois que la mémoire de l'objet est déplacé hors de l'espace "Eden".
Enfin et surtout, la comparaison est un peu injuste car le code C ++ n'alloue normalement pas une cargaison d'objets individuellement sur le tas. Un code C ++ décent a tendance à allouer de la mémoire pour de nombreux éléments dans des blocs contigus ou sur la pile. S'il alloue un bateau de petits objets un par un sur la boutique gratuite, le code est merdique.