it-swarm-eu.dev

Boxe et déballage avec des génériques

La façon .NET 1.0 de créer une collection d'entiers (par exemple) était:

ArrayList list = new ArrayList();
list.Add(i);          /* boxing   */
int j = (int)list[0]; /* unboxing */

La pénalité d'utilisation est le manque de sécurité et de performances du type dû à la boxe et au déballage.

La manière .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 boxe (à ma connaissance) est la nécessité de créer un objet sur le tas, de copier l'entier alloué de la pile vers le nouvel objet et vice-versa pour le déballage.

Comment l'utilisation des génériques résout-elle cela? Est-ce que l'entier alloué par pile reste sur la pile et est pointé du tas (je suppose que ce n'est pas le cas à cause de ce qui se passera quand il sortira du champ d'application)? Il semble qu'il soit encore nécessaire de le copier ailleurs dans la pile.

Qu'est-ce qui se passe réellement?

64
Itay Karo

En ce qui concerne les collections, les génériques permettent d'éviter la boxe/unboxing en utilisant des tableaux T[] Réels en interne. List<T> Utilise par exemple un tableau T[] Pour stocker son contenu.

Le tablea, bien sûr, est un type de référence et est donc (dans la version actuelle du CLR, yada yada) stocké sur le tas. Mais comme c'est 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 tablea au lieu d'être encadré 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 qui "ressemblerait" donc à quelque chose comme ceci:

 [* a * b * c] 

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

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

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

64
Dan Tao

Votre confusion est le résultat d'une mauvaise compréhension de la relation entre la pile, le tas et les variables. Voici la bonne façon 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 "court", nous entendons "jusqu'à ce que la fonction actuelle retourne ou lance" et par "long", nous entendons "peut-être plus long que cela".
  • Si le type d'une variable est un type de référence, 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 dont la durée de vie est garantie peut être alloué sur la pile. Un emplacement de stockage qui peut être de longue durée 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 ne sont pas toujours alloués sur la pile:

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

x[1] est un emplacement de stockage. C'est une vie longue; il pourrait vivre plus longtemps que cette méthode. Il doit donc être sur le tas. Le fait qu'il contienne un int est sans importance.

Vous dites correctement pourquoi un int en boîte coûte cher:

Le prix de la boxe est la nécessité de créer un objet sur le tas, de copier l'entier alloué de la pile vers le nouvel objet et vice-versa pour le déballage.

Là où vous vous trompez, c'est "l'entier alloué par pile". Peu importe où l'entier a été alloué. Ce qui importe, c'est que son stockage contenait l'entier , au lieu de contenir une référence à un emplacement de tas . Le prix est la nécessité de créer l'objet et de faire la copie; c'est le seul coût qui soit 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 contient un int. Que cet emplacement de stockage soit sur la pile ou que le tas soit complètement hors de propos. Ce qui est pertinent, c'est que l'emplacement de stockage contient un int , au lieu de contenir une référence à quelque chose sur le tas . Étant donné que l'emplacement de stockage contient un int, vous n'avez pas à assumer les coûts de boxing et unboxing: allouer un nouveau stockage sur le tas et copier l'int dans le nouveau stockage.

Est-ce maintenant clair?

63
Eric Lippert

Un ArrayList ne gère que le type object, donc pour utiliser cette classe, il faut effectuer un cast vers et depuis object. Dans le cas des types de valeur, ce casting implique la boxe et le déballage.

Lorsque vous utilisez une liste générique, le compilateur génère un code spécialisé pour ce type de valeur afin que les valeurs réelles soient stockées dans la liste plutôt qu'une référence aux objets qui contiennent les valeurs. Aucune boxe n'est donc requise.

Le prix de la boxe (à ma connaissance) est la nécessité de créer un objet sur le tas, de copier l'entier alloué de la pile vers le nouvel objet et vice-versa pour le déballage.

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 sur le tas, sur la pile ou dans des registres. Pour plus d'informations à ce sujet, veuillez consulter l'article d'Eric Lippert: The Truth About Value Types .

3
Mark Byers

Les génériques permettent de taper le tableau interne de la liste int[] Au lieu de object[], Ce qui nécessiterait de la boxe.

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

  1. Vous appelez Add(1).
  2. L'entier 1 Est encadré dans un objet, ce qui nécessite qu'un nouvel objet soit construit sur le tas.
  3. Cet objet est passé à ArrayList.Add().
  4. L'objet encadré est placé dans un object[].

Il existe 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 bourré 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 cela sera probablement plus dû à l'alignement et au rembourrage. 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 entier encadré; la méthode générique ne le fait pas. Ceci est plus rapide et réduit le taux de désabonnement du GC.
  • La méthode non générique nécessite des transtypages pour extraire des valeurs. Ce n'est pas typé et c'est un peu plus lent.
3
cdhowie

Dans .NET 1, lorsque la méthode Add est appelée:

  1. L'espace est alloué sur le tas; une nouvelle référence est faite
  2. Le contenu de la variable i est copié dans la référence
  3. Une copie de la référence est placée en fin de liste

Dans .NET 2:

  1. Une copie de la variable i est passée à la méthode Add
  2. Une copie de cette copie est placée à la fin de la liste

Oui, la variable i est toujours copiée (après tout, c'est un type de valeur, et les types de valeur sont toujours copiés - même si ce ne sont que des paramètres de méthode). Mais il n'y a pas de copie redondante faite sur le tas.

1
Tim Robinson

Pourquoi pensez-vous en termes de WHERE les valeurs\objets sont stockés? En C #, les types de valeurs peuvent être stockés sur la pile ainsi que sur le tas en fonction de ce que le CLR choisit.

Lorsque les génériques font la différence, WHAT est stocké dans la collection. Dans le cas de ArrayList, la collection contient des références à des objets encadrés alors que List<int> contient les valeurs int elles-mêmes.

1
Unmesh Kondolikar