Я делал некоторое тестирование производительности, главным образом таким образом, я могу понять различие между итераторами и простой для циклов. Как часть этого я создал простой набор тестов и был тогда полностью удивлен результатами. Для некоторых методов 64 бита были почти в 10 раз быстрее, чем 32 бита.
То, что я ищу, является некоторым объяснением того, почему это происходит.
[Ответ ниже указывает, что это происходит из-за арифметики на 64 бита в приложении на 32 бита. Изменение longs к ints приводит к хорошей производительности в системах на 32 и 64 бита.]
Вот эти 3 рассматриваемых метода.
private static long ForSumArray(long[] array)
{
var result = 0L;
for (var i = 0L; i < array.LongLength; i++)
{
result += array[i];
}
return result;
}
private static long ForSumArray2(long[] array)
{
var length = array.LongLength;
var result = 0L;
for (var i = 0L; i < length; i++)
{
result += array[i];
}
return result;
}
private static long IterSumArray(long[] array)
{
var result = 0L;
foreach (var entry in array)
{
result += entry;
}
return result;
}
У меня есть простая тестовая обвязка, которая тестирует это
var repeat = 10000;
var arrayLength = 100000;
var array = new long[arrayLength];
for (var i = 0; i < arrayLength; i++)
{
array[i] = i;
}
Console.WriteLine("For: {0}", AverageRunTime(repeat, () => ForSumArray(array)));
repeat = 100000;
Console.WriteLine("For2: {0}", AverageRunTime(repeat, () => ForSumArray2(array)));
Console.WriteLine("Iter: {0}", AverageRunTime(repeat, () => IterSumArray(array)));
private static TimeSpan AverageRunTime(int count, Action method)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < count; i++)
{
method();
}
stopwatch.Stop();
var average = stopwatch.Elapsed.Ticks / count;
return new TimeSpan(average);
}
Когда я выполняю их, я получаю следующие результаты:
32 бита:
For: 00:00:00.0006080 For2: 00:00:00.0005694 Iter: 00:00:00.0001717
64 бита
For: 00:00:00.0007421 For2: 00:00:00.0000814 Iter: 00:00:00.0000818
Вещи, которые я считал из этого, состоят в том, что использование LongLength является медленным. Если я использую массив. Длина, производительность для первого для цикла довольно хороша в 64 битах, но не 32 битах.
Другая вещь, которую я считал из этого, состоит в том, что итерация по массиву так же эффективна как для цикла, и код является намного более чистым и легче читать!
Процессоры x64 содержат 64-битные регистры общего назначения, с помощью которых они могут вычислять операции с 64-битными целыми числами в одной инструкции. В 32-битных процессорах этого нет. Это особенно актуально для вашей программы, поскольку в ней широко используются длинные
(64-битные целые) переменные.
Например, в сборке x64, чтобы добавить пару 64-битных целых чисел, хранящихся в регистрах, вы можете просто do:
; adds rbx to rax
add rax, rbx
Чтобы выполнить ту же операцию на 32-битном процессоре x86, вам придется использовать два регистра и вручную использовать перенос первой операции во второй операции:
; adds ecx:ebx to edx:eax
add eax, ebx
adc edx, ecx
Чем больше инструкций, тем меньше регистров, тем больше часов циклы, выборки из памяти, ... что в конечном итоге приведет к снижению производительности. Разница очень заметна в приложениях для обработки числа.
Для приложений .NET, кажется, что 64-битный JIT-компилятор выполняет более агрессивную оптимизацию, улучшая общую производительность.
Что касается вашей точки зрения об итерации массива, компилятор C # достаточно умен, чтобы распознавать foreach
над массивами и обрабатывать их специально. Сгенерированный код идентичен использованию цикла for
, и рекомендуется использовать foreach
, если вам не нужно изменять элемент массива в цикле. Кроме того, среда выполнения распознает шаблон для (int i = 0; i
LongLength
и приведет к снижению производительности (как для 32-битного, так и для 64-битного случая); и так как ты
Длинный тип данных - 64-битный, и в 64-битном процессе он обрабатывается как единая единица собственной длины. В 32-битном процессе он рассматривается как 2 32-битных блока. Математика, особенно для этих "сплит-типов", потребует много ресурсов процессора.
Не уверен, «почему», но я бы обязательно вызвал ваш «метод» хотя бы один раз за пределами цикла таймера, чтобы вы не считали джиттинг в первый раз. (Поскольку мне кажется, что это C #).
О, это просто. Я предполагаю, что вы используете технологию x86. Что вам нужно для выполнения циклов в ассемблере?
Итак, вам нужны три переменные. Доступ к переменным будет самым быстрым, если вы можете хранить их в регистрах; если вам нужно перемещать их в память и обратно, вы теряете скорость. Для 64-битных длин вам нужны два регистра на 32-битных, и у нас есть только четыре регистра, поэтому велики шансы, что все переменные не могут быть сохранены в регистрах, а должны храниться в промежуточном хранилище, таком как стек. Уже одно это значительно замедлит доступ.
Добавление чисел: Сложение необходимо два раза; первый раз без бита переноса и второй раз с битом переноса. 64-битная версия может работать за один цикл.
Перемещение / загрузка: Для каждой 1-тактной 64-битной переменной необходимо два цикла для 32-битной загрузки / выгрузки длинного целого числа в память.
Каждый тип данных компонента (типы данных, которые состоят из большего количества бит, чем бит регистра / адреса) потеряет значительную скорость. Увеличение скорости на порядок является причиной того, что графические процессоры по-прежнему предпочитают числа с плавающей запятой (32 бит) вместо двойных (64 бит).
Как говорили другие, выполнение 64-битной арифметики на 32-битной машине потребует дополнительных манипуляций, более того - умножения или деления.
Возвращаясь к вашему беспокойству об итераторах в сравнении с простыми для циклов, итераторы могут иметь довольно сложные определения, и они будут быстрыми только в том случае, если встраивание и оптимизация компилятора способны заменить их на эквивалентную простую форму. Это действительно зависит от типа итератора и реализации лежащего в его основе контейнера. Самый простой способ определить, была ли оптимизация достаточно хорошо сгенерирована - это изучить сгенерированный ассемблерный код. Другой способ - поместить его в длительный цикл, поставить его на паузу и посмотреть на стек, что он делает.
.