Чому моя програма повільна під час перегляду певних 8192 елементів?


755

Ось витяг із відповідної програми. Матриця 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]; // input image
float res[SIZE][SIZE]; //result of mean filter
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 secs
SIZE = 8192: 7.20 secs
SIZE = 8193: 3.18 secs

Компілятором є GCC. З того, що я знаю, це через управління пам’яттю, але я насправді не знаю надто багато про цю тему, тому я тут прошу.

Також, як це виправити було б непогано, але якби хтось міг пояснити ці строки виконання, я вже був би досить задоволений.

Я вже знаю malloc / free, але проблема полягає не в кількості використовуваної пам’яті, це просто час виконання, тому я не знаю, як це допоможе.


67
@bokan це буває, коли розмір кратний критичному кроку кешу.
Лучіан Григоре

5
@ Містичний, це не має значення, він виставляє таку саму точну проблему; код може бути різним, але в основному обидва питання задаються приблизно в один і той же час (і їхні заголовки, безумовно, схожі).
Griwes

33
Ви не повинні обробляти зображення за допомогою двовимірного масиву, якщо ви хочете отримати високу продуктивність. Розглянемо, що всі пікселі знаходяться в сирому вигляді та обробляють їх як масив одного виміру. Робіть це розмиття за два проходи. Спочатку додайте значення оточуючих пікселів за допомогою ковзної суми в 3 пікселі: slideSum + = src [i + 1] -src [i-1]; dest [i] = slideSum;. Потім зробіть те саме по вертикалі і розділіть одночасно: dest [i] = (src [i-width] + src [i] + src [i + width]) / 9. www-personal.engin.umd.umich.edu/~jwvm/ece581/18_RankedF.pdf
bokan

8
Тут насправді відбувається дві речі. Це не просто суперрівнювання.
Містичний

7
(Лише незначна нитка на вашу відповідь. Для першого сегменту коду було б добре, якби всі ваші петлі мали дужки.)
Тревор Бойд Сміт

Відповіді:


954

Відмінність викликана тим самим питанням про суперрівнювання від наступних пов'язаних питань:

Але це лише тому, що є ще одна проблема з кодом.

Починаючи з початкового циклу:

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;
}

Спочатку зауважте, що дві внутрішні петлі тривіальні. Їх можна розкрутити наступним чином:

for(i=1;i<SIZE-1;i++) {
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

Так що залишаються дві зовнішні петлі, які нас цікавлять.

Тепер ми бачимо, що в цьому питанні проблема однакова: Чому порядок циклів впливає на продуктивність при ітерації над 2D масивом?

Ви повторюєте матрицю стовпців замість рядка.


Щоб вирішити цю проблему, слід обмінятися двома петлями.

for(j=1;j<SIZE-1;j++) {
    for(i=1;i<SIZE-1;i++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

Це повністю виключає весь непослідовний доступ, щоб ви більше не отримували випадкових сповільнень на великих потужностях-двох.


Core i7 920 при 3,5 ГГц

Оригінальний код:

8191: 1.499 seconds
8192: 2.122 seconds
8193: 1.582 seconds

Змінені зовнішні петлі:

8191: 0.376 seconds
8192: 0.357 seconds
8193: 0.351 seconds

217
Я також зазначу, що розгортання внутрішніх циклів не впливає на продуктивність. Компілятор, ймовірно, робить це автоматично. Я розкрутив їх з єдиною метою позбутися від них, щоб полегшити виявлення проблеми із зовнішніми петлями.
Містичний

29
І ви можете прискорити цей код ще на три коефіцієнти, кешируючи суми вздовж кожного рядка. Але та та інші оптимізації виходять за рамки початкового питання.
Eric Postpischil

33
@ClickUpvote Це фактично апаратне (кешування) питання. Це не має нічого спільного з мовою. Якби ви спробували його будь-якою іншою мовою, яка компілює або JIT в рідний код, ви, ймовірно, побачите ті самі ефекти.
Містичний

19
@ClickUpvote: Ви виглядаєте досить помилково. Ця "друга петля" була просто містичним розгортанням внутрішніх петель вручну. Це щось, що ваш компілятор майже напевно зробить, і Mystical лише зробив це, щоб зробити проблему із зовнішніми петлями більш очевидною. Це аж ніяк не те, що вам слід заважати робити самому.
Лілі Баллард

154
ЦЕ є прекрасним прикладом гарної відповіді на SO: Посилається на подібні запитання, покроково пояснює, як ви підійшли до нього, пояснює проблему, пояснює, як виправити проблему, має чудове форматування і навіть приклад запущеного коду на вашій машині. Дякую за ваш внесок.
MattSayar

57

Наступні тести були зроблені з компілятором Visual C ++, оскільки він використовується за допомогою програми Qt Creator за замовчуванням (я думаю, що немає прапор оптимізації). Під час використання GCC немає великої різниці між версією Mystical і моїм "оптимізованим" кодом. Отже, висновок полягає в тому, що оптимізація компілятора краще піклується про мікрооптимізацію, ніж люди (нарешті я). Решту своєї відповіді я залишаю для довідки.


Обробляти зображення таким чином не ефективно. Краще використовувати одномірні масиви. Обробка всіх пікселів виконується в один цикл. Випадковий доступ до пунктів можна зробити за допомогою:

pointer + (x + y*width)*(sizeOfOnePixel)

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

Я зробив кілька тестів і, думаю, варто їх поділитися. Кожен результат - це в середньому п’ять тестів.

Оригінальний код користувача1615209:

8193: 4392 ms
8192: 9570 ms

Версія Mystical:

8193: 2393 ms
8192: 2190 ms

Два пропуски за допомогою 1D масиву: перший пропуск для горизонтальних сум, другий для вертикальної суми та середній. Адреса з двома проходами з трьома вказівниками та лише з таким кроком:

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 ms
8192: 974 ms

Два проходи, використовуючи 1D масив і адресуючи так:

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(hsumPointer[i-SIZE]+hsumPointer[i]+hsumPointer[i+SIZE])/9;
}

8193: 932 ms
8192: 925 ms

Один прохід кешування горизонтальними сумами лише на один ряд попереду, щоб вони залишалися в кеші:

// Horizontal sums for the first two lines
for(i=1;i<SIZE*2;i++){
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
}
// Rest of the computation
for(;i<totalSize;i++){
    // Compute horizontal sum for next line
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
    // Final result
    resPointer[i-SIZE]=(hsumPointer[i-SIZE-SIZE]+hsumPointer[i-SIZE]+hsumPointer[i])/9;
}

8193: 599 ms
8192: 652 ms

Висновок:

  • Немає переваг від використання декількох покажчиків та просто збільшення (я думав, що це буде швидше)
  • Добирати горизонтальні суми краще, ніж обчислити їх кілька разів.
  • Два проходи не втричі швидше, лише в два рази.
  • Можна досягти 3,6 разів швидше, використовуючи як один прохід, так і кешуючи посередницький результат

Я впевнений, що можна зробити набагато краще.

ПРИМІТКА. Зверніть увагу, що я написав цю відповідь, щоб орієнтуватися на загальні проблеми продуктивності, а не на проблему кешу, пояснену в чудовій відповіді Mystical. На початку це був просто псевдо-код. Мене попросили зробити тести в коментарях ... Ось повністю відновлена ​​версія з тестами.


9
"Я думаю, що це принаймні в 3 рази швидше" - подбаєте зробити резервну копію цієї заяви за допомогою певних показників чи цитат?
Адам Розенфілд

8
@AdamRosenfield "Я думаю" = припущення! = "Це" = претензія. У мене немає метрики для цього, і я хотів би побачити тест. Але мої вимагають 7 кроків, 2 допоміжних, 2 додавання та один дів на піксель. Кожен цикл, що використовує менш локальний var, ніж є реєстрація в процесорі. Для інших потрібні 7 кроків, 6 скорочень, 1 поділ і від 10 до 20 мюл для адресації залежно від оптимізації компілятора. Крім того, кожна інструкція в циклі вимагає результатів попередньої інструкції, це відкидає переваги супер-скалярної архітектури Pentiums. Тож має бути швидше.
bokan

3
Відповідь на оригінальне питання стосується ефектів пам'яті та кешу. Причина того, що код OP настільки повільний, полягає в тому, що схема його доступу до пам'яті йде по стовпцях, а не по рядках, що має дуже погану локацію кешу. Це особливо погано в 8192, оскільки тоді послідовні рядки закінчуються використанням одних і тих же рядків кешу в кеш-пам'яті з прямим відображенням або кеш-пам'яті з низькою асоціативністю, тому швидкість пропуску кешу ще більше. Обмін петлями забезпечує величезне підвищення продуктивності, значно збільшуючи місце кешу.
Адам Розенфілд

1
Молодці, це вражаючі цифри. Як ви виявили, справа в тому, що продуктивність пам'яті - використання декількох покажчиків із збільшенням не принесло користі.
Адам Розенфілд

2
@AdamRosenfield Я дуже хвилювався сьогодні вранці, оскільки не міг відтворити тести. Виявляється, підвищення продуктивності відбувається лише за допомогою компілятора Visual C ++. Використовуючи gcc, є лише невелика різниця.
bokan
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.