Подтвердить что ты не робот

Бокс и распаковка с дженериками

.NET 1.0 способ создания коллекции целых чисел (например):

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

Отказ от использования этого - отсутствие безопасности и производительности типа из-за бокса и распаковки.

Способ .NET 2.0 - использовать дженерики:

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

Стоимость бокса (по моему мнению) - это необходимость создания объекта в куче, копирование выделенного стека в новый объект и наоборот для распаковки.

Как использование дженериков преодолевает это? Является ли выделенное стекем целое число в стеке и указывается из кучи (думаю, это не так из-за того, что произойдет, когда оно выйдет из сферы)? Похоже, что все еще нужно копировать его где-то еще из стека.

Что действительно происходит?

4b9b3361

Ответ 1

Когда дело доходит до коллекций, дженерики позволяют избежать бокса/распаковки, используя внутренние массивы T[] внутри. List<T>, например, использует массив T[] для хранения его содержимого.

Массив, конечно, является ссылочным типом и поэтому (в текущей версии CLR, yada yada), хранящейся в куче. Но так как это T[], а не object[], элементы массива могут быть сохранены "напрямую": то есть они все еще находятся в куче, но они находятся в куче в массиве, а не в коробке и наличие массива содержит ссылки на поля.

Итак, для a List<int>, например, то, что у вас было бы в массиве, будет выглядеть следующим образом:

[ 1 2 3 ]

Сравните это с ArrayList, который использует object[] и поэтому будет "выглядеть" примерно так:

[ *a *b *c ]

... где *a и т.д. - ссылки на объекты (целые числа):

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

Извините эти грубые иллюстрации; надеюсь, вы знаете, что я имею в виду.

Ответ 2

Ваше замешательство является результатом непонимания того, что отношение между стеком, кучей и переменными. Вот правильный способ подумать об этом.

  • Переменная - это хранилище с типом.
  • Время жизни переменной может быть коротким или длинным. Под "коротким" мы подразумеваем "до тех пор, пока текущая функция не вернется или не будет выбрана", а "длинный" означает "возможно, дольше, чем это".
  • Если тип переменной является ссылочным типом, то содержимое переменной является ссылкой на долговременное хранилище. Если тип переменной является типом значения, то содержимое переменной является значением.

В качестве детали реализации в стеке может быть выделено место хранения, которое гарантированно будет недолговечным. Место хранения, которое может быть долговечным, выделяется в куче. Обратите внимание, что это ничего не говорит о том, что "типы значений всегда выделяются в стеке". Типы значений не всегда выделяются в стеке:

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

x[1] - это место хранения. Он долгоживущий; он может жить дольше, чем этот метод. Поэтому он должен быть в куче. Тот факт, что он содержит int, не имеет значения.

Вы правильно говорите, почему в штучной упаковке стоит дорого:

Цена бокса - это необходимость создания объекта в куче, копирование целочисленного стека в новый объект и наоборот для распаковки.

Если вы ошибетесь, скажите "выделенное стеком целое число". Неважно, где было выделено целое число. Важно то, что его хранилище содержало целое число, а не содержало ссылку на место кучи. Цена - это необходимость создания объекта и копирования; что единственная стоимость, которая имеет значение.

Итак, почему не является общей переменной дорогостоящей? Если у вас есть переменная типа T, а T построена как int, тогда у вас есть переменная типа int, period. Переменная типа int является местом хранения и содержит int. Независимо от того, находится ли это место хранения в стеке, или куча полностью неактуальна. Важно то, что место хранения содержит int, а не содержит ссылку на что-то в куче. Поскольку место хранения содержит int, вам не нужно брать на себя расходы на бокс и распаковку: распределение нового хранилища в куче и копирование int в новое хранилище.

Теперь ясно?

Ответ 3

ArrayList обрабатывает только тип object, поэтому для использования этого класса требуется лить в и из object. В случае типов значений это литье включает бокс и распаковку.

Когда вы используете общий список, компилятор выводит специализированный код для этого типа значений, чтобы фактические значения сохранялись в списке, а не ссылка на объекты, которые содержат значения. Поэтому бокс не требуется.

Стоимость бокса (насколько мне известно) - это необходимость создания объекта в куче, копирование выделенного стека в новый объект и наоборот для распаковки.

Я думаю, вы предполагаете, что типы значений всегда создаются в стеке. Это не так - их можно создать либо в куче, так и в стеке или в регистрах. Для получения дополнительной информации об этом см. Статью Эрика Липперта: Правда о типах значений.

Ответ 4

Generics позволяет набирать внутренний массив списка int[] вместо эффективного object[], который потребует бокса.

Вот что происходит без дженериков:

  • Вы вызываете Add(1).
  • Целое число 1 помещается в объект, который требует создания нового объекта в куче.
  • Этот объект передается ArrayList.Add().
  • Вложенный объект помещается в object[].

Здесь есть три уровня косвенности: ArrayListobject[]objectint.

С generics:

  • Вы вызываете Add(1).
  • int 1 передается List<int>.Add().
  • int помещается в int[].

Итак, существуют только два уровня косвенности: List<int>int[]int.

Несколько других отличий:

  • Необработанный метод потребует сумму 8 или 12 байтов (один указатель, один int) для хранения значения, 4/8 в одном распределении и 4 в другом. И это, вероятно, будет больше из-за выравнивания и заполнения. Общий метод потребует всего 4 байта пространства в массиве.
  • Необработанный метод требует выделения boxed int; общий метод этого не делает. Это быстрее и уменьшает отток GC.
  • Необработанный метод требует отбрасываний для извлечения значений. Это не типично, а немного медленнее.

Ответ 5

В .NET 1, когда вызывается метод Add:

  • Пространство выделяется в куче; новая ссылка сделана
  • Содержимое переменной i копируется в ссылку
  • Копия ссылки помещается в конец списка

В .NET 2:

  • Копия переменной i передается методу Add
  • Копия этой копии помещается в конец списка

Да, переменная i все еще копируется (в конце концов, это тип значения, а типы значений всегда копируются - даже если это только параметры метода). Но в куче нет избыточной копии.

Ответ 6

Почему вы думаете с точки зрения WHERE значения \objects хранятся? В типах значений С# могут храниться в стеке, а также в кучу в зависимости от того, что выбирает CLR.

В тех случаях, когда генерические переменные имеют значение, WHAT хранится в коллекции. В случае ArrayList коллекция содержит ссылки на объекты в коробке, где в качестве List<int> содержатся значения самих значений.