Я рассматривал реализации .NET Список и ArrayList с Reflector.
При просмотре Добавить (элемент T) я столкнулся с этим. EnsureCapacity (this._size + 1):
public void Add(T item)
{
if (this._size == this._items.Length)
{
this.EnsureCapacity(this._size + 1);
}
this._items[this._size++] = item;
this._version++;
}
Итак, EnsureCapacity выглядит следующим образом:
private void EnsureCapacity(int min)
{
if (this._items.Length < min)
{
int num = (this._items.Length == 0) ? 4 : (this._items.Length * 2);
if (num < min)
{
num = min;
}
this.Capacity = num;
}
}
Почему внутренняя емкость по умолчанию равна 4, а затем увеличивается на кратные 2 (т.е.: double)?
Что касается удвоения, когда требуется изменить размер, это происходит по следующей причине. Скажем, что вы хотите вставить n
элементы в List
. Мы изменим размер списка не более, чем на log n
раз. Поэтому вставка n
элементов в List
будет иметь наихудший случай O(n)
, поэтому вставки будут постоянными в амортизированном времени. Кроме того, количество потерянного пространства ограничено выше на n
. Любая стратегия постоянного пропорционального роста приведет к постоянному амортизированному времени ввода и линейному пространству потерь. Рост быстрее, чем постоянный пропорциональный рост, может привести к более быстрым вставкам, но за счет большего пространства впустую. Рост медленнее, чем постоянный пропорциональный рост, может привести к меньшему расходованию пространства, но за счет более медленных вставок.
По умолчанию емкость такова, что небольшая память теряется впустую (и нет никакого вреда при старте мала, поскольку стратегия удвоения-масштабирования хороша с точки зрения времени/пространства, как мы только что видели).
4 является хорошим дефолтом, поскольку в большинстве коллекций будет только несколько элементов. Приращение выполняется, чтобы гарантировать, что вы не выполняете выделение памяти при каждом добавлении элемента.
Посмотрите эту хорошую статью Джоэла на использование памяти и почему выделение двойного, что вам нужно, является хорошей идеей.
http://www.joelonsoftware.com/printerFriendly/articles/fog0000000319.html
Здесь соответствующая цитата:
Предположим, вы написали функцию smart strcat, которая автоматически перераспределяет целевой буфер. Должен ли он всегда перераспределять его до нужного размера? Мой учитель и наставник Стэн Эйзенстат предполагает, что при вызове realloc вы должны всегда удваивать размер ранее выделенной памяти. Это означает, что вам никогда не придется вызывать realloc больше, чем lg n раз, что имеет достойные характеристики производительности даже для огромных строк, и вы никогда не тратите больше 50% своей памяти.
В качестве альтернативы, список < > и dictionary < > теперь по умолчанию равен 10, но я бы сказал, что они имеют одинаковую природущую логику.
Я уверен, что вы можете создавать небольшие списки без нескольких распределений. Удвоение размера является простотой, а не сложным алгоритмом масштабирования.
Похоже, что 4 - это разумный компромисс между достаточно большими, чтобы содержать частые сценарии наличия 4 предметов или меньше, а также не слишком много потерянных предметов.
Емкость удваивается для каждого увеличения распределения, гарантируя, что она может удерживать удвоенное количество элементов, уже находящихся в контейнере. Это аналогичный алгоритм для векторного контейнера С++.