Почему векторный массив удвоен?

Результаты профиля Greg являются большими для точного сценария, который он покрыл, но интересно, относительные затраты на различные методы изменяются существенно при считании многих различных факторов включая количество типов сравненными, и частотность и любые шаблоны в базовых данных.

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

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

, если я выполняю следующее:

int value = 25124;
if(value == 0) ...
else if (value == 1) ...
else if (value == 2) ...
...
else if (value == 25124) ... 

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

switch(value) {
 case 0:...break;
 case 1:...break;
 case 2:...break;
 ...
 case 25124:...break;
}

выполнит один простой переход к корректному биту кода.

то, Где это становится более сложным в Вашем примере, - то, что Ваш другой метод использует переключатель на строках, а не целых числах, который становится немного более сложным. На низком уровне строки не могут быть включены таким же образом, что целочисленные значения могут так компилятор C# делать некоторое волшебство сделать эту работу для Вас.

, Если оператор переключения является "достаточно небольшим" (то, где компилятор делает то, что это думает, является лучшим автоматически), включение строк генерирует код, который совпадает с если/еще цепочка.

switch(someString) {
    case "Foo": DoFoo(); break;
    case "Bar": DoBar(); break;
    default: DoOther; break;
}

совпадает с:

if(someString == "Foo") {
    DoFoo();
} else if(someString == "Bar") {
    DoBar();
} else {
    DoOther();
}

, Как только список объектов в словаре становится "достаточно большим" компилятор, автоматически создаст внутренний словарь, который отображается от строк в переключателе к целочисленному индексу и затем переключателе на основе того индекса.

Это выглядит примерно так (Просто воображают больше записей, чем я собираюсь потрудиться вводить)

А, статическое поле определяется в "скрытом" месте, которое связано с классом, содержащим оператор переключения типа Dictionary<string, int>, и дано скорректированное имя

//Make sure the dictionary is loaded
if(theDictionary == null) { 
    //This is simplified for clarity, the actual implementation is more complex 
    // in order to ensure thread safety
    theDictionary = new Dictionary<string,int>();
    theDictionary["Foo"] = 0;
    theDictionary["Bar"] = 1;
}

int switchIndex;
if(theDictionary.TryGetValue(someString, out switchIndex)) {
    switch(switchIndex) {
    case 0: DoFoo(); break;
    case 1: DoBar(); break;
    }
} else {
    DoOther();
}

В некоторых быстрых тестах, которые я просто запустил, Если/Еще метод о 3x с такой скоростью, как переключатель для 3 различных типов (где типы случайным образом распределяются). В 25 типах переключатель быстрее маленьким полем (16%) в 50 типах, которые переключатель более двух раз как быстро.

, Если бы Вы собираетесь быть включением большого количества типов, я предложил бы 3-й метод:

private delegate void NodeHandler(ChildNode node);

static Dictionary<RuntimeTypeHandle, NodeHandler> TypeHandleSwitcher = CreateSwitcher();

private static Dictionary<RuntimeTypeHandle, NodeHandler> CreateSwitcher()
{
    var ret = new Dictionary<RuntimeTypeHandle, NodeHandler>();

    ret[typeof(Bob).TypeHandle] = HandleBob;
    ret[typeof(Jill).TypeHandle] = HandleJill;
    ret[typeof(Marko).TypeHandle] = HandleMarko;

    return ret;
}

void HandleChildNode(ChildNode node)
{
    NodeHandler handler;
    if (TaskHandleSwitcher.TryGetValue(Type.GetRuntimeType(node), out handler))
    {
        handler(node);
    }
    else
    {
        //Unexpected type...
    }
}

Это подобно тому, что предложенный Ted Elliot, но использование типа выполнения обрабатывает вместо полных текстовых объектов, избегает издержек загрузки текстового объекта посредством отражения.

Вот некоторые быстрые синхронизации на моей машине:

Testing 3 iterations with 5,000,000 data elements (mode=Random) and 5 types
Method                Time    % of optimal
If/Else               179.67  100.00
TypeHandleDictionary  321.33  178.85
TypeDictionary        377.67  210.20
Switch                492.67  274.21

Testing 3 iterations with 5,000,000 data elements (mode=Random) and 10 types
Method                Time    % of optimal
If/Else               271.33  100.00
TypeHandleDictionary  312.00  114.99
TypeDictionary        374.33  137.96
Switch                490.33  180.71

Testing 3 iterations with 5,000,000 data elements (mode=Random) and 15 types
Method                Time    % of optimal
TypeHandleDictionary  312.00  100.00
If/Else               369.00  118.27
TypeDictionary        371.67  119.12
Switch                491.67  157.59

Testing 3 iterations with 5,000,000 data elements (mode=Random) and 20 types
Method                Time    % of optimal
TypeHandleDictionary  335.33  100.00
TypeDictionary        373.00  111.23
If/Else               462.67  137.97
Switch                490.33  146.22

Testing 3 iterations with 5,000,000 data elements (mode=Random) and 25 types
Method                Time    % of optimal
TypeHandleDictionary  319.33  100.00
TypeDictionary        371.00  116.18
Switch                483.00  151.25
If/Else               562.00  175.99

Testing 3 iterations with 5,000,000 data elements (mode=Random) and 50 types
Method                Time      % of optimal
TypeHandleDictionary  319.67    100.00
TypeDictionary        376.67    117.83
Switch                453.33    141.81
If/Else               1,032.67  323.04

На моей машине, по крайней мере, подход словаря дескриптора типа бьет всех другие для чего-либо более чем 15 различных типов, когда распределение типов, используемых в качестве входа к методу, случайно.

, Если, с другой стороны, вход составлен полностью типа, который проверяется сначала в, если/еще цепочка, что метод очень быстрее:

Testing 3 iterations with 5,000,000 data elements (mode=UniformFirst) and 50 types
Method                Time    % of optimal
If/Else               39.00   100.00
TypeHandleDictionary  317.33  813.68
TypeDictionary        396.00  1,015.38
Switch                403.00  1,033.33

С другой стороны, если вход всегда является последней вещью в, если/еще цепочка, это имеет противоположный эффект:

Testing 3 iterations with 5,000,000 data elements (mode=UniformLast) and 50 types
Method                Time      % of optimal
TypeHandleDictionary  317.67    100.00
Switch                354.33    111.54
TypeDictionary        377.67    118.89
If/Else               1,907.67  600.52

, Если можно сделать некоторые предположения о входе, Вы могли бы получить лучшую производительность от гибридного подхода, где Вы выполняете, если/еще проверки на несколько типов, которые наиболее распространены, и затем отступают к управляемому словарем подходу, если те перестали работать.

18
задан sharptooth 15 September 2009 в 12:08
поделиться

7 ответов

When calculating the average time to insert into a vector, you need to allow for the non-growing inserts and the growing inserts.

Call the total number of operations to insert n items ototal, and the average oaverage.

If you insert n items, and you grow by a factor of A as required, then there are ototal = n + ΣAi [ 0 < i < 1 + lnAn ] operations. In the worst case you use 1/A of the allocated storage.

Intuitively, A = 2 means at worst you have ototal = 2n, so oaverage is O(1), and the worst case you use 50% of the allocated storage.

For a larger A, you have a lower ototal, but more wasted storage.

For a smaller A, ototal is larger, but you don't waste so much storage. As long as it grows geometrically, it's still O(1) amortised insertion time, but the constant will get higher.

For growth factors 1.25 ( red ), 1.5 ( cyan ), 2 ( black ), 3 ( blue ) and 4 ( green ), these graphs show point and average size efficiency ( ratio of size/allocated space; more is better ) on the left and time efficiency ( ratio of insertions / operations; more is better ) on the right for inserting 400,000 items. 100% space efficiency is reached for all growth factors just prior to resizing; the case for A = 2 shows time efficiency between 25% and 50%, and space efficiency about 50%, which is good for most cases:

space and time efficiency graph - C like implementations

For runtimes such as Java, arrays are zero filled, so the number of operations to allocate is proportional to the size of the array. Taking account of this gives reduces the difference between the time efficiency estimates:

space and time efficiency graph - Java like implementations

21
ответ дан 30 November 2019 в 07:39
поделиться

Экспоненциальное удвоение размера массива (или строки) - хороший компромисс между наличием достаточного количества ячеек в массиве и потерей слишком большого количества памяти.

Допустим, мы начинаем с 10 элементов:

1–10
2–20
3–40
4–80
5 - 160

Когда мы утроим размер, мы растем слишком быстро

1 - 10
2–30
3–90
4 - 270
5 - 810

На практике вы вырастете в 10 или 12 раз. Если вы утроите, вы, возможно, сделаете это 7 или 8 раз - время выполнения перераспределения, которое несколько раз достаточно мало, чтобы беспокоиться, но вы, скорее всего, полностью превысите требуемый размер.

4
ответ дан 30 November 2019 в 07:39
поделиться

Если бы вы должны были выделить блок памяти необычного размера, то, когда этот блок будет освобожден (либо из-за того, что вы изменяете его размер, либо из-за того, что он получает GC), возникнет необычный дыра в памяти, которая может вызвать головную боль у диспетчера памяти. Поэтому обычно предпочтительнее распределять память по степени двойки. В некоторых случаях базовый диспетчер памяти будет предоставлять вам блоки только определенных размеров, и если вы запросите странный размер, он будет округлен до следующего большего размера. Поэтому вместо того, чтобы запрашивать 470 единиц, в любом случае вернуть 512, а затем снова изменить размер, когда вы использовали все 470, которые вы просили, с таким же успехом можно просто попросить 512 для начала.

3
ответ дан 30 November 2019 в 07:39
поделиться

Любое кратное - это компромисс. Сделайте его слишком большим, и вы потратите слишком много памяти. Сделайте его слишком маленьким, и вы потратите много времени на перераспределение и копирование. Я предполагаю, что удвоение существует, потому что оно работает и его очень легко реализовать. Я также видел проприетарную STL-подобную библиотеку, которая использует 1,5 в качестве множителя для того же самого - я полагаю, что ее разработчики считали удвоение слишком большого количества памяти.

3
ответ дан 30 November 2019 в 07:39
поделиться

Если вы спрашиваете о Специфичная для Java реализация Vector и ArrayList , то она не обязательно удваивается при каждом расширении.

Из документации Javadoc для Vector:

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

Один из конструкторов вектора позволяет вам указать начальный размер и приращение емкости для вектора. Класс Vector также предоставляет protectCapacity (int minCapacity) и setSize (int newSize) для ручной настройки минимального размера вектора и для изменения размера вектора самостоятельно.

Класс ArrayList очень похож:

Каждый экземпляр ArrayList имеет емкость. Емкость - это размер массива, используемого для хранения элементов в списке. Он всегда не меньше размера списка. По мере добавления элементов в список ArrayList его емкость автоматически увеличивается. Детали политики роста не уточняются, за исключением того факта, что добавление элемента имеет постоянные амортизированные временные затраты.

Приложение может увеличить емкость экземпляра ArrayList перед добавлением большого количества элементов с помощью операции sureCapacity. Это может уменьшить количество инкрементного перераспределения.

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

Это может уменьшить количество инкрементного перераспределения.

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

Это может уменьшить количество инкрементного перераспределения.

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

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

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

2
ответ дан 30 November 2019 в 07:39
поделиться

Лично я считаю, что это произвольный выбор. Мы могли бы использовать базу e вместо базы 2 (вместо удвоения кратного размера на (1 + e).)

Если вы собираетесь добавлять в вектор большое количество переменных, то было бы выгодно иметь высокий base (чтобы уменьшить количество копий, которые вы будете выполнять). С другой стороны, если вам нужно хранить только несколько элементов в avg, тогда подойдет и низкая база, которая сократит количество накладных расходов, следовательно, ускорит процесс.

База 2 - это компромисс.

2
ответ дан 30 November 2019 в 07:39
поделиться

Нет никаких причин для увеличения производительности по сравнению с утроением или учетверением, поскольку все они имеют одинаковые профили производительности большого O. Однако в абсолютном выражении при обычном сценарии удвоение будет иметь более эффективное использование пространства.

-1
ответ дан 30 November 2019 в 07:39
поделиться
Другие вопросы по тегам:

Похожие вопросы: