Бокс и распаковка с дженериками
.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];
Стоимость бокса (по моему мнению) - это необходимость создания объекта в куче, копирование выделенного стека в новый объект и наоборот для распаковки.
Как использование дженериков преодолевает это? Является ли выделенное стекем целое число в стеке и указывается из кучи (думаю, это не так из-за того, что произойдет, когда оно выйдет из сферы)? Похоже, что все еще нужно копировать его где-то еще из стека.
Что действительно происходит?
Ответы
Ответ 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[]
.
Здесь есть три уровня косвенности: ArrayList
→ object[]
→ object
→ int
.
С 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>
содержатся значения самих значений.