Qu'est-ce que le pool d'objets? Amélioration des performances de la mémoire en C #
L'allocation de mémoire en C # est relativement coûteuse et constitue un point clé d'optimisation pour toute application à performances critiques. Le regroupement d'objets est une technique qui peut aider à réduire la surcharge d'une application gourmande en mémoire.
Comment la mise en pool d'objets améliore-t-elle les performances?
En fin de compte, les performances des ordinateurs sont essentiellement limitées à deux choses: la vitesse de traitement du processeur et les performances de la mémoire. Le premier peut être amélioré avec des algorithmes plus efficaces qui utilisent moins de cycles d'horloge, mais il est souvent plus difficile d'optimiser l'utilisation de la mémoire, en particulier lorsque vous travaillez avec de grands ensembles de données et des échelles de temps très courtes.
Le pool d'objets est une technique utilisée pour réduire la mémoire allocations. Il est souvent inutile d’allouer de la mémoire, mais vous n’avez peut-être pas besoin d’allouer aussi souvent que vous le faites. Les allocations d'objets sont lentes pour plusieurs raisons: la mémoire sous-jacente est allouée sur le tas (ce qui est beaucoup plus lent que les allocations de pile de type valeur), et les objets complexes peuvent avoir des constructeurs gourmands en performances. De plus, comme il s'agit d'une mémoire basée sur le tas, le garbage collector devra le nettoyer, ce qui peut nuire aux performances si vous le déclenchez trop souvent.
Par exemple, supposons que vous ayez une boucle qui s'exécute plusieurs fois et alloue un nouvel objet comme une liste à chaque exécution. C’est une grande quantité de mémoire utilisée, et elle n’est nettoyée qu’après la fin de tout et l’exécution du garbage collection. Le code suivant s'exécutera 10 000 fois et laissera 10 000 listes sans propriétaire allouées en mémoire à la fin de la fonction.
Lorsque le ramasse-miettes s’exécute finalement, il va avoir du mal à nettoyer tous ces déchets, ce qui aura un impact négatif sur les performances en attendant que GC se termine.
Au lieu de cela, une approche plus judicieuse consiste à l'initialiser une fois et à réutiliser l'objet. Cela garantit que vous réutilisez le même espace en mémoire, plutôt que de l'oublier et de laisser le garbage collector s'en occuper. Ce n’est pas de la magie, et vous devrez éventuellement nettoyer.
Pour cet exemple, l'approche recyclable serait en cours d'exécution new List
avant la boucle pour effectuer la première allocation, puis en cours d'exécution .Clear
ou réinitialiser les données pour économiser de l'espace mémoire et des déchets créés. Une fois cette boucle terminée, il ne restera qu'une seule liste en mémoire, qui est beaucoup plus petite que 10 000 d'entre elles.
Le regroupement d'objets est essentiellement une implémentation générique de ce concept. C'est une collection d'objets qui peuvent être réutilisés. Il n’existe pas d’interface officielle pour un, mais en général, ils ont un magasin de données interne et implémentent deux méthodes: GetObject()
, et ReleaseObject()
.
Plutôt que d'allouer un nouvel objet, vous en demandez un dans le pool d'objets. La piscine peut créer un nouvel objet s’il n’en a pas de disponible. Ensuite, lorsque vous en avez terminé, vous relâchez cet objet dans la piscine. Plutôt que de jeter l'objet à la poubelle, le pool d'objets le garde alloué mais l'efface de toutes les données. La prochaine fois que tu courras GetObject
, il renvoie le nouvel objet vide. Pour les objets volumineux, comme les listes, cela est beaucoup plus facile pour la mémoire sous-jacente.
Vous devez vous assurer que vous libérez l'objet avant de l'utiliser à nouveau, car la plupart des pools auront un nombre maximum d'objets vides qu'ils gardent sous la main. Si vous essayez d'obtenir 1 000 objets de la piscine, vous la tarirez et elle commencera à les allouer normalement sur demande, ce qui va à l'encontre de l'objectif d'une réserve.
Dans les cas où les performances sont critiques, en particulier lorsque vous travaillez souvent avec beaucoup de données répétées, la mise en pool d'objets peut considérablement améliorer les performances. Cependant, ce n’est pas un fourre-tout et il arrive souvent que vous ne souhaitiez pas regrouper d’objets. Allocations avec new
sont encore assez rapides, donc à moins que vous allouiez très souvent de gros morceaux de mémoire ou que vous allouiez des objets avec des constructeurs lourds en performances, il est souvent préférable d’utiliser new
plutôt que de regrouper inutilement, d'autant plus que le pool lui-même ajoute une surcharge et une complexité supplémentaires à l'allocation. L'allocation d'un seul objet régulier sera toujours plus rapide que l'allocation d'un objet en pool unique; la différence de performance vient lorsque vous allouez plusieurs fois.
Vous devrez également vous assurer que votre mise en œuvre de pool efface correctement l'état de l'objet. Sinon, vous pouvez risquer GetObject
renvoyer quelque chose qui a des données périmées. Cela pourrait être un problème majeur si, par exemple, vous renvoyiez les données d'un utilisateur différent lors de la récupération de quelqu'un d'autre. En règle générale, ce n'est pas un problème si cela est fait correctement, mais c'est quelque chose à garder à l'esprit.
Si vous souhaitez utiliser des pools d'objets, Microsoft propose une implémentation dans Microsoft.Extensions.ObjectPool
. Si vous souhaitez l'implémenter vous-même, vous pouvez ouvrir la source pour voir comment cela fonctionne dans DefaultObjectPool
. En règle générale, vous disposez d'un pool d'objets différent pour chaque type, avec un nombre maximal d'objets à conserver dans le pool.