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

// arr points to the following array: [1,2,4,8,16,32,64]
// assume that it has already been created, so that the
// array creation does not cause a time penalty
int result = 0;
for (int i = 0; i < 7; i++) {
    result += arr[i];
}
int result = 0;
for (int i = 0; i < 7; i++) {
    result += (1 << i);
}

Я почти уверен, что доступ к памяти будет медленнее, но мне нужно подтверждение.

РЕДАКТИРОВАТЬ: чтобы прояснить этот вопрос, я КОНКРЕТНО интересуюсь неоптимизированной версией этого кода. Не потому, что я на самом деле намереваюсь использовать этот код в производстве, а потому, что меня интересует концепция того, быстрее ли доступ к памяти или арифметика. Возможно, мне следовало написать код на языке ассемблера вместо C / C ++, чтобы еще раз прояснить, что меня не интересует оптимизация кода компилятором.

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

Интересующая архитектура - x86 или x86-64 - то, что вы можете найти в современном ноутбуке или настольном компьютере в 2021 году. Я отредактирую этот вопрос еще раз, как только я проведу несколько тестов как для неоптимизированной, так и для оптимизированной версии этого кода. Спасибо всем, кто ответил.

2
Agent 008 24 Фев 2021 в 22:50

3 ответа

Лучший ответ

Я рекомендую вам взглянуть на отличные руководства от Agner Fog, особенно на относящийся к C ++

Оптимизация скорости важна, когда доступ к ЦП и доступ к памяти являются критически важными потребителями времени.

В большинстве современных архитектур переключение (инструкция ALU) будет намного быстрее, чем доступ к памяти, цитируя руководство:

Операции сдвига (1 такт)

на большинстве микропроцессоров операции сдвига занимают только один такт

Доступ к памяти (от 2 до 4 тактовых циклов или хуже)

Доступ к данным из оперативной памяти может занять довольно много времени по сравнению со временем, которое требуется для выполнения вычислений с данными. Это причина того, что все современные компьютеры имеют кеш-память. Обычно кэш данных уровня 1 составляет от 8 до 64 Кбайт, а кэш уровня 2 - от 256 кбайт до 2 Мбайт. Часто также имеется кэш-память 3-го уровня размером в несколько мегабайт.

общий размер всех данных в программе больше, чем кеш уровня 2 и данные разбросаны по памяти или доступны непоследовательным образом, то вполне вероятно, что Доступ к памяти - самый большой потребитель времени в программе. Чтение или письмо переменная в памяти занимает всего 2-4 такта, если она кэширована , но несколько сотен тактов циклы, если он не кэширован. См. Стр. 25 о хранении данных и стр. 89 о памяти. кеширование.

Итак, в вашем конкретном примере вы должны переключиться.

2
Antonin GAVREL 24 Фев 2021 в 20:52

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

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

Пример фрагмента кода устройства Даффа:

void copy_duff(register short *to, register short *from, register  count)
{
    register n=(count+7)/8;
    switch(count%8) {
        case 0:    do {    *to = *from++;
        case 7:        *to = *from++;
        case 6:        *to = *from++;
        case 5:        *to = *from++;
        case 4:        *to = *from++;
        case 3:        *to = *from++;
        case 2:        *to = *from++;
        case 1:        *to = *from++;
        } while(--n>0);
    }
}
1
ryyker 24 Фев 2021 в 21:19

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

*p

против . что из

p + 1

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

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

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

1
John Bollinger 24 Фев 2021 в 20:42