Яка різниця між кодом, непривабливим до кешу, та кодом " дружнього до кешу "?
Як я можу переконатися, що я записую кешований код?
Яка різниця між кодом, непривабливим до кешу, та кодом " дружнього до кешу "?
Як я можу переконатися, що я записую кешований код?
Відповіді:
На сучасних комп'ютерах лише однієї структури пам'яті найнижчого рівня ( регістри ) можуть переміщувати дані за один тактовий цикл. Однак регістри дуже дорогі, і більшість комп'ютерних ядер мають менше декількох десятків регістрів (всього кілька сотень, а може і тисяча байтів ). На іншому кінці спектру пам'яті ( DRAM ) пам’ять дуже дешева (тобто буквально в мільйони разів дешевша ), але займає сотні циклів після запиту на отримання даних. Щоб подолати цей розрив між супер швидким і дорогим і надто повільним і дешевим - це кеш-пам'ять, названі L1, L2, L3 зі зменшенням швидкості та вартості. Ідея полягає в тому, що більшість виконуючого коду буде часто потрапляти на невеликий набір змінних, а решта (набагато більший набір змінних) нечасто. Якщо процесор не може знайти дані в кеші L1, він виглядає в кеш-пам'яті L2. Якщо ні, то кеш L3, а якщо ні, головна пам'ять. Кожен з цих "промаху" у часі дорогий.
(Аналогія кеш-пам’яті - це системна пам’ять, оскільки системна пам’ять - це занадто зберігання на жорсткому диску. Зберігання жорсткого диска - дуже дешево, але дуже повільно)
Кешування - один з основних методів зниження впливу затримки . Перефразовуючи Herb Sutter (див. Посилання нижче): збільшити пропускну здатність легко, але ми не можемо придбати вихід із затримки .
Дані завжди отримуються за допомогою ієрархії пам'яті (найменший == найшвидший до найповільнішого). Кеш / промах зазвичай відноситься до хіту / промаху в найвищому рівні кеша - пам'яті в CPU - на самому високому рівні , я маю в виду найбільшою == повільним. Швидкість удару кеша є вирішальною для продуктивності, оскільки кожен пропущений кеш приводить до отримання даних з оперативної пам'яті (або ще гірше ...), що вимагає багато часу (сотні циклів для оперативної пам’яті, десятки мільйонів циклів для жорсткого диска). Для порівняння, зчитування даних із кешу (найвищого рівня) зазвичай займає лише кілька циклів.
У сучасних комп'ютерних архітектурах вузькі місця продуктивності залишають процесорний процес (наприклад, доступ до оперативної пам’яті або вище). Це з часом тільки погіршиться. Збільшення частоти процесора наразі вже не має значення для підвищення продуктивності. Проблема - доступ до пам'яті. Тому зусилля з розробки програмного забезпечення в центральних процесорах в даний час в основному зосереджені на оптимізації кеш-пам'яті, попередньої завантаження, конвеєрів та одночасності. Наприклад, сучасні процесори витрачають близько 85% загиблих на кеші і до 99% для зберігання / переміщення даних!
На цю тему можна сказати досить багато. Ось кілька чудових довідок про кеші, ієрархії пам'яті та правильне програмування:
Дуже важливим аспектом коду, кероване кешем, є все про принцип локальності , мета якого - розміщення пов'язаних даних в пам’яті для забезпечення ефективного кешування. Що стосується кеш-процесора, важливо бути в курсі рядків кеша, щоб зрозуміти, як це працює: Як працюють лінії кеша?
Наступні конкретні аспекти мають велике значення для оптимізації кешування:
Використовуйте відповідне c ++ контейнери
Простий приклад кеш-пам’яті проти кеш-недружнього c ++'s std::vector
проти std::list
. Елементи a std::vector
зберігаються в суміжній пам'яті, і тому такий доступ до них набагато більш сприятливий для кешу, ніж доступ до елементів в a std::list
, який зберігає його вміст у всьому місці. Це пов’язано з просторовою локальністю.
Дуже приємну ілюстрацію цього дає Б'ярн Струструп у цьому відеоролику на YouTube (спасибі @Mohammad Ali Baydoun за посилання!).
Не нехтуйте кешем у структурі даних та дизайні алгоритму
По можливості намагайтеся адаптувати структуру даних та порядок обчислень таким чином, щоб максимально використовувати кеш. Поширеною технікою в цьому плані є блокування кешу (версія Archive.org) , яке має надзвичайно важливе значення у високопродуктивних обчисленнях (див., Наприклад, ATLAS ).
Знати та використовувати неявну структуру даних
Ще один простий приклад, про який багато людей у цій галузі іноді забувають - це головний стовпчик (колишній. фортран,матлаб) проти впорядкування основних рядків (напр. c,c ++) для зберігання двомірних масивів. Наприклад, розглянемо таку матрицю:
1 2
3 4
У порядку впорядкування основних рядків це зберігається в пам'яті як 1 2 3 4
; у впорядкуванні основного стовпця це зберігається як 1 3 2 4
. Неважко помітити, що реалізації, які не використовують це впорядкування, швидко натраплять на проблеми з кешем (які легко уникнути!). На жаль, такі речі дуже часто я бачу в своєму домені (машинне навчання). @MatteoItalia показав цей приклад більш докладно у своїй відповіді.
При отриманні певного елемента матриці з пам'яті елементи, поруч з нею, також будуть отримані та збережені в кеш-рядку. Якщо замовлення експлуатується, це призведе до меншої кількості доступу до пам'яті (тому що наступні декілька значень, необхідних для наступних обчислень, вже знаходяться в кеш-рядку).
Для простоти, припустимо, що кеш містить одну лінію кешу, яка може містити 2 елементи матриці, і що коли даний елемент витягується з пам'яті, наступний теж. Скажімо, ми хочемо взяти суму над усіма елементами в прикладі матриці 2x2 вище (давайте назвати це M
):
Експлуатування замовлення (наприклад, спочатку зміна індексу стовпців у c ++):
M[0][0] (memory) + M[0][1] (cached) + M[1][0] (memory) + M[1][1] (cached)
= 1 + 2 + 3 + 4
--> 2 cache hits, 2 memory accesses
Не використовувати замовлення (наприклад, спочатку змінити індекс рядка в c ++):
M[0][0] (memory) + M[1][0] (memory) + M[0][1] (memory) + M[1][1] (memory)
= 1 + 3 + 2 + 4
--> 0 cache hits, 4 memory accesses
У цьому простому прикладі використання замовлення приблизно подвоює швидкість виконання (оскільки для доступу до пам'яті потрібно набагато більше циклів, ніж обчислення сум). На практиці різниця в продуктивності може бути значно більшою.
Уникайте непередбачуваних гілок
Сучасні архітектури мають конвеєри та компілятори стають дуже хорошими при перепорядкуванні коду, щоб мінімізувати затримки через доступ до пам'яті. Якщо ваш критичний код містить (непередбачувані) гілки, важко або неможливо попередньо вибирати дані. Це побічно призведе до збільшення кількості пропусків кешу.
Це пояснено дуже добре тут (завдяки @ 0x90 за посилання): Чому обробка відсортованого масиву швидша, ніж обробка несортованого масиву?
Уникайте віртуальних функцій
В контексті c ++, virtual
методи представляють суперечливе питання щодо помилок кешу (існує загальний консенсус, що їх слід уникати, коли це можливо, з точки зору продуктивності). Віртуальні функції можуть спричиняти пропуски кешу під час пошуку, але це відбувається лише в тому випадку, якщо конкретна функція не викликається часто (інакше вона може бути кешована), тому деякі розцінюють це як проблему. Для ознайомлення з цією проблемою перевірте: яка вартість продуктивності наявності віртуального методу в класі C ++?
Поширена проблема в сучасних архітектурах з багатопроцесорними кешами називається помилковим обміном . Це відбувається, коли кожен окремий процесор намагається використовувати дані в іншій області пам’яті та намагається зберігати їх у тій же лінії кешу . Це призводить до того, що рядок кешу, який містить дані, який може використовувати інший процесор, знову і знову перезаписується. Ефективно, різні потоки змушують один одного чекати, викликаючи пропуски кешу в цій ситуації. Дивіться також (спасибі @Matt за посилання): Як і коли вирівняти розмір лінії кешу?
Надзвичайним симптомом поганого кешування пам’яті оперативної пам’яті (що, мабуть, не так, що ви маєте на увазі в цьому контексті), є так зване трелювання . Це відбувається, коли процес постійно генерує помилки сторінки (наприклад, доступ до пам’яті, якої немає на поточній сторінці), що вимагає доступу до диска.
На додаток до відповіді @Marc Claesen, я вважаю, що повчальним класичним прикладом коду, непривабливого до кешу, є код, який сканує двовимірний масив C (наприклад, зображення растрового зображення) замість рядка.
Елементи, які примикають до ряду, також суміжні в пам'яті, тому доступ до них послідовно означає доступ до них у порядку зростання пам’яті; це зручно для кешу, оскільки кеш має тенденцію попередньо вибирати суміжні блоки пам'яті.
Натомість, доступ до таких елементів у стовпчиках є непривабливим для кешування, оскільки елементи одного стовпця віддалені в пам’яті один від одного (зокрема, їх відстань дорівнює розміру рядка), тож при використанні цього шаблону доступу ви скачуть по пам’яті, потенційно витрачаючи зусилля кешу на пошук елементів поблизу пам’яті.
І все, що потрібно, щоб зіпсувати виставу - це піти
// Cache-friendly version - processes pixels which are adjacent in memory
for(unsigned int y=0; y<height; ++y)
{
for(unsigned int x=0; x<width; ++x)
{
... image[y][x] ...
}
}
до
// Cache-unfriendly version - jumps around in memory for no good reason
for(unsigned int x=0; x<width; ++x)
{
for(unsigned int y=0; y<height; ++y)
{
... image[y][x] ...
}
}
Цей ефект може бути досить драматичним (кілька порядків швидкості) в системах з невеликими кешами та / або в роботі з великими масивами (наприклад, 10+ мегапікселів на 24 bpp зображення на поточних машинах); з цієї причини, якщо вам доведеться зробити багато вертикальних сканувань, часто краще спочатку повернути зображення на 90 градусів і пізніше виконати різні аналізи, обмеживши кеш-код, непривітний, лише обертанням.
Оптимізація використання кешу значною мірою зводиться до двох факторів.
Перший фактор (на який уже згадували інші) - це місце відліку. Місцевість відліку дійсно має два виміри: простір та час.
Просторовий вимір також зводиться до двох речей: по-перше, ми хочемо щільно упакувати нашу інформацію, щоб більше інформації вмістилося в цій обмеженій пам'яті. Це означає (наприклад), що вам потрібно значно покращити складність обчислень, щоб виправдати структури даних на основі невеликих вузлів, об'єднаних покажчиками.
По-друге, ми хочемо, щоб інформація, яка буде оброблятися разом, також знаходилася разом. Типовий кеш працює в "рядках", це означає, що коли ви отримуєте доступ до деякої інформації, інша інформація за сусідніми адресами буде завантажена в кеш з тією частиною, яку ми торкнулися. Наприклад, коли я торкаюся одного байта, кеш може завантажувати 128 або 256 байт поблизу цього. Щоб скористатися цим, ви, як правило, хочете, щоб дані, упорядковані, збільшили максимум ймовірності того, що ви також будете використовувати ті інші дані, які були завантажені одночасно.
Що стосується насправді тривіального прикладу, це може означати, що лінійний пошук може бути набагато конкурентоспроможнішим для двійкового пошуку, ніж ви очікували. Після завантаження одного елемента з рядка кешу, використання решти даних у цьому рядку кешу майже безкоштовне. Бінарний пошук стає помітно швидшим лише тоді, коли дані є достатньо великими, що двійковий пошук зменшує кількість рядків кешу, до яких ви отримуєте доступ.
Вимір часу означає, що коли ви робите деякі операції над деякими даними, ви хочете (якомога більше) виконати всі операції з цими даними одночасно.
Так як ви додали це як C ++, я вкажу на класичний приклад щодо кеш-недружнім дизайн: std::valarray
. valarray
Перевантаження найбільш арифметичні оператори, так що я можу (наприклад) сказати a = b + c + d;
(де a
, b
, c
і d
все valarrays) , щоб зробити поелементне складання цих масивів.
Проблема в цьому полягає в тому, що він проходить через одну пару входів, дає результати тимчасові, проходить через іншу пару входів тощо. Маючи велику кількість даних, результат одного обчислення може зникнути з кешу, перш ніж він буде використаний у наступному обчисленні, тому ми закінчуємо читання (записування) даних неодноразово, перш ніж отримуємо наш кінцевий результат. Якщо кожен елемент кінцевого результату буде що - щось на зразок (a[n] + b[n]) * (c[n] + d[n]);
, ми зазвичай вважаємо за краще читати кожен a[n]
, b[n]
, c[n]
і d[n]
один раз, робити обчислення, записуємо результат, збільшення n
і повторіть «сезам ми зробили. 2
Другий головний фактор - уникнення спільного використання ліній. Щоб зрозуміти це, нам, ймовірно, потрібно створити резервну копію та трохи подивитися, як організовано кеші. Найпростіша форма кешу - це пряме відображення. Це означає, що одна адреса в основній пам'яті може зберігатися лише в одному конкретному місці в кеші. Якщо ми використовуємо два елементи даних, які відображають на одне місце в кеші, це працює погано - кожен раз, коли ми використовуємо один елемент даних, інший повинен бути видалений з кешу, щоб звільнити місце для іншого. Решта кешу може бути порожньою, але ці елементи не використовуватимуть інші частини кеша.
Щоб цього не допустити, більшість кеш-пам'яток називають "встановити асоціативний". Наприклад, у кеш-сховищі 4-ходового набору будь-який елемент з основної пам'яті може зберігатися в будь-якому з 4 різних місць кеша. Отже, коли кеш буде завантажувати елемент, він шукає найменш використаний 3 елемент серед цих чотирьох, передає його в основну пам'ять і завантажує новий елемент на своє місце.
Проблема, ймовірно, досить очевидна: для кешу з прямим відображенням два операнди, які трапляються на карті в одне місце кешу, можуть призвести до поганої поведінки. N-шлях кеш-асоціативного кеша збільшує число від 2 до N + 1. Впорядкування кешу в більш "способи" вимагає додаткової схеми і, як правило, працює повільніше, тому (наприклад, 8192-набір асоційованого кеша також рідко є хорошим рішенням.
Зрештою, цей фактор важче контролювати в портативному коді. Ваш контроль над тим, де розміщуються ваші дані, зазвичай досить обмежений. Гірше, точне відображення від адреси до кешу різниться між інакше подібними процесорами. У деяких випадках, однак, варто зробити такі речі, як виділення великого буфера, а потім використовувати лише частини того, що ви виділили, щоб забезпечити обмін даними одними і тими ж лініями кешу (хоча, ймовірно, вам потрібно буде виявити точний процесор і діяти відповідно, щоб це зробити).
Є ще один пов'язаний елемент, який називається "помилковим спільним доступом". Це виникає в багатопроцесорній або багатоядерній системі, де два (або більше) процесорів / ядер мають окремі дані, але потрапляють у ту саму лінію кешу. Це змушує обох процесорів / ядер координувати свій доступ до даних, хоча кожен має свій окремий елемент даних. Особливо, якщо обидві модифікують дані по черзі, це може призвести до значного уповільнення, оскільки дані повинні постійно перетинатися між процесорами. Це неможливо легко вилікувати, організувавши кеш у більш "способи" або щось подібне. Основний спосіб запобігти це - гарантувати, що два потоки рідко (бажано ніколи) не змінюють дані, які, можливо, можуть знаходитися в одній лінії кешу (з однаковими застереженнями щодо складності управління адресами, за якими розподіляються дані).
Тим, хто добре знає C ++, може бути цікаво, чи це відкриття для оптимізації через щось на зразок шаблонів виразів. Я впевнений, що відповідь полягає в тому, що так, це можна було б зробити, і якби це було, це, мабуть, буде досить вагомим виграшем. Я не знаю, як хтось зробив це, але, враховуючи те, як мало valarray
звикає, я був би принаймні трохи здивований, коли бачив когось.
Якщо хтось valarray
замислюється про те, як це (розроблено спеціально для продуктивності), може бути це дуже неправильно, це зводиться до одного: він був справді розроблений для таких машин, як старі Крейси, які використовували швидку основну пам'ять і не кешували. Для них це справді був майже ідеальним дизайном.
Так, я спрощую: більшість кешів насправді точно не вимірюють найменш використаний елемент, але вони використовують евристичний характер, який повинен бути близьким до цього, без необхідності зберігати повний штамп часу для кожного доступу.
valarray
приклад.
Ласкаво просимо у світ Дизайну, орієнтованого на дані. Основна мантра полягає в сортуванні, усуненні гілок, партії, усуненні virtual
викликів - всі кроки до кращого місцевості.
Оскільки ви позначили це питання C ++, ось обов'язковий типовий C ++ Bullshit . Тоні Альбрехт « Підводні камені об’єктно-орієнтованого програмування» також є чудовим вступом у цю тему.
Тільки закладаємо: класичний приклад непридатного до кешу порівняно з кешами зручного коду - це "блокування кеша" матриці множення.
Наївна матриця множення виглядає так:
for(i=0;i<N;i++) {
for(j=0;j<N;j++) {
dest[i][j] = 0;
for( k==;k<N;i++) {
dest[i][j] += src1[i][k] * src2[k][j];
}
}
}
Якщо N
він великий, наприклад, якщо N * sizeof(elemType)
більший розмір кеша, то кожен доступ до нього src2[k][j]
буде пропущеним кешем.
Існує багато різних способів оптимізації цього кешу. Ось дуже простий приклад: замість того, щоб читати один елемент у кеш-рядку у внутрішньому циклі, використовуйте всі елементи:
int itemsPerCacheLine = CacheLineSize / sizeof(elemType);
for(i=0;i<N;i++) {
for(j=0;j<N;j += itemsPerCacheLine ) {
for(jj=0;jj<itemsPerCacheLine; jj+) {
dest[i][j+jj] = 0;
}
for( k=0;k<N;k++) {
for(jj=0;jj<itemsPerCacheLine; jj+) {
dest[i][j+jj] += src1[i][k] * src2[k][j+jj];
}
}
}
}
Якщо розмір рядка кеш-пам'яті становить 64 байти, а ми працюємо на 32-бітових (4-байтних) поплавках, то в кеш-рядку є 16 елементів. А кількість кеш-пропусків за допомогою цього простого перетворення зменшується приблизно в 16 разів.
Фантастичні перетворення працюють на 2D плитках, оптимізують для декількох кешів (L1, L2, TLB) тощо.
Деякі результати "блокування кешу" Google:
http://stumptown.cc.gt.atl.ga.us/cse6230-hpcta-fa11/slides/11a-matmul-goto.pdf
http://software.intel.com/en-us/articles/cache-blocking-techniques
Приємна відеоанімація оптимізованого алгоритму блокування кешу.
http://www.youtube.com/watch?v=IFWgwGMMrh0
Петля плиткою дуже тісно пов'язана:
k==;
Я сподіваюся, що це друкарська помилка?
Сьогодні процесори працюють з багатьма рівнями каскадної області пам'яті. Таким чином у процесора буде купа пам'яті, яка знаходиться на самому чіпі процесора. Він має дуже швидкий доступ до цієї пам'яті. Існують різні рівні кешу, кожен з яких має повільніший доступ (і більший), ніж наступний, доки ви не перейдете до системної пам’яті, яка відсутня в процесорі та є відносно набагато повільнішою для доступу.
Логічно, в наборі інструкцій процесора ви просто звертаєтесь до адрес пам'яті у гігантському віртуальному адресному просторі. Коли ви отримаєте доступ до однієї адреси пам'яті, центральний процесор отримає її. за старих часів він мав би отримати лише одну єдину адресу. Але сьогодні процесор отримає купу пам'яті навколо потрібного вам біта і скопіює його в кеш. Передбачається, що якщо ви попросили певну адресу, велика ймовірність, що ви найближчим часом попросите адресу поблизу. Наприклад, якщо ви копіювали буфер, ви могли б читати та писати з послідовних адрес - одна за одною.
Отже, сьогодні, коли ви отримуєте адресу, він перевіряє кеш першого рівня, щоб побачити, чи він уже прочитав цю адресу в кеші, якщо він не знайде її, то це пропуск кеша, і він повинен вийти на наступний рівень кеш, щоб знайти його, поки він з часом не повинен вийти в основну пам'ять.
Дружній код кешу намагається зберегти доступ близько до пам’яті, щоб мінімізувати пропуски кешу.
Таким прикладом можна уявити, що ви хотіли скопіювати гігантську двовимірну таблицю. Він організований з рядком охоплення послідовно в пам'яті, а один рядок слідує за наступним відразу після.
Якщо ви скопіювали елементи по одному рядку зліва направо - це буде кешованим. Якщо ви вирішили скопіювати таблицю по одному стовпцю за один раз, ви скопіювали б точно такий же об'єм пам'яті - але це буде кеш недружньо.
Потрібно уточнити, що не тільки дані повинні бути кешованими, це так само важливо для коду. Це на додаток до передбачення галузей, переупорядкування інструкцій, уникання фактичних поділів та інших методик.
Зазвичай, чим щільніше код, тим менше рядків кешу буде потрібно для його зберігання. Це призводить до того, що для даних буде доступно більше рядків кеша.
Код не повинен викликати функції в усьому світі, оскільки вони зазвичай потребують однієї або декількох власних ліній кеш-пам'яті, що призводить до меншої кількості ліній кешу даних.
Функція повинна починатися з адреси кешування рядків кешу. Хоча для цього є компілятори компілятора (gcc), майте на увазі, що якщо функції дуже короткі, для кожної з них може бути марно зайняти цілу лінію кешу. Наприклад, якщо три найчастіше використовувані функції вміщуються в один 64-байтний кеш-рядок, це менш марно, ніж якщо кожна з них має свою лінію і призводить до двох ліній кеша, менш доступних для іншого використання. Типове значення вирівнювання може бути 32 або 16.
Тому витратьте трохи зайвого часу, щоб зробити код щільним. Тестуйте різні конструкції, компілюйте та переглядайте створений розмір коду та профіль.
Як @Marc Claesen зазначав, що один із способів написати дружній код кеша - це використання структури, в якій зберігаються наші дані. На додаток до цього ще одним способом написання зручного кеш-коду є: змінити спосіб зберігання наших даних; потім напишіть новий код для доступу до даних, що зберігаються в цій новій структурі.
Це має сенс у випадку, як системи баз даних лінеаризують кортежі таблиці та зберігають їх. Є два основні способи зберігання кортежів таблиці, тобто зберігання рядків та стовпців. У магазині рядків, як випливає з назви, кортежі зберігаються рядно. Припустимо, що таблиця з назвою, Product
що зберігається, має 3 атрибути, тобто int32_t key, char name[56]
і int32_t price
, тому загальний розмір кортежу становить 64
байти.
Ми можемо імітувати виконання основних запитів зберігання рядків у основній пам'яті, створивши масив Product
структур розміром N, де N - кількість рядків у таблиці. Такий макет пам'яті також називають масивом структур. Тож структура продукту може бути такою:
struct Product
{
int32_t key;
char name[56];
int32_t price'
}
/* create an array of structs */
Product* table = new Product[N];
/* now load this array of structs, from a file etc. */
Аналогічно ми можемо імітувати дуже базове виконання запиту зберігання стовпців у основній пам'яті, створивши 3 масиви розміром N, один масив для кожного атрибуту Product
таблиці. Таке розташування пам’яті ще називають структурою масивів. Отже, 3 масиви для кожного атрибуту Product можуть виглядати так:
/* create separate arrays for each attribute */
int32_t* key = new int32_t[N];
char* name = new char[56*N];
int32_t* price = new int32_t[N];
/* now load these arrays, from a file etc. */
Тепер після завантаження масиву структур (макет рядків) та 3 окремих масивів (макет стовпців) Product
у нас на пам’яті зберігаються рядки і сховища стовпців .
Тепер ми переходимо до дружньої кодової частини кешу. Припустимо, що навантаження на нашій таблиці така, що у нас є запит на агрегацію щодо атрибуту ціни. Як от
SELECT SUM(price)
FROM PRODUCT
Для магазину рядків ми можемо перетворити вищезазначений запит SQL в
int sum = 0;
for (int i=0; i<N; i++)
sum = sum + table[i].price;
Для магазину стовпців ми можемо перетворити вищезазначений SQL запит у
int sum = 0;
for (int i=0; i<N; i++)
sum = sum + price[i];
Код для зберігання стовпців був би швидшим, ніж код для макета рядків у цьому запиті, оскільки він вимагає лише підмножини атрибутів, і в макеті стовпців ми робимо саме це, тобто лише доступ до стовпця цін.
Припустимо, що розмір рядка кеша - це 64
байти.
У випадку компонування рядків, коли читається рядок кешу, ціна лише 1 ( cacheline_size/product_struct_size = 64/64 = 1
) кортежу зчитується, тому що наш розмір структури - 64 байти, і він заповнює весь наш рядок кешу, тому для кожного кортежу пропуск кеша відбувається у випадку макетів рядків
У випадку компонування стовпців при зчитуванні рядка кеша зчитується цінова вартість 16 ( cacheline_size/price_int_size = 64/4 = 16
) кортежів, тому що 16 суміжних цін цін, що зберігаються в пам'яті, вносяться в кеш, тому для кожного шістнадцятого кортежу кеш пропускає у випадку макет стовпця
Таким чином, макет стовпців буде швидшим у випадку заданого запиту, і швидшим при таких запитах агрегації на підмножині стовпців таблиці. Ви можете спробувати такий експеримент для себе, скориставшись даними з еталону TPC-H , і порівняти час виконання для обох макетів. Стаття у вікіпедії про системи баз даних, орієнтованих на стовпці, також хороша.
Таким чином, в системах баз даних, якщо навантаження запиту заздалегідь відома, ми можемо зберігати наші дані в макетах, які відповідатимуть запитам у навантаженні та матимуть доступ до даних цих макетів. У наведеному вище прикладі ми створили макет стовпців і змінили наш код на обчислення суми, щоб він став зручним для кешу.
Майте на увазі, що кеші не просто кешують постійну пам'ять. Вони мають кілька ліній (щонайменше 4), тому розривна пам'ять, що перекривається, часто може зберігатися так само ефективно.
Те, чого не вистачає у всіх вищезазначених прикладах, - це вимірювані орієнтири. Існує багато міфів про продуктивність. Якщо ви її не виміряєте, ви не знаєте. Не ускладнюйте свій код, якщо ви не помітно покращили.