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

.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[], что потребует бокса.

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

  1. Ты звонишь Add(1),
  2. Целое число 1 заключен в объект, который требует создания нового объекта в куче.
  3. Этот объект передается ArrayList.Add(),
  4. В штучной упаковке объект вставлен в object[],

Здесь есть три уровня косвенности: ArrayList -> object[] -> object -> int,

С дженериками:

  1. Ты звонишь Add(1),
  2. Int 1 передается List<int>.Add(),
  3. Инт вставлен в int[],

Таким образом, существует только два уровня косвенности: List<int> -> int[] -> int,

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

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

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

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

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

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

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

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

В.NET 2:

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

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

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

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

Другие вопросы по тегам