Як один код запису, який найкраще використовує кеш процесора для підвищення продуктивності?


159

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

  1. Як зробити код, ефективний кеш / кеш-пам'ять (більше звернень до кешу, якомога менше пропусків кешу)? З обох позицій кеш даних та кеш програм (кеш інструкцій), тобто які речі в коді, пов'язані зі структурами даних та конструкціями коду, повинні подбати про те, щоб зробити кеш ефективним.

  2. Чи є якісь структури даних, які потрібно використовувати / уникати, чи є певний спосіб доступу до членів цієї структури тощо ..., щоб зробити кеш-код ефективним.

  3. Чи існують будь-які конструкти програм (якщо, для, перемикання, перерва, перехід, ...), кодового потоку (для всередині, якщо, якщо всередині, для тощо), у цьому питанні слід дотримуватися / уникати?

Я з нетерпінням чекаю почуття індивідуального досвіду, пов'язаного з тим, щоб зробити кеш-код ефективним в цілому. Це може бути будь-яка мова програмування (C, C ++, асамблея, ...), будь-яка цільова апаратура (ARM, Intel, PowerPC, ...), будь-яка ОС (Windows, Linux, S ymbian, ...) тощо. .

Сорт допоможе краще зрозуміти його глибоко.


1
Як вступ, ця розмова дає хороший огляд youtu.be/BP6NxVxDQIs
schoetbi

Наведена вище скорочена URL-адреса вже не працює, це повна URL-адреса для розмови: youtube.com/watch?v=BP6NxVxDQIs
Abhinav Upadhyay

Відповіді:


119

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

Методи уникнення страждань від затримки пам’яті, як правило, перше, що слід врахувати, а іноді допомагає довгим шляхом. Обмежена пропускна здатність пам'яті також є обмежуючим фактором, особливо для багатоядерних та багатопотокових програм, де багато потоків хочуть використовувати шину пам'яті. Різний набір методів допомагає вирішити останнє питання.

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

Поліпшення використання кеш-рядків допомагає у трьох аспектах:

  • Він, як правило, вміщує більш корисні дані в кеш, істотно збільшуючи ефективний розмір кешу.
  • Він, як правило, вміщує більш корисні дані в один рядок кешу, збільшуючи ймовірність того, що запитувані дані можуть бути знайдені в кеші.
  • Це зменшує вимоги до пропускної здатності пам’яті, оскільки їх буде менше.

Поширені методи:

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

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

Сучасний процесор часто має один або декілька апаратних попередників . Вони тренуються за промахами в кеш-пам'яті і намагаються помітити закономірності. Наприклад, після декількох пропусків до наступних рядків кешу, префетер hw почне виводити кешові рядки в кеш, очікуючи потреби програми. Якщо у вас є звичайний шаблон доступу, апаратний префедер зазвичай робить дуже хорошу роботу. І якщо у вашій програмі не відображаються регулярні схеми доступу, ви можете покращити, додавши інструкції з попереднього вибору самостійно.

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

Щоб знизити загальний тиск в шині пам’яті, потрібно почати звертатися до того, що називається тимчасовою місцевістю . Це означає, що вам доведеться повторно використовувати дані, поки вони ще не були вилучені з кешу.

Об'єднання циклів, які стосуються одних і тих же даних ( циклічне злиття ), та використання методів перезапису, відомих як плитка або блокування, все прагнуть уникнути цих додаткових витягів пам'яті.

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

Ці речі справді окупаються в багатоядерному світі, де ти зазвичай не бачиш багато покращень пропускної здатності після додавання другого потоку.


5
Переглянувши різні стандартні орієнтири, ми побачили, що дивно велика частка тих, хто не використовує 100% вилучених ліній кеша, перш ніж рядки кешу будуть виселені. Чи можу я запитати, які інструменти для профілювання дають вам таку інформацію та як?
Драконова енергія

"Організуйте свої дані, щоб уникнути вирівнювання отворів (сортування членів структури за зменшенням розміру - це один із способів)" - чому компілятор не оптимізує це сам? чому компілятор не завжди може "сортувати членів за зменшенням розміру"? Яка перевага, щоб утримувати членів несортивно?
javapowered

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

1
@javapowered Компілятор може зробити це залежно від мови, хоча я не впевнений, чи є хтось із них. Причина, коли ви не можете цього зробити в C, полягає в тому, що цілком справедливо звертатися до членів за базовою адресою + зсувом, а не за назвою, що означає, що упорядкування членів повністю порушило б програму.
Ден Бешард

56

Не можу повірити, що відповідей на це більше немає. У будь-якому випадку, один класичний приклад - це повторити багатовимірний масив "назовні":

pseudocode
for (i = 0 to size)
  for (j = 0 to size)
    do something with ary[j][i]

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

for (i = 0 to size)
  for (j = 0 to size)
    do something with ary[i][j]

Він запуститься набагато швидше.


9
Ще в коледжі ми мали завдання з множення матриць. З'ясувалось, що швидше спочатку перенести матрицю "стовпців" і множити рядки на рядки, а не на рядки на знаки з цієї точної причини.
ікаганович

11
насправді, більшість сучасних компіляторів можуть зрозуміти це самостійно (з увімкненими оптимізаціями)
Рікардо Нолде,

1
@ykaganovich Це також приклад у статті Ульріха Дрепперса: lwn.net/Articles/255364
Simon Stender Boisen

Я не впевнений, що це завжди правильно - якщо весь масив вписується в кеш-пам'ять L1 (часто 32 к!), Обидва замовлення матимуть однакову кількість звернень та пропусків кешу. Можливо, попереднє вилучення пам'яті може мати певний вплив. Раді, що виправилися, звичайно.
Метт Паркінс

хто коли-небудь обере першу версію цього коду, якщо замовлення не має значення?
срібний_рокет

45

Основні правила насправді досить прості. Там, де це стає складним, полягає в тому, як вони застосовуються до вашого коду.

Кеш працює на двох принципах: темпоральна локальність та просторова локальність. Перша - це думка, що якщо ви нещодавно використали певний фрагмент даних, вам, швидше за все, знадобляться незабаром. Останнє означає, що якщо ви нещодавно використовували дані за адресою X, вам, ймовірно, незабаром знадобиться адреса X + 1.

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

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

Простий приклад - обхід 2D масиву, який показав відповідь 1800 року. Якщо ви проходите рядок за часом, ви читаєте пам'ять послідовно. Якщо ви це зробите в стовпчику, ви прочитаєте один запис, а потім перейдете до зовсім іншого місця (початок наступного рядка), прочитайте один запис та ще раз перескочіть. І коли ви нарешті повернетесь до першого ряду, він більше не буде в кеші.

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

Використання кеш-інструкцій, як правило, набагато менше проблеми. Що зазвичай потрібно хвилюватися - це кеш даних.

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

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


4
+1, хороші та практичні поради. Одне доповнення: комбіновані часові локації та простори дозволяють припустити, що, наприклад, для матричних опцій, може бути доцільним розділити їх на менші матриці, які повністю вписуються в рядок кешу, або рядки / стовпці яких вписуються в рядки кешу. Я пам’ятаю, що робив це для візуалізації мультидім. дані. Це забезпечило серйозний удар в штани. Добре пам’ятати, що кеш-пам'ять містить декілька «рядків»;)
AndreasT

1
Ви кажете, що лише 1 CPU може мати задану адресу в кеші L1 за часом - я припускаю, що ви маєте на увазі рядки кешу, а не адресу. Також я чув про помилкові проблеми спільного використання, коли принаймні один з процесорів робить записи, але не, якщо обидва лише читають. Отже, під "доступом" ви справді маєте на увазі "пише"?
Джозеф Гарвін

2
@JosephGarvin: так, я мав на увазі пише. Ви маєте рацію, кілька ядер можуть мати однакові лінії кешу в своїх кешах L1 одночасно, але коли одне ядро ​​записує на ці адреси, воно стає недійсним у всіх інших кешах L1, і тоді їм доведеться перезавантажити його, перш ніж вони зможуть це зробити нічого з цим. Вибачте за неточну (неправильну) формулювання. :)
jalf

44

Рекомендую прочитати статтю з 9 частин Що повинен знати кожен програміст про пам'ять від Ulrich Drepper, якщо вас цікавить взаємодія пам'яті та програмного забезпечення. Він також доступний у вигляді PDF-сторінки на 104 сторінки .

Особливо важливими для цього питання є розділи 2 (кеші процесора) та частина 5 (що можуть зробити програмісти - оптимізація кешу).


16
Вам слід додати короткий зміст основних пунктів до статті.
Азмісов

Чудова прочитана, але ще одна книга, яку ПОВИННО згадати тут, - це Хеннесі, Паттерсон, Архітектура комп’ютерів, Кількісний підхід , який доступний у п'ятому виданні на сьогодні.
Haymo Kutschbach

15

Окрім шаблонів доступу до даних, головним фактором зручного для кешу коду є розмір даних . Менше даних означає, що більше їх вписується в кеш.

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

Аналогічно, булевий масив Java використовує цілий байт для кожного значення, щоб дозволити безпосередньо працювати над окремими значеннями. Ви можете зменшити розмір даних в 8 разів, якщо використовуєте фактичні біти, але тоді доступ до окремих значень стає набагато складнішим, вимагаючи операцій з переміщенням бітів та маскування ( BitSetклас робить це для вас). Однак через кеш-ефекти це все-таки може бути значно швидшим, ніж використання булевого [], коли масив великий. IIRC я ​​колись досяг цього прискорення на коефіцієнт 2 або 3 таким чином.


9

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

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

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

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


@Grover: Про ваш пункт 2. Так можна сказати, що якщо всередині щільного циклу, функція викликається для кожного підрахунку циклу, то вона отримає новий код взагалі і спричинить пропуск кеша, натомість якщо ви можете поставити функцію як код у циклі for for, без виклику функції, це було б швидше через меншу кількість пропусків кешу?
goldenmean

1
Так і ні. Нова функція буде завантажена в кеш. Якщо в кеш-пам'яті достатньо місця, то при другій ітерації він вже буде виконувати цю функцію в кеші, тому немає причини повторно його завантажувати. Тож це хіт на перший дзвінок. У C / C ++ ви можете попросити компілятора розмістити функції поруч один з одним за допомогою відповідних сегментів.
grover

Ще одна примітка: Якщо ви зателефонуєте з циклу і не вистачає місця в кеші, нова функція буде завантажена в кеш незалежно. Може навіть статися, що початковий цикл буде викинутий з кеша. У цьому випадку виклик понесе до трьох штрафів за кожну ітерацію: один для завантаження цілі виклику, а інший для перезавантаження циклу. І третє, якщо головка циклу не знаходиться в тій же лінії кешу, що і адреса повернення виклику. У такому випадку для переходу на головку циклу також потрібен новий доступ до пам'яті.
grover

8

Один із прикладів, який я бачив, як використовується в ігровому двигуні, - це переміщення даних з об'єктів у власні масиви. Ігровий об’єкт, який підпорядковується фізиці, також може мати багато інших даних, що додаються до нього. Але під час циклу оновлення фізики все, що хвилювало двигун, - це дані про положення, швидкість, масу, обмежувальний ящик тощо. Отже, все це було розміщено у власні масиви та максимально оптимізовано для SSE.

Так під час циклу фізики дані фізики оброблялися в порядку масиву з використанням векторної математики. Ігрові об’єкти використовували свій ідентифікатор об'єкта як індекс у різних масивах. Це не було вказівником, тому що вказівники можуть стати недійсними, якщо масиви доведеться переселити.

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

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


2
+1 Не застаріло. Це найкращий спосіб впорядкувати дані для ігрових двигунів - зробити блоки даних безперервними та виконувати всі задані типи операцій (скажімо, AI), перш ніж перейти до наступної (скажімо, фізики), щоб використовувати близькість кеша / локальність довідник.
Інженер

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

@will: Ні, я не пам’ятаю, де саме це було.
Zan Lynx

Це сама ідея складової системи сутності (ECS: en.wikipedia.org/wiki/Entity_component_system ). Зберігайте дані як структуру масивів, а не традиційні масиви структур, які заохочують практики OOP.
BuschnicK

7

Лише одна публікація торкнулася її, але велика проблема виникає при обміні даними між процесами. Ви хочете уникати декількох процесів, які намагаються одночасно змінити ту саму лінію кешу. На що слід звернути увагу - це "помилковий" обмін, де дві суміжні структури даних ділять кеш-рядок, а модифікації однієї інвалідують лінію кешу для іншої. Це може призвести до того, що рядки кешу необгрунтовано переміщаються вперед і назад між кешами процесора, що обмінюються даними в багатопроцесорній системі. Спосіб уникнути цього - вирівняти та прокладати структури даних, щоб розмістити їх у різних лініях.


7

Зауваження до "класичного прикладу" користувача 1800 ІНФОРМАЦІЯ (занадто довго для коментаря)

Я хотів перевірити різницю в часі для двох порядків ітерації ("зовнішня" та "внутрішня"), тому я зробив простий експеримент з великим 2D-масивом:

measure::start();
for ( int y = 0; y < N; ++y )
for ( int x = 0; x < N; ++x )
    sum += A[ x + y*N ];
measure::stop();

а другий випадок з forпетлями поміщений.

Більш повільна версія ("x перше") становила 0,88 сек, а швидша - 0,06 сек. Оце сила кешування :)

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


Не впевнений, що я це отримую. В обох прикладах ви все ще отримуєте доступ до кожної змінної в циклі for. Чому один шлях швидший за інший?
Ред.

Зрештою, для мене зрозуміло, як це впливає :)
Laie

@EdwardCorlew Це через порядок їх доступу. У першому порядку швидше, оскільки він отримує доступ до даних послідовно. Коли запитується перший запис, кеш L1 завантажує цілий рядок кешу, який включає запитуваний int плюс наступні 15 (припускаючи 64-байтну кеш-лінію), тому немає стійла процесора, що чекає наступного 15. The x -перший порядок повільніший, оскільки доступ до елемента не є послідовним, і, імовірно, N є достатньо великим, що пам'ять, до якої звертаються, завжди знаходиться поза кешем L1, і тому кожна операція зупиняється.
Метт Паркінс

4

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


@Andrew: Як щодо структур. Чи кеш-ефективні вони? Чи є у них обмеження розміру для кеш-ефективності?
goldenmean

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

Деякий спосіб використання пов'язаних списків без вбивства кешу, найбільш ефективний для не великих списків, - це створити власний пул пам'яті, тобто - виділити один великий масив. то замість пам'яті "malloc'ing (або" new'ing "в C ++) для кожного маленького пов'язаного члена списку, який може бути виділений на зовсім інше місце в пам'яті, і витрачаючи місце для управління, ви надаєте йому пам'ять з пулу пам'яті, сильно збільшуючи шанси, що логічно закриють учасників списку, будуть разом у кеші.
Ліран Ореві

Звичайно, але це багато роботи, щоб отримати std :: list <> та ін. використовувати власні блоки пам'яті. Коли я був молодим викрадачем, я абсолютно пішов цим шляхом, але в наші дні ... надто багато іншого, щоб вирішити.
Андрій


4

Кеш розташований в "кешових рядках", а (справжня) пам'ять читається і записується в шматки такого розміру.

Отже, структури даних, що містяться в одній лінії кешу, є ефективнішими.

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

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


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

4

Запитати про те, як зробити код, кешувати ефективним кешем та більшість інших питань, як правило, запитати, як оптимізувати програму, це тому, що кеш має такий величезний вплив на продуктивність, що будь-яка оптимізована програма - це кеш ефективні кеші

Пропоную почитати про оптимізацію, на цьому сайті є кілька хороших відповідей. Що стосується книг, то я рекомендую програму "Комп'ютерні системи: перспектива програміста", яка містить тонкий текст про правильне використання кешу.

(btw - так погано, як може бути пропущення кешу, ще гірше - якщо програма переходить на жорсткий диск ...)


4

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

Ідея запозичена з інших методів конвеєрного транспорту, наприклад, інструкції з конвеєрного процесору.

Цей тип шаблону найкраще стосується таких процедур

  1. може бути розбита на розумні декілька під кроків, S [1], S [2], S [3], ... час виконання яких приблизно порівняно з часом доступу до ОЗУ (~ 60-70ns).
  2. бере пакет даних і зробить вищезазначені кілька кроків над ними, щоб отримати результат.

Візьмемо простий випадок, коли існує лише одна підпроцедура. Зазвичай код бажає:

def proc(input):
    return sub-step(input))

Для кращої продуктивності ви можете передати декілька входів для функції в пакетному режимі, щоб ви амортизували накладні виклики функцій, а також збільшували локальність кеш-коду.

def batch_proc(inputs):
    results = []
    for i in inputs:
        // avoids code cache miss, but still suffer data(inputs) miss
        results.append(sub-step(i))
    return res

Однак, як було сказано раніше, якщо виконання кроку приблизно збігається з часом доступу до оперативної пам’яті, ви можете додатково вдосконалити код приблизно до цього:

def batch_pipelined_proc(inputs):
    for i in range(0, len(inputs)-1):
        prefetch(inputs[i+1])
        # work on current item while [i+1] is flying back from RAM
        results.append(sub-step(inputs[i-1]))

    results.append(sub-step(inputs[-1]))

Потік виконання виглядатиме так:

  1. prefetch (1) попросить ЦП попередньо вилучити вхід [1] в кеш, де інструкція попереднього вибору бере P цикли сам і повертається, а на фоні введення [1] надходить у кеш після R циклів.
  2. works_on (0) холодний промах на 0 і працює на ньому, що приймає M
  3. prefetch (2) видає інший вибір
  4. works_on (1) якщо P + R <= M, то входи [1] повинні бути в кеші вже до цього кроку, таким чином уникайте пропуску кешу даних
  5. work_on (2) ...

Можливо, буде задіяно більше кроків, тоді ви можете спроектувати багатоступеневий конвеєр до тих пір, поки терміни кроків та затримка доступу до пам'яті не збігаються, ви не зазнаєте пропуску кешу коду / даних. Однак цей процес повинен бути налаштований на багато експериментів, щоб з’ясувати правильну групування етапів та попередній вибір часу. Завдяки необхідним зусиллям, він бачить більше прийняття у високопродуктивній обробці даних / пакетному потоці. Хороший приклад виробничого коду можна знайти в дизайні трубопроводу DPDK QoS Enqueue : http://dpdk.org/doc/guides/prog_guide/qos_framework.html Розділ 21.2.4.3. Трубопровід для анкетування

Більше інформації можна знайти:

https://software.intel.com/en-us/articles/memory-management-for-optimal-performance-on-intel-xeon-phi-coprocessor-alignment-and

http://infolab.stanford.edu/~ullman/dragon/w06/lectures/cs243-lec13-wei.pdf


1

Напишіть програму на мінімальний розмір. Ось чому не завжди корисно використовувати оптимізацію -O3 для GCC. Він займає більший розмір. Часто -О так само добре, як -O2. Все залежить від процесора, який використовується. YMMV.

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

Щоб допомогти вам краще використовувати тимчасову / просторову локалізацію інструкцій, ви можете вивчити, як ваш код перетворюється на збірку. Наприклад:

for(i = 0; i < MAX; ++i)
for(i = MAX; i > 0; --i)

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


Цікавий момент. Чи роблять кеші, що дивляться вперед, припущення, засновані на напрямку циклу / проходження через пам'ять?
Андрій

1
Існує багато способів проектування спекулятивних кеш-даних. На базі кроку вимірюють "відстань" та "напрямок" доступу до даних. На основі вмісту переслідують ланцюжки покажчиків. Є й інші способи їх проектування.
sybreon

1

Окрім вирівнювання вашої структури та полів, якщо ваша структура, якщо розміщена купа, ви можете використовувати алокатори, які підтримують вирівняні розподіли; як _aligned_malloc (sizeof (DATA), SYSTEM_CACHE_LINE_SIZE); в іншому випадку у вас може бути випадковий помилковий обмін; пам'ятайте, що в Windows купі за замовчуванням є вирівнювання 16 байт.

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