Почему моя программа работает медленно при обходе ровно 8192 элементов?
Описание проблемы:
У меня есть программа, которая работает с матрицей img[][]
размером SIZE×SIZE и инициализирует ее следующим образом:
img[j][i] = 2 * j + i
Затем я создаю матрицу res[][]
, где каждое поле представляет собой среднее значение 9 полей вокруг него из матрицы img
. Для упрощения границы матрицы оставлены равными 0.
Вот соответствующий участок кода:
for(i=1;i<SIZE-1;i++)
for(j=1;j<SIZE-1;j++) {
res[j][i]=0;
for(k=-1;k<2;k++)
for(l=-1;l<2;l++)
res[j][i] += img[j+l][i+k];
res[j][i] /= 9;
}
Это вся логика программы. Для полноты картины, вот что идет перед этим кодом, последующего кода нет. Как видно, это только инициализация:
#define SIZE 8192
float img[SIZE][SIZE]; // входное изображение
float res[SIZE][SIZE]; // результат применения среднего фильтра
int i,j,k,l;
for(i=0;i<SIZE;i++)
for(j=0;j<SIZE;j++)
img[j][i] = (2*j+i)%8196;
Проблема заключается в том, что программа работает медленно, когда SIZE является кратным 2048. Например, времена выполнения следующие:
SIZE = 8191: 3.44 сек
SIZE = 8192: 7.20 сек
SIZE = 8193: 3.18 сек
Компилятор — GCC. Из того, что я знаю, это может быть связано с управлением памятью, но я не очень разбираюсь в этой теме, поэтому и задаю вопрос здесь.
Будет полезно узнать, как это исправить, но если кто-то сможет объяснить, почему времена выполнения так отличаются, я буду достаточно благодарен.
Я уже знаю о malloc/free
, но проблема не в объеме используемой памяти, а именно во времени выполнения, и я не совсем понимаю, как это может помочь.
1 ответ(ов)
Вопрос был о производительности обработки изображений, и ниже представлены результаты тестов, проведённых с компилятором Visual C++, который используется в стандартной установке Qt Creator (предполагаю, без флагов оптимизации). При использовании GCC значительной разницы между версией Mystical и моим "оптимизированным" кодом не обнаружено. Вывод таков: оптимизации компилятора лучше справляются с микрооптимизациями, чем люди (по крайней мере, чем я). Остальную часть ответа оставляю для справки.
Избытком памяти не следует обрабатывать изображения подобным образом. Лучше использовать одномерные массивы. Обработка всех пикселей выполняется в одном цикле. Доступ к точкам можно осуществлять следующим образом:
pointer + (x + y*width)*(sizeOfOnePixel)
В данном случае лучше сначала вычислить и закэшировать сумму трёх групп пикселей по горизонтали, так как они используются трижды.
Я провёл несколько тестов и считаю, что стоит поделиться результатами. Каждый результат — это среднее значение пяти тестов.
Исходный код от пользователя user1615209:
8193: 4392 мс
8192: 9570 мс
Версия Mystical:
8193: 2393 мс
8192: 2190 мс
Два прохода с использованием одномерного массива: первый проход — для горизонтальных сумм, второй — для вертикальной суммы и среднего значения. Два прохода с использованием трёх указателей и только инкрементов, как это:
imgPointer1 = &avg1[0][0];
imgPointer2 = &avg1[0][SIZE];
imgPointer3 = &avg1[0][SIZE+SIZE];
for(i=SIZE;i<totalSize-SIZE;i++){
resPointer[i]=(*(imgPointer1++)+*(imgPointer2++)+*(imgPointer3++))/9;
}
Результаты:
8193: 938 мс
8192: 974 мс
Два прохода с использованием одномерного массива и адресацией следующим образом:
for(i=SIZE;i<totalSize-SIZE;i++){
resPointer[i]=(hsumPointer[i-SIZE]+hsumPointer[i]+hsumPointer[i+SIZE])/9;
}
Результаты:
8193: 932 мс
8192: 925 мс
Один проход, кэшируя горизонтальные суммы только на одну строку вперёд, чтобы они оставались в кэше:
// Горизонтальные суммы для первых двух строк
for(i=1;i<SIZE*2;i++){
hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
}
// Остальная часть вычислений
for(;i<totalSize;i++){
// Вычисление горизонтальной суммы для следующей строки
hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
// Финальный результат
resPointer[i-SIZE]=(hsumPointer[i-SIZE-SIZE]+hsumPointer[i-SIZE]+hsumPointer[i])/9;
}
Результаты:
8193: 599 мс
8192: 652 мс
Выводы:
- Нет преимуществ от использования нескольких указателей и только инкрементов (я думал, что будет быстрее).
- Кэширование горизонтальных сумм лучше, чем их вычисление несколько раз.
- Два прохода не в три раза быстрее, а всего лишь в два раза.
- Можно добиться увеличения скорости в 3.6 раза, используя и однопроходный алгоритм, и кэширование промежуточного результата.
Уверен, что можно добиться ещё больших результатов.
ПРИМЕЧАНИЕ
Обратите внимание, что я написал этот ответ, ориентируясь на общие вопросы производительности, а не на проблему кэша, объясненную в отличном ответе Mystical. В начале это был просто псевдокод. Меня попросили провести тесты в комментариях... Вот полностью переработанная версия с тестами.
Является ли < быстрее, чем <=?
Почему замена 0.1f на 0 замедляет производительность в 10 раз?
В чём разница между g++ и gcc?
Можно ли получить доступ к памяти локальной переменной вне её области видимости?
Что такое "кэш-дружественный" код?