Результаты профиля 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
, Если можно сделать некоторые предположения о входе, Вы могли бы получить лучшую производительность от гибридного подхода, где Вы выполняете, если/еще проверки на несколько типов, которые наиболее распространены, и затем отступают к управляемому словарем подходу, если те перестали работать.
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:
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:
Экспоненциальное удвоение размера массива (или строки) - хороший компромисс между наличием достаточного количества ячеек в массиве и потерей слишком большого количества памяти.
Допустим, мы начинаем с 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 раз - время выполнения перераспределения, которое несколько раз достаточно мало, чтобы беспокоиться, но вы, скорее всего, полностью превысите требуемый размер.
Если бы вы должны были выделить блок памяти необычного размера, то, когда этот блок будет освобожден (либо из-за того, что вы изменяете его размер, либо из-за того, что он получает GC), возникнет необычный дыра в памяти, которая может вызвать головную боль у диспетчера памяти. Поэтому обычно предпочтительнее распределять память по степени двойки. В некоторых случаях базовый диспетчер памяти будет предоставлять вам блоки только определенных размеров, и если вы запросите странный размер, он будет округлен до следующего большего размера. Поэтому вместо того, чтобы запрашивать 470 единиц, в любом случае вернуть 512, а затем снова изменить размер, когда вы использовали все 470, которые вы просили, с таким же успехом можно просто попросить 512 для начала.
Любое кратное - это компромисс. Сделайте его слишком большим, и вы потратите слишком много памяти. Сделайте его слишком маленьким, и вы потратите много времени на перераспределение и копирование. Я предполагаю, что удвоение существует, потому что оно работает и его очень легко реализовать. Я также видел проприетарную STL-подобную библиотеку, которая использует 1,5 в качестве множителя для того же самого - я полагаю, что ее разработчики считали удвоение слишком большого количества памяти.
Если вы спрашиваете о Специфичная для Java реализация Vector и ArrayList , то она не обязательно удваивается при каждом расширении.
Из документации Javadoc для Vector:
Каждый вектор пытается оптимизировать управление хранилищем с помощью поддержание
емкости
и увеличения емкости. Емкость всегда не меньше размера вектора; обычно он больше, поскольку по мере добавления компонентов к вектору объем памяти вектора увеличивается порциями до размера
capacityIncrement
. Приложение может увеличить емкость вектора перед вставкой большого количества компонентов; это уменьшает количество инкрементного перераспределения.
Один из конструкторов вектора позволяет вам указать начальный размер и приращение емкости для вектора. Класс Vector также предоставляет protectCapacity (int minCapacity)
и setSize (int newSize)
для ручной настройки минимального размера вектора и для изменения размера вектора самостоятельно.
Класс ArrayList очень похож:
Каждый экземпляр
ArrayList
имеет емкость. Емкость - это размер массива, используемого для хранения элементов в списке. Он всегда не меньше размера списка. По мере добавления элементов в список ArrayList его емкость автоматически увеличивается. Детали политики роста не уточняются, за исключением того факта, что добавление элемента имеет постоянные амортизированные временные затраты.Приложение может увеличить емкость экземпляра
ArrayList
перед добавлением большого количества элементов с помощью операции sureCapacity. Это может уменьшить количество инкрементного перераспределения.
Если вы спрашиваете об общей реализации вектора, то выбор увеличения размера и степени является компромиссом. Как правило, векторы поддерживаются массивами. Массивы имеют фиксированный размер. Изменение размера вектора, потому что он заполнен, означает, что вам нужно скопировать все элементы массива в новый, более крупный массив. Если вы сделаете новый массив слишком большим, то у вас будет выделена память, которую вы никогда не будете использовать. Если он слишком мал, копирование элементов из старого массива в новый, более крупный массив может занять слишком много времени - операция, которую вы не хотите выполнять очень часто.
Это может уменьшить количество инкрементного перераспределения.Если вы спрашиваете об общей реализации вектора, то выбор увеличения размера и степени является компромиссом. Как правило, векторы поддерживаются массивами. Массивы имеют фиксированный размер. Изменение размера вектора, поскольку он заполнен, означает, что вам нужно скопировать все элементы массива в новый, более крупный массив. Если вы сделаете новый массив слишком большим, то у вас будет выделенная память, которую вы никогда не будете использовать. Если он слишком мал, копирование элементов из старого массива в новый, более крупный массив может занять слишком много времени - операция, которую вы не хотите выполнять очень часто.
Это может уменьшить количество инкрементного перераспределения.Если вы спрашиваете об общей реализации вектора, то выбор увеличения размера и степени является компромиссом. Как правило, векторы поддерживаются массивами. Массивы имеют фиксированный размер. Изменение размера вектора, потому что он заполнен, означает, что вам нужно скопировать все элементы массива в новый, более крупный массив. Если вы сделаете новый массив слишком большим, то у вас будет выделенная память, которую вы никогда не будете использовать. Если он слишком мал, копирование элементов из старого массива в новый, более крупный массив может занять слишком много времени - операция, которую вы не хотите выполнять очень часто.
Массивы имеют фиксированный размер. Изменение размера вектора, потому что он заполнен, означает, что вам нужно скопировать все элементы массива в новый, более крупный массив. Если вы сделаете новый массив слишком большим, то у вас будет выделена память, которую вы никогда не будете использовать. Если он слишком мал, копирование элементов из старого массива в новый, более крупный массив может занять слишком много времени - операция, которую вы не хотите выполнять очень часто. Массивы имеют фиксированный размер. Изменение размера вектора, поскольку он заполнен, означает, что вам нужно скопировать все элементы массива в новый, более крупный массив. Если вы сделаете новый массив слишком большим, то у вас будет выделена память, которую вы никогда не будете использовать. Если он слишком мал, копирование элементов из старого массива в новый, более крупный массив может занять слишком много времени - операция, которую вы не хотите выполнять очень часто.Лично я считаю, что это произвольный выбор. Мы могли бы использовать базу e вместо базы 2 (вместо удвоения кратного размера на (1 + e).)
Если вы собираетесь добавлять в вектор большое количество переменных, то было бы выгодно иметь высокий base (чтобы уменьшить количество копий, которые вы будете выполнять). С другой стороны, если вам нужно хранить только несколько элементов в avg, тогда подойдет и низкая база, которая сократит количество накладных расходов, следовательно, ускорит процесс.
База 2 - это компромисс.
Нет никаких причин для увеличения производительности по сравнению с утроением или учетверением, поскольку все они имеют одинаковые профили производительности большого O. Однако в абсолютном выражении при обычном сценарии удвоение будет иметь более эффективное использование пространства.