#c #arrays #loops #optimization #heap-memory
#c #массивы #циклы #оптимизация #куча-память
Вопрос:
Итак, недавно я столкнулся с проблемой, которая показалась мне интересной, и я не мог полностью объяснить. Я выделил природу проблемы в следующем коде:
#include <cstring>
#include <chrono>
#include <iostream>
#define NLOOPS 10
void doWorkFast(int total, int *write, int *read)
{
for (int j = 0; j < NLOOPS; j ) {
for (int i = 0; i < total; i ) {
write[i] = read[i] i;
}
}
}
void doWorkSlow(int total, int *write, int *read, int innerLoopSize)
{
for (int i = 0; i < NLOOPS; i ) {
for (int j = 0; j < total/innerLoopSize; j ) {
for (int k = 0; k < innerLoopSize; k ) {
write[j*k k] = read[j*k k] j*k k;
}
}
}
}
int main(int argc, char *argv[])
{
int n = 1000000000;
int *heapMemoryWrite = new int[n];
int *heapMemoryRead = new int[n];
for (int i = 0; i < n; i )
{
heapMemoryRead[i] = 1;
}
std::memset(heapMemoryWrite, 0, n * sizeof(int));
auto start1 = std::chrono::high_resolution_clock::now();
doWorkFast(n,heapMemoryWrite, heapMemoryRead);
auto finish1 = std::chrono::high_resolution_clock::now();
auto duration1 = std::chrono::duration_cast<std::chrono::microseconds>(finish1 - start1);
for (int i = 0; i < n; i )
{
heapMemoryRead[i] = 1;
}
std::memset(heapMemoryWrite, 0, n * sizeof(int));
auto start2 = std::chrono::high_resolution_clock::now();
doWorkSlow(n,heapMemoryWrite, heapMemoryRead, 10);
auto finish2 = std::chrono::high_resolution_clock::now();
auto duration2 = std::chrono::duration_cast<std::chrono::microseconds>(finish2 - start2);
std::cout << "Small inner loop:" << duration1.count() << " microseconds.n" <<
"Large inner loop:" << duration2.count() << " microseconds." << std::endl;
delete[] heapMemoryWrite;
delete[] heapMemoryRead;
}
Глядя на две функции DoWork *, для каждой итерации мы считываем одни и те же адреса, добавляем одно и то же значение и записываем на одни и те же адреса. Я понимаю, что в doWorkSlow
реализации мы выполняем еще одну или две операции для разрешения j*k k
, однако я думаю, что разумно предположить, что относительно времени, необходимого для загрузки / сохранения для чтения и записи в память, вклад во время этих операций незначителен.
Тем не менее, doWorkSlow
это занимает примерно в два раза больше времени (46,8 с) по сравнению с doWorkFast
(25,5 с) на моем i7-3700 с использованием g --version
версии 7.5.0. Хотя на ум приходят такие вещи, как предварительная выборка кэша и прогнозирование ветвлений, у меня нет хорошего объяснения, почему doWorkFast
это намного быстрее, чем doWorkSlow
. У кого-нибудь есть понимание?
Спасибо
Комментарии:
1. Каковы ваши флаги компиляции?
2. Я не использую никаких флагов, хотя, когда я компилировал с -o3 -o2 и -o1, у меня были похожие результаты (т. Е. Медленное время ~ 2 раза).).
3. Я предполагаю, что разница заключается в том,
doWorkFast
что доступ к массиву осуществляется, обращается к элементам массива один за другим (увеличивается на 1), ноdoWorkSlow
увеличивает индексы наj 1
, что затрудняет предварительную сборку кэша.4. Без оптимизации вы могли бы рассчитывать
total/innerLoopSize
каждую итерацию. То же самое касаетсяj*k k
. Попробуйте вычислитьj*k k
один раз и сохранить во временной (постоянной) переменной передwrite
оператором, напримерconst int index = j * k k; write[index] = ...;
Ответ №1:
Глядя на две функции DoWork *, для каждой итерации мы считываем одни и те же адреса, добавляем одно и то же значение и записываем на одни и те же адреса.
Это неправда!
В doWorkFast
вы индексируете каждое целое число постепенно, как array[i]
.
array[0]
array[1]
array[2]
array[3]
В doWorkSlow
вы индексируете каждое целое число как array[j*k k]
, которое перескакивает и повторяется.
j
Например, когда равно 10, и вы выполняете итерацию k
с 0 и далее, вы получаете доступ
array[0] // 10*0 0
array[11] // 10*1 1
array[22] // 10*2 2
array[33] // 10*3 3
Это не позволит вашему оптимизатору использовать инструкции, которые могут работать со многими соседними целыми числами одновременно.
Комментарии:
1. Потрясающе! Спасибо, что указали на это. В этом примере кодирование индексов таким образом было недостатком реализации (должно было быть
j*innerLoopSize k
, однако, при решении проблемы я увидел, что время выполнения было аналогичным. Однако главный урок, который я вижу, заключается в том, что мы должны выполнять непрерывный доступ к памяти, когда это возможно, хотя для использования SIMD?