Бокс и распаковка с дженериками
.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];
Цена бокса (насколько я понимаю) - это необходимость создать объект в куче, скопировать целое число в стеке для нового объекта и наоборот для распаковки.
Как использование дженериков преодолевает это? Означает ли выделенное стеку целое число в стеке и на которое указывает куча (я полагаю, что это не так из-за того, что произойдет, когда оно выйдет из области видимости)? Кажется, все еще есть необходимость скопировать его из стека в другое место.
Что на самом деле происходит?
6 ответов
Когда дело доходит до коллекций, дженерики позволяют избежать упаковки / распаковки, используя фактические T[]
массивы внутри. List<T>
например использует T[]
массив для хранения его содержимого.
Массив, конечно, является ссылочным типом и поэтому (в текущей версии CLR, yada yada) хранится в куче. Но так как это T[]
и не object[]
элементы массива могут храниться "напрямую": то есть они все еще находятся в куче, но они находятся в куче в массиве, а не упакованы и имеют массив, содержащий ссылки на блоки.
Так что для List<int>
Например, то, что у вас будет в массиве, будет "выглядеть" так:
[1 2 3]
Сравните это с ArrayList
, который использует object[]
и поэтому "выглядит" примерно так:
[* a * b * c]
...где *a
и т. д. являются ссылками на объекты (целые числа в штучной упаковке):
* a -> 1 * b -> 2 * с -> 3
Извините за эти грубые иллюстрации; надеюсь, вы понимаете, о чем я.
Ваша путаница является результатом неправильного понимания того, каковы отношения между стеком, кучей и переменными. Вот правильный способ думать об этом.
- Переменная - это место хранения, имеющее тип.
- Время жизни переменной может быть коротким или длинным. Под "коротким" мы подразумеваем "пока текущая функция не вернет или не сгенерирует", а под "длинным" мы подразумеваем "возможно, дольше, чем это".
- Если тип переменной является ссылочным типом, то содержимое переменной является ссылкой на долгоживущее хранилище. Если тип переменной является типом значения, то содержимое переменной является значением.
В качестве детали реализации, место хранения, которое гарантированно будет кратковременным, может быть выделено в стеке. Место хранения, которое может быть долгоживущим, размещается в куче. Обратите внимание, что это ничего не говорит о "типах значений, всегда размещаемых в стеке". Типы значений не всегда размещаются в стеке:
int[] x = new int[10];
x[1] = 123;
x[1]
это место хранения. Это долгоживущий; он может жить дольше, чем этот метод. Поэтому это должно быть в куче. Тот факт, что он содержит int, не имеет значения.
Вы правильно говорите, почему упакованный int стоит дорого:
Ценой бокса является необходимость создания объекта в куче, копирования целого стека, выделенного целым числом, в новый объект и наоборот для распаковки.
Если вы ошибетесь, то скажете: "В стеке выделено целое число". Неважно, где было выделено целое число. Важно то, что в его хранилище содержалось целое число, а не ссылка на местоположение в куче. Цена - это необходимость создать объект и сделать копию; это единственная стоимость, которая имеет отношение к делу.
Так почему общая переменная не стоит дорого? Если у вас есть переменная типа T, а T сконструирован так, чтобы быть int, тогда у вас есть переменная типа int, period. Переменная типа int является местом хранения, и она содержит int. Находится ли это хранилище в стеке или в куче, не имеет значения. Важно то, что место хранения содержит int, а не ссылку на что-то в куче. Поскольку в хранилище содержится int, вам не нужно брать на себя расходы по упаковке и распаковке: выделение нового хранилища в куче и копирование int в новое хранилище.
Это теперь понятно?
Обобщения позволяют набирать внутренний массив списка int[]
вместо эффективно object[]
, что потребует бокса.
Вот что происходит без генериков:
- Ты звонишь
Add(1)
, - Целое число
1
заключен в объект, который требует создания нового объекта в куче. - Этот объект передается
ArrayList.Add()
, - В штучной упаковке объект вставлен в
object[]
,
Здесь есть три уровня косвенности: ArrayList
-> object[]
-> object
-> int
,
С дженериками:
- Ты звонишь
Add(1)
, - Int 1 передается
List<int>.Add()
, - Инт вставлен в
int[]
,
Таким образом, существует только два уровня косвенности: List<int>
-> int[]
-> int
,
Несколько других отличий:
- Не универсальный метод потребует сумму 8 или 12 байтов (один указатель, одно целое) для хранения значения, 4/8 в одном распределении и 4 в другом. И это, вероятно, будет больше из-за выравнивания и заполнения. Общий метод потребует только 4 байта пространства в массиве.
- Неуниверсальный метод требует выделения упакованного целого числа; универсальный метод не. Это быстрее и уменьшает отток ГХ.
- Неуниверсальный метод требует приведений для извлечения значений. Это небезопасно и немного медленнее.
ArrayList обрабатывает только тип object
поэтому для использования этого класса требуется приведение к и от object
, В случае типов значений это приведение включает в себя упаковку и распаковку.
Когда вы используете общий список, компилятор выводит специализированный код для этого типа значения, чтобы фактические значения сохранялись в списке, а не как ссылка на объекты, содержащие значения. Поэтому бокс не требуется.
Цена бокса (насколько я понимаю) - это необходимость создать объект в куче, скопировать целое число в стеке для нового объекта и наоборот для распаковки.
Я думаю, вы предполагаете, что типы значений всегда создаются в стеке. Это не так - они могут быть созданы либо в куче, в стеке или в регистрах. Для получения дополнительной информации об этом см. Статью Эрика Липперта: Правда о типах значений.
В.NET 1, когда Add
метод называется:
- Пространство выделено в куче; новая ссылка сделана
- Содержание
i
переменная копируется в ссылку - Копия ссылки помещается в конец списка
В.NET 2:
- Копия переменной
i
передается вAdd
метод - Копия этой копии помещается в конец списка
Да, i
переменная все еще копируется (в конце концов, это тип значения, а типы значений всегда копируются - даже если они являются просто параметрами метода). Но в куче нет избыточной копии.
Почему ты думаешь с точки зрения WHERE
значения \ объекты хранятся? В C# типы значений могут храниться как в стеке, так и в куче, в зависимости от того, что выбирает CLR.
Где дженерики имеют значение WHAT
хранится в коллекции. В случае ArrayList
коллекция содержит ссылки на объекты в штучной упаковке, где в качестве List<int>
содержит сами значения int.