64 votes

Boxing unboxing et génériques

La méthode .NET 1.0 de création d'une collection d'entiers (par exemple) était la suivante :

 ArrayList list = new ArrayList();
 list.Add(i);
 int j = (int)list[0];

L'inconvénient de cette utilisation est le manque de sécurité et de performance des types en raison de la mise en boîte et du déballage.

La méthode .NET 2.0 consiste à utiliser des génériques :

List<int> list = new List<int>();
list.Add(i);
int j = list[0];

Le prix de la mise en boîte (à ma connaissance) est la nécessité de créer un objet sur le tas, de copier l'entier alloué par la pile dans le nouvel objet et vice-versa pour la mise hors boîte.

Comment l'utilisation de génériques permet-elle de surmonter ce problème ? Est-ce que l'entier alloué à la pile reste sur la pile et est pointé à partir du tas (je suppose que ce n'est pas le cas à cause de ce qui se passera quand il sortira de la portée) ? Il semble qu'il soit toujours nécessaire de le copier ailleurs que sur la pile.

Qu'est-ce qui se passe vraiment ?

65voto

Eric Lippert Points 300275

Votre confusion est le résultat d'une mauvaise compréhension de la relation entre la pile, le tas et les variables. Voici la façon correcte d'y penser.

  • Une variable est un emplacement de stockage qui a un type.
  • La durée de vie d'une variable peut être courte ou longue. Par "courte", nous entendons "jusqu'à ce que la fonction actuelle renvoie ou jette" et par "longue", nous entendons "éventuellement plus longtemps que cela".
  • Si le type d'une variable est un type de référence, alors le contenu de la variable est une référence à un emplacement de stockage à longue durée de vie. Si le type d'une variable est un type de valeur, le contenu de la variable est une valeur.

En tant que détail d'implémentation, un emplacement de stockage qui est garanti pour être de courte durée peut être alloué sur la pile. Un emplacement de stockage qui pourrait avoir une longue durée de vie est alloué sur le tas. Notez que cela ne dit rien sur "les types de valeurs sont toujours alloués sur la pile". Les types de valeurs sont pas toujours alloué sur la pile :

int[] x = new int[10];
x[1] = 123;

x[1] est un emplacement de stockage. Il a une longue durée de vie ; il pourrait vivre plus longtemps que cette méthode. Il doit donc se trouver sur le tas. Le fait qu'il contienne un int n'est pas pertinent.

Vous avez bien dit pourquoi une boîte d'int est chère :

Le prix de la mise en boîte est la nécessité de créer un objet sur le tas, de copier l'entier alloué par la pile dans le nouvel objet et vice-versa pour la mise hors boîte.

Là où vous vous trompez, c'est en disant "le nombre entier alloué par la pile". L'endroit où l'entier a été alloué n'a pas d'importance. Ce qui compte, c'est que son stockage contenait le nombre entier au lieu de contenir une référence à un emplacement de tas . Le prix correspond à la nécessité de créer l'objet et de faire la copie ; c'est le seul coût pertinent.

Alors pourquoi une variable générique n'est-elle pas coûteuse ? Si vous avez une variable de type T, et que T est construit pour être int, alors vous avez une variable de type int, point. Une variable de type int est un emplacement de stockage, et elle contient un int. Que cet emplacement de stockage soit sur la pile ou le tas n'a aucune importance. . Ce qui est important, c'est que le lieu de stockage contient un int au lieu de contenir une référence à quelque chose sur le tas . Puisque l'emplacement de stockage contient un int, vous n'avez pas à assumer les coûts du boxing et du unboxing : allocation d'un nouveau stockage sur le tas et copie de l'int dans le nouveau stockage.

Est-ce que c'est maintenant clair ?

65voto

Dan Tao Points 60518

Lorsqu'il s'agit de collections, les génériques permettent d'éviter le boxing/unboxing en utilisant de véritables T[] en interne. List<T> par exemple, utilise un T[] pour stocker son contenu.

El tableau est bien sûr un type de référence et est donc (dans la version actuelle du CLR, yada yada) stocké sur le tas. Mais comme il s'agit d'un T[] et non un object[] les éléments du tableau peuvent être stockés "directement" : c'est-à-dire qu'ils sont toujours sur le tas, mais ils sont sur le tas dans le tableau au lieu d'être mis en boîte et que le tableau contienne des références aux boîtes.

Ainsi, pour un List<int> Par exemple, ce que vous auriez dans le tableau ressemblerait à ceci :

\[ 1 2 3 \]

Comparez cela à un ArrayList qui utilise un object[] et ressemblerait donc à quelque chose comme ça :

\[ \*a \*b \*c \]

...où *a etc. sont des références à des objets (nombres entiers encadrés) :

\*a -> 1
\*b -> 2
\*c -> 3

Excusez ces illustrations grossières ; j'espère que vous comprenez ce que je veux dire.

3voto

Mark Byers Points 318575

Une ArrayList ne gère que le type object Ainsi, pour utiliser cette classe, il est nécessaire d'effectuer un casting vers et à partir de object . Dans le cas des types de valeurs, ce moulage implique le boxing et le unboxing.

Lorsque vous utilisez une liste générique, le compilateur produit un code spécialisé pour ce type de valeur de sorte que l'option valeurs réelles sont stockées dans la liste plutôt qu'une référence aux objets qui contiennent les valeurs. Par conséquent, aucune mise en boîte n'est nécessaire.

Le prix de la mise en boîte (à ma connaissance) est la nécessité de créer un objet sur le tas, de copier l'entier alloué par la pile dans le nouvel objet et vice-versa pour la mise hors boîte.

Je pense que vous supposez que les types de valeurs sont toujours instanciés sur la pile. Ce n'est pas le cas - ils peuvent être créés soit sur le tas, soit sur la pile, soit dans les registres. Pour plus d'informations à ce sujet, veuillez consulter l'article d'Eric Lippert : La vérité sur les types de valeurs .

3voto

cdhowie Points 62253

Generics permet au tableau interne de la liste d'être typé int[] au lieu d'effectivement object[] ce qui nécessiterait une mise en boîte.

Voici ce qui se passe sans les génériques :

  1. Vous appelez Add(1) .
  2. Le nombre entier 1 est mis en boîte dans un objet, ce qui nécessite la construction d'un nouvel objet sur le tas.
  3. Cet objet est transmis à ArrayList.Add() .
  4. L'objet emballé est placé dans un object[] .

Il y a trois niveaux d'indirection ici : ArrayList -> object[] -> object -> int .

Avec des génériques :

  1. Vous appelez Add(1) .
  2. L'int 1 est passé à List<int>.Add() .
  3. L'int est placé dans un int[] .

Il n'y a donc que deux niveaux d'indirection : List<int> -> int[] -> int .

Quelques autres différences :

  • La méthode non générique nécessitera une somme de 8 ou 12 octets (un pointeur, un int) pour stocker la valeur, 4/8 dans une allocation et 4 dans l'autre. Et ce sera probablement plus en raison de l'alignement et du remplissage. La méthode générique ne nécessitera que 4 octets d'espace dans le tableau.
  • La méthode non générique nécessite l'allocation d'un int boxé ; la méthode générique ne le fait pas. Cette méthode est plus rapide et réduit la rotation du GC.
  • La méthode non générique nécessite des castings pour extraire les valeurs. Ce n'est pas sûr et c'est un peu plus lent.

1voto

Tim Robinson Points 28696

Dans .NET 1, lorsque le Add est appelée :

  1. De l'espace est alloué sur le tas ; une nouvelle référence est créée.
  2. Le contenu de la i est copiée dans la référence
  3. Une copie de la référence est placée à la fin de la liste.

Dans .NET 2 :

  1. Une copie de la variable i est transmis à la Add méthode
  2. Une copie de cette copie est mise à la fin de la liste

Oui, le i est toujours copiée (après tout, il s'agit d'un type de valeur, et les types de valeur sont toujours copiés, même s'il s'agit simplement de paramètres de méthode). Mais il n'y a pas de copie redondante faite sur le tas.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X