Pourquoi les listes de struct sont 15 fois plus rapide à allouer qu'une liste de classe

Comme évoquer dans la description du billet, nous avons affaire à un gain de 15x. Essayons de comprendre pourquoi afin d'évaluer les cas d'usage qui permettent de privilégier des structures.

La première chose pour nous est de comprendre la différence entre allouer une instance d'un type de référence sur le tas et allouer une instance d'une structure sur la pile. Le temps d'allocation de mémoire dans un tas géré pour un type de référence est généralement une opération rapide concernant les objets alloués et stockés en continu. Le CLR (Common Language Runtime) a un pointeur sur le premier espace libre en mémoire. L'allocation d'un nouvel objet implique l'ajout de la taille du nouvel objet au pointeur.

Une fois l'objet placé sur le tas géré, son adresse est réécrite dans la référence objet qui a été créée sur la pile. En général, l'ensemble de ce processus est assez "bon marché". Cependant, le processus d'allocation de mémoire pour un objet de type référence n'est pas toujours aussi simple et peut impliquer des étapes lourdes supplémentaires.

Si le type de référence est supérieur à 85Ko, le runtime passera plus de temps à rechercher l'emplacement approprié dans le tas d'objets volumineux pour stocker l'objet car la mémoire y est fragmentée (bloc libre ou trous dans l'espace d'adressage).

L'allocation d'objets de type référence est lente dans les cas oú il n'y a plus d'espace libre dans le Small Object Heap pour stocker l'objet demandé par l'application. Lorsqu'un tel cas se produit, le CLR doit exécuter le processus de récupération de place. Si le ramasse-miettes (Garbage Collector) n'a pas libéré suffisamment de mémoire, le runtime demande des pages supplémentaires de mémoire virtuelle.

Qu'en est-il de l'allocation d'une instance de type valeur sur la pile ?

L'allocation de mémoire pour le type valeur est une opération presque instantanée et le temps d'allocation ne dépend presque pas de la taille du type valeur. La seule chose que le runtime doit faire est de créer le cadre de pile d'une taille appropriée pour stocker le type de valeur et modifier le pointeur de pile. Le point à retenir est que placer une instance d'un type valeur sur la pile est rapide et, plus important encore, est un processus déterministe en termes de temps par rapport à l'allocation d'un objet de type référence sur un tas.

Revenons à nous moutons, lorsqu'un million d'instances d'un type de référence sont allouées, elles sont poussée une par une dans le tas géré et les références sont réenregistrées dans l'instance de collection. En fait, il y aura un million +1 objet en mémoire.

Cependant, lorsqu'un million d'instances d'un type valeur sont allouées, il n'y a que le seul objet poussé dans un tas géré qui est l'instance d'une collection. Un million de structures seront intégrées dans la liste d'instance. La seule chose à faire pour l'exécution après la création de la liste est de la remplir avec les données.

Les développeurs bénéficient non seulement d'un temps d'allocation rapide lors du choix d'une structure au lieu d'une classe pour une grande collection, mais également du temps de publication.

Si les développeurs allouent un million d'instances de classe, pendant la phase de "marquage et nettoyage" du garbage collecteur (ou ramasse miette) il devra analyser un million d'objets et vérifier si chacun a encore des références. Ensuite, lors de la phase "compact", le ramasse miette devra déplacer un million d'objets. Finalement, les adresses  stockées dans la liste devront être mise à jour avec de nouvelles adresses, c'est beaucoup de travail.

Mais pour le ramasse miettes, les choses vont beaucoup mieux lorsque les développeurs allouent un million de structure car il y aura la seule liste dans un tas géré avec laquelle le ramasse miettes devra travailler.

Conclusion 

Les structures peuvent être plus performantes que les classes, mais analysez toujours soigneusement votre cas spécifique avant de remplacer la classe par struct ou l'inverse. Le travail d'un développeur n'est pas de suivre aveuglément les recommendations ou les meilleures pratiques, mais de choisir les outils, méthodes, approches les plus appropriés pour résoudre son cas unique par la meilleure façon possible. 


Rejoindre la conversation