Чому в результаті множення масивів 2048x2048 порівняно з множиною 2047x2047 є величезна ефективність?


127

Я роблю деякий показник множення матриць, як це було зазначено раніше, Чому MATLAB настільки швидкий у матричному множенні?

Тепер у мене з'явився ще один випуск: при множенні двох матриць 2048x2048 існує велика різниця між C # та іншими. Коли я намагаюся помножити лише матриці 2047x2047, це здається нормальним. Додано ще деякі для співставлення.

1024x1024 - 10 секунд.

1027x1027 - 10 секунд.

2047x2047 - 90 секунд.

2048x2048 - 300 секунд.

2049x2049 - 91 секунда. (оновлення)

2500x2500 - 166 секунд

Це різниця в три з половиною хвилини для випадку 2k на 2k.

використовуючи 2dim масиви

//Array init like this
int rozmer = 2048;
float[,] matice = new float[rozmer, rozmer];

//Main multiply code
for(int j = 0; j < rozmer; j++)
{
   for (int k = 0; k < rozmer; k++)
   {
     float temp = 0;
     for (int m = 0; m < rozmer; m++)
     {
       temp = temp + matice1[j,m] * matice2[m,k];
     }
     matice3[j, k] = temp;
   }
 }

23
Це було б чудовим екзаменаційним запитанням для програмування вищого рівня C або класу дизайну ОС ;-)
Dana Sane

Ви пробували тестувати як багатовимірні [,], так і нерівні [] [] масиви, а також 32 та 64 бітові? Я тестував лише кілька разів, але зубчастий здався більш відповідним вашим результатам, але зубчастий 64-бітний був високим, я не знаю, чи є евристика в джиті, яка стосується цієї ситуації, або якщо кеш-пам'ять пов'язана, як було запропоновано раніше. Якщо ви хочете вирішити GPGPU, існує research.microsoft.com/en-us/projects/accelerator, який повинен бути конкурентоспроможним у часі на вашій іншій посаді.
Крис

Питання дещо наївне, але скільки оп (додавання / множення) бере участь у множенні двох квадратних матриць?
Нік Т

Відповіді:


61

Це, мабуть, стосується конфліктів у вашому кеш-пам'яті L2.

Пропуски кеша на matice1 не є проблемою, оскільки до них звертаються послідовно. Однак для matice2, якщо повний стовпець вписується в L2 (тобто, коли ви отримуєте доступ до matice2 [0, 0], matice2 [1, 0], matice2 [2, 0] ... і т. Д., Нічого не вилучається), ніж немає проблем з кеш пропущений і з matice2.

Тепер заглибимось у те, як працює кеш, якщо байт-адреса вашої змінної дорівнює X, ніж рядок кешу для неї буде (X >> 6) & (L - 1). Де L - загальна кількість рядків кеша у вашому кеші. L завжди потужність 2. Шість походить від того, що 2 ^ 6 == 64 байти - це стандартний розмір лінії кеша.

Тепер що це означає? Ну це означає, що якщо у мене адреса X і Y і (X >> 6) - (Y >> 6) ділиться на L (тобто деяка велика потужність 2), вони будуть зберігатися в тій же кеш-лінії.

Тепер, щоб повернутися до вашої проблеми, яка різниця між 2048 та 2049,

коли 2048 - ваш розмір:

якщо взяти & matice2 [x, k] і & matice2 [y, k], різниця (& matice2 [x, k] >> 6) - (& matice2 [y, k] >> 6) буде розділена на 2048 * 4 (розмір поплавця). Так велика потужність 2.

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

Якщо розмір становить 2049, то різниця становить 2049 * 4, що не має сили 2, тож у вас буде менше конфліктів, і ваш стовпець буде безпечно вписуватися у ваш кеш.

Тепер для перевірки цієї теорії ви можете зробити кілька речей:

Виділіть свій масив matice2, як цей matice2 [razmor, 4096], і запустіть з razmor = 1024, 1025 або будь-якого розміру, і ви повинні побачити дуже погану продуктивність порівняно з тим, що раніше мали. Це тому, що ви змушуєте вирівнювати всі стовпці, щоб суперечити один одному.

Потім спробуйте matice2 [razmor, 4097] і запустіть його будь-якого розміру, і ви повинні побачити набагато кращу продуктивність.


Ви помилилися в своїх останніх 2 абзацах? Обидві спроби абсолютно однакові. :)
Xeo

Асоціативність кешу також відіграє певну роль.
Бен Джексон

20

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

Якщо ви час інших пар (2 ^ n-1,2 ^ n), я думаю, ви побачите подібні ефекти.

Для більш повного пояснення у внутрішній циклі, де ви отримуєте доступ до matice2 [m, k], ймовірно, що matice2 [m, k] та matice2 [m + 1, k] зміщені один від одного на 2048 * sizeof (float) і, таким чином, відображати один і той же індекс у кеші L1. З керованим асоціативним кешем N-способу у вас зазвичай буде 1-8 розташувань кешу для всіх цих. Таким чином, майже всі ці звернення ініціюють вилучення кешу L1 та отримання даних із більш повільного кешу чи основної пам'яті.


+1. Звучить вірогідно. Слід бути обережним із асоціативністю кешу.
Макке

16

Це може бути пов'язано з розміром кеш-процесора. Якщо 2 ряди матричної матриці не підходять, то ви втратите час замінюючи елементи з ОЗП. Додаткових 4095 елементів може бути достатньо, щоб не допустити розміщення рядків.

У вашому випадку 2 ряди для 2047 2d матриць потрапляють в пам'ять 16 КБ (при 32 типі бітів). Наприклад, якщо у вас є кеш L1 (найближчий до процесора в шині) об'ємом 64 Кб, то ви можете помістити принаймні 4 ряди (2047 * 32) в кеш відразу. Якщо довші ряди є необхідними, щоб прокладки пари рядків перевищували 16 КБ, тоді все починає плутатись. Крім того, кожен раз, коли ви "пропускаєте" кеш, заміна даних з іншого кешу або основної пам'яті затримує речі.

Моя здогадка полягає в тому, що на відмінність часу запуску, яке ви бачите з різними розмірами матриць, впливає те, наскільки ефективно операційна система може використовувати наявний кеш (а деякі комбінації просто проблематичні). Звичайно, це все грубе спрощення з мого боку.


2
але це малоймовірно, що у нього є 16,7 Мб кешу процесора
Маріно Шіміч

Я оновив результати 2049x2049 - 91 секунду. Якщо це була "проблема кешу", чи не повинно це все ще бути 300+ с?
Вовк

@Marino відповідь було оновлено, щоб врахувати це.
Dana Sane

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

2
Я не думаю, що це пояснення є правильним. Проблема полягає в неповному використанні ємності кеша внаслідок конфліктів лінії кеш-пам'яті, коли розмір становить потужність 2. Також операційна система насправді не має нічого спільного з кешами, оскільки це не ОС, яка вирішує, що робити кешування, а що вилучити, це все в апараті. ОС має щось спільне з вирівнюванням даних, але в цьому випадку мова йде про те, як C # вирішує розподілити дані та як представити 2D масив у пам'яті, ОС це не має нічого спільного.
zviadm

10

Луї Бренді написав дві публікації в блозі, аналізуючи саме це питання:

Більше Craziness Cache та обчислювальна продуктивність - практичне дослідження для початківців із цікавою статистикою та спробами пояснити поведінку більш детально, воно дійсно зводиться до обмежень розміру кешу.


5

Враховуючи, що час падає на більші розміри, чи не буде більш ймовірним конфлікт кешу, особливо з потужностями 2 для проблемних розмірів матриці? Я не є фахівцем з питань кешування, але відмінна інформація з питань продуктивності кеша , пов'язані тут .


Розділ 5 посилання на асоціативність кешу, схоже, застосовується зокрема.
Dana Sane

4

Коли ви звертаєтесь до matice2масиву вертикально, він буде помінятись у кеш-пам'ять та виходити з неї значно більше. Якщо дзеркально відобразити масив по діагоналі, щоб ви могли отримати доступ до нього, використовуючи [k,m]замість [m,k], код запуститься набагато швидше.

Я перевірив це на матрицях 1024x1024, і це приблизно вдвічі швидше. Для матриць 2048x2048 це приблизно в десять разів швидше.


Це не пояснює, чому 2049 швидший, ніж 2048 р.
Макке

@Macke: Це тому, що він проходить деяку межу в кешуванні пам'яті, щоб було набагато більше пропусків кеша.
Гуффа

Чому потік? Якщо ви не скажете, що ви вважаєте неправильним, це не може покращити відповідь.
Гуффа

Ще один голос без будь-яких пояснень ... Чи є в моїй відповіді занадто мало "ймовірно", "здогадатися" і "повинно", як відповіді, які отримують найбільше відгуків ...?
Гуффа

4

Збільшення кешу

Або кеш-обмолот , якщо я можу ввести термін.

Кеші працюють за допомогою індексації бітами низького порядку та позначенням бітів високого порядку.

Уявіть, що у вашому кеші є 4 слова, а у вашої матриці розмір 4 х 4. Коли доступ до стовпця має значення, а рядок має будь-яку потужність у довжину двох, то кожен елемент стовпця в пам'яті буде відображатись на той самий елемент кешу.

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

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

Оскільки кеш значно швидший, ніж DRAM (в основному в силу того, що він знаходиться на чіпі), швидкість звернення - це все.


2

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

Незалежно від проблеми, ви просто не повинні писати матричне множення самостійно в C #, а натомість використовувати оптимізовану версію BLAS. Цей розмір матриці повинен бути помножений на секунду на будь-якій сучасній машині.


1
Мені відомо про BLAS, але завдання полягало не в тому, щоб зробити його максимально швидким, а написати та протестувати на різних мовах. Це дуже дивна проблема для мене, і мені дуже цікаво, чому результати такі, як вони є.
Вовк

3
@Wolf Мені важко буде хвилюватися з приводу того, що щось, що повинно зайняти секунду, займає 90 секунд або 300 секунд.
Девід Геффернан

4
Найкращий спосіб дізнатися, як щось працює, - написати це самостійно і побачити, як можна вдосконалити свою реалізацію; це (сподіваємось), що робить Вовк.
Callum Rogers

@Callum Rogers, погодився. Саме тому я дізнався важливість розмірів буфера в операціях копіювання файлів.
Келлі С. Французька

1

Ефективне використання ієрархії кешу є дуже важливим. Потрібно переконатися, що багатовимірні масиви мають гарне розташування даних, що може бути досягнуто плиткою . Для цього вам потрібно буде зберегти 2D масив як 1D масив разом з механізмом індексації. Проблема традиційного методу полягає в тому, що хоча два суміжні елементи масиву, що знаходяться в одному рядку, знаходяться поруч один з одним у пам'яті, два суміжні елементи в одному стовпчику будуть розділені W елементами в пам'яті, де W - кількість стовпців . Плитка може скласти стільки ж, скільки і в десять разів різниця в продуктивності.


Hmm - все-таки масив, оголошений як 2D (float [,] matice = new float [rozmer, rozmer];), виділяється в оперативній пам'яті лише як одновимірний масив і обчислення рядків / кроків, виконані під кришкою. То чому б оголосити його як 1D і робити ручні обчислення рядків / кроків швидше? Ви маєте на увазі, що sol'n виділяє великий масив як масив менших плиток, кожна з яких може вміститися в кеш, де не було б великого масиву?
Ерік М

1
Якщо ваша бібліотека чи будь-який інструмент, який ви використовуєте, робить плитку, то вам цього не потрібно. Але якби ви використовували традиційний 2D масив, скажімо, C / C ++, то плитка покращить продуктивність.
Арлен

0

Я підозрюю, що це результат чогось під назвою " Послідовне потоплення ". Це те, що ви намагаєтеся провести перегляд списку об'єктів, який трохи більший за розмір кешу, тому кожен окремий запит до списку (масиву) повинен бути виконаний з оперативної пам'яті, і ви не отримаєте жодного кешу хіт.

У вашому випадку ви перебираєте свої масиви 2048 в індексах 2048 разів, але у вас є лише простір для 2047 (можливо, через деяку накладну структуру масиву), тому кожен раз, коли ви отримуєте доступ до позиції масиву, йому потрібно отримати цю позицію масиву від барана. Потім він зберігається в кеші, але безпосередньо перед його повторним використанням він скидається. Таким чином кеш по суті є марним, що призводить до набагато більшого часу виконання.


1
Неправильно. 2049 швидше, ніж 2048 рік, що спростовує ваші претензії.
Макке

@Macke: Це цілком можливо. Але є невелика ймовірність, що політика кешу, що використовується в його процесорі, все-таки може прийняти таке рішення. Це не дуже ймовірно, але це немислимо.
Automatico
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.