8

Почему моя программа работает медленно при обходе ровно 8192 элементов?

1

Описание проблемы:

У меня есть программа, которая работает с матрицей 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 ответ(ов)

0

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

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь