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

Я делал некоторое тестирование производительности, главным образом таким образом, я могу понять различие между итераторами и простой для циклов. Как часть этого я создал простой набор тестов и был тогда полностью удивлен результатами. Для некоторых методов 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 битах.

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

21
задан Nick Randell 21 December 2009 в 20:42
поделиться

5 ответов

Процессоры 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-битного случая); и так как ты

51
ответ дан 29 November 2019 в 06:42
поделиться

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

5
ответ дан 29 November 2019 в 06:42
поделиться

Не уверен, «почему», но я бы обязательно вызвал ваш «метод» хотя бы один раз за пределами цикла таймера, чтобы вы не считали джиттинг в первый раз. (Поскольку мне кажется, что это C #).

1
ответ дан 29 November 2019 в 06:42
поделиться

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

  1. Одна индексная переменная i
  2. Одна результирующая переменная result
  3. Длинный массив результатов.

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

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

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

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

1
ответ дан 29 November 2019 в 06:42
поделиться

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

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

.
0
ответ дан 29 November 2019 в 06:42
поделиться
Другие вопросы по тегам:

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