Подтвердить что ты не робот

Почему это происходит быстрее на 64 бит, чем на 32 бит?

Я выполнял некоторые тесты производительности, в основном, поэтому я могу понять разницу между итераторами и простыми для циклов. В рамках этого я создал простой набор тестов и был полностью удивлен результатами. Для некоторых методов бит 64 бит был почти в 10 раз быстрее, чем 32 бит.

То, что я ищу, является некоторым объяснением того, почему это происходит.

[Следующий ответ говорит, что это связано с 64-разрядной арифметикой в ​​32-битном приложении. Изменение longs to int приводит к хорошей производительности в 32 и 64-битных системах.]

Вот три метода, о которых идет речь.

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 происходит медленно. Если я использую array.Length, производительность для первого цикла цикла довольно хороша в 64-битной, но не 32-разрядной версии.

Другое, что я прочитал из этого, состоит в том, что итерация по массиву столь же эффективна, как и цикл for, а код намного чище и легче читать!

4b9b3361

Ответ 1

Процессоры

x64 содержат 64-битные регистры общего назначения, с помощью которых они могут вычислять операции с 64-битными целыми числами в одной команде. У 32-битных процессоров этого нет. Это особенно актуально для вашей программы, поскольку она сильно использует переменные long (64-разрядные целые).

Например, в сборке x64 для добавления пары 64-битных целых чисел, хранящихся в регистрах, вы можете просто сделать:

; adds rbx to rax
add rax, rbx

Чтобы выполнить ту же операцию на 32-битном процессоре x86, вам придется использовать два регистра и вручную использовать перенос первой операции во второй операции:

; adds ecx:ebx to edx:eax
add eax, ebx
adc edx, ecx

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

Для приложений .NET кажется, что 64-битный JIT-компилятор выполняет более агрессивные оптимизации, улучшая общую производительность.

Что касается вашей точки вокруг итерации массива, компилятор С# достаточно умен, чтобы распознавать foreach по массивам и обрабатывать их специально. Сгенерированный код идентичен использованию цикла for, и в нем рекомендуется использовать foreach, если вам не нужно изменять элемент массива в цикле. Кроме того, среда выполнения распознает шаблон for (int i = 0; i < a.Length; ++i) и пропускает связанные проверки для доступа к массиву внутри цикла. Это не произойдет в случае LongLength и приведет к снижению производительности (как для 32-битного, так и для 64-битного случая); и поскольку вы будете использовать переменные long с LongLength, 32-разрядная производительность будет еще более ухудшена.

Ответ 2

Длинный тип данных - 64-битный, а в 64-битном режиме он обрабатывается как единое целое. В 32-битном процессе он рассматривается как 2 32-битных блока. Математика, особенно на этих "разделенных" типах, будет иметь интенсивный процессор.

Ответ 3

Не уверен в том, "почему", но я обязательно позову ваш "метод", по крайней мере, один раз за пределами таймера, чтобы вы не считали одноразовый удар. (Так как это выглядит как С# для меня).

Ответ 4

О, это легко. Я предполагаю, что вы используете технологию x86. Что вам нужно для выполнения циклов в ассемблере?

  • Одна индексная переменная i
  • Результат результата результата
  • Длинный массив результатов.

Итак, вам нужны три переменные. Переменный доступ выполняется быстрее всего, если вы можете хранить их в реестрах; если вам нужно переместить их в память, вы теряете скорость. Для 64-битных длин вам нужны два регистра на 32 бит, и у нас есть только четыре регистра, поэтому шансы высоки, что все переменные не могут быть сохранены в регистрах, но должны храниться в промежуточном хранилище, таком как стек. Это само по себе значительно замедлит доступ.

Добавление чисел: Дополнение должно быть два раза; первый раз без бита переноса и второй раз с битом переноса. 64bit может это сделать за один цикл.

Перемещение/Загрузка: Для каждого 1-битного 64-битного var вам нужно два цикла для 32 бит для загрузки/выгрузки длинного целого в память.

Каждый компонентный тип данных (типы данных, которые состоят из большего количества бит, чем биты регистра/адреса) потеряет значительную скорость. Прирост скорости на порядок - причина, по которой GPU по-прежнему предпочитают поплавки (32 бит) вместо удваивания (64 бит).

Ответ 5

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

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