Что такое "кеширующий" код?

В чем разница между недружественным кодом кеша и кодом с кэшем дружественных?

Как я могу убедиться, что я пишу код, эффективный для кэширования?

Ответ 1

прелиминарии

На современных компьютерах только структуры памяти самого низкого уровня (регистры) могут перемещать данные за один такт. Однако регистры очень дороги, и большинство ядер компьютеров имеют менее нескольких десятков регистров (всего от нескольких сотен до, может быть, тысячи байт). На другом конце спектра памяти (DRAM) память очень дешевая (т.е. Буквально в миллионы раз дешевле), но после запроса на получение данных занимает сотни циклов. Чтобы преодолеть этот разрыв между супер быстрым и дорогим и супер медленным и дешевым, используются кэш-память, называемая L1, L2, L3 по уменьшающейся скорости и стоимости. Идея состоит в том, что большая часть исполняемого кода будет часто использовать небольшой набор переменных, а остальные (гораздо больший набор переменных) нечасто. Если процессор не может найти данные в кеше L1, он выглядит в кеше L2. Если нет, то кэш L3, а если нет, то основная память. Каждое из этих "промахов" дорого по времени.

(Аналогия заключается в том, что кэш-память относится к системной памяти, так как системная память является слишком жестким диском. Память на жестком диске очень дешевая, но очень медленная).

Кэширование является одним из основных методов уменьшения влияния задержки. Перефразируя Херба Саттера (см. Ссылки ниже): увеличить пропускную способность очень просто, но мы не можем выкупить выход из задержки.

Данные всегда извлекаются через иерархию памяти (от наименьшего == к быстрейшему к медленному). Попадание/нехватка кэша обычно относится к попаданию/промаху на самом высоком уровне кэша в ЦП - под самым высоким уровнем я подразумеваю самый большой == самый медленный. Частота обращений к кешу имеет решающее значение для производительности, так как каждая потеря кеша приводит к извлечению данных из ОЗУ (или хуже...), что занимает много времени (сотни циклов для ОЗУ, десятки миллионов циклов для жесткого диска). Для сравнения, чтение данных из (наивысшего уровня) кэша обычно занимает всего несколько циклов.

В современных компьютерных архитектурах узкое место в производительности покидает процессор (например, доступ к ОЗУ или выше). Со временем это только ухудшится. Увеличение частоты процессора в настоящее время более не актуально для повышения производительности. Проблема в доступе к памяти. Поэтому в настоящее время усилия по проектированию аппаратного обеспечения в процессорах сосредоточены на оптимизации кэшей, предварительной выборке, конвейерах и параллелизме. Например, современные процессоры тратят около 85% кристаллов на кеши и до 99% на хранение/перемещение данных!

На эту тему можно сказать много. Вот несколько замечательных ссылок о кешах, иерархиях памяти и правильном программировании:

Основные понятия для кеш-кода

Очень важный аспект кода, дружественного к кешу, связан с принципом локальности, цель которого - разместить связанные данные близко к памяти, чтобы обеспечить эффективное кэширование. Что касается кэша ЦП, важно знать о строках кэша, чтобы понять, как это работает: как работают строки кэша?

Следующие конкретные аспекты имеют большое значение для оптимизации кэширования:

  1. Временная локализация: когда к определенной ячейке памяти обращались, вполне вероятно, что к той же локации снова будет получен доступ в ближайшем будущем. В идеале, эта информация все еще будет кэшироваться в этот момент.
  2. Пространственная локализация: это относится к размещению связанных данных близко друг к другу. Кэширование происходит на многих уровнях, а не только в процессоре. Например, когда вы читаете из ОЗУ, обычно выбирается больший кусок памяти, чем было запрошено, потому что очень часто программе требуются эти данные в ближайшее время. Кэши HDD придерживаются той же линии мысли. Специально для кэшей ЦП важно понятие строк кэша.

Используйте соответствующие контейнеры

Простой пример кеш-дружественных и кеш-непригодных - это std::vector против std::list. Элементы std::vector хранятся в смежной памяти, и, таким образом, доступ к ним гораздо более удобен для кэша, чем доступ к элементам в std::list, который хранит свое содержимое повсюду. Это связано с пространственной локализацией.

Очень хорошая иллюстрация этого дана Бьярном Страуструпом в этом клипе на YouTube (спасибо @Mohammad Ali Baydoun за ссылку!).

Не пренебрегайте кешем в структуре данных и разработке алгоритмов

По возможности старайтесь адаптировать свои структуры данных и порядок вычислений таким образом, чтобы максимально использовать кеш. Общепринятым методом в этом отношении является блокировка кэша (версия Archive.org), которая имеет чрезвычайно важное значение в высокопроизводительных вычислениях (см., Например, ATLAS).

Знать и использовать неявную структуру данных

Другой простой пример, который иногда забывают многие в этой области, - это мажор столбцов (например, , ) или упорядочение по основным строкам (например, , ) для хранения двумерных массивов. Например, рассмотрим следующую матрицу:

1 2
3 4

При упорядочении по основным строкам это сохраняется в памяти как 1 2 3 4; при упорядочении по основным столбцам это будет храниться как 1 3 2 4. Легко видеть, что реализации, которые не используют этот порядок, быстро столкнутся (легко можно избежать!) С проблемами кэширования. К сожалению, я часто вижу подобные вещи в своей области (машинное обучение). @MatteoItalia показал этот пример более подробно в своем ответе.

При извлечении определенного элемента матрицы из памяти также будут извлечены элементы рядом с ним и сохранены в строке кэша. Если упорядочение используется, это приведет к меньшему количеству обращений к памяти (поскольку следующие несколько значений, которые необходимы для последующих вычислений, уже находятся в строке кэша).

Для простоты предположим, что кэш содержит одну строку кеша, которая может содержать 2 матричных элемента, и что, когда данный элемент выбирается из памяти, следующий тоже. Скажем, мы хотим взять сумму по всем элементам в приведенной выше примере матрицы 2x2 (назовем ее M):

Использование порядка (например, сначала изменение индекса столбца в ):

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

Не использовать порядок (например, сначала изменить индекс строки в ):

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 за ссылку): почему обработка отсортированного массива выполняется быстрее, чем обработка несортированного массива?

Избегайте виртуальных функций

В контексте virtual методы представляют спорную проблему в отношении промахов кэша (существует общее мнение, что их следует избегать, когда это возможно, с точки зрения производительности). Виртуальные функции могут вызывать пропадание кеша при поиске, но это происходит только в том случае, если конкретная функция вызывается не часто (в противном случае она, вероятно, будет кэшироваться), поэтому некоторые считают ее бесполезной. Для справки об этой проблеме, посмотрите: Какова стоимость производительности наличия виртуального метода в классе C++?

Общие проблемы

Общая проблема в современных архитектурах с многопроцессорным кэшем называется ложным разделением. Это происходит, когда каждый отдельный процессор пытается использовать данные в другой области памяти и пытается сохранить их в той же строке кэша. Это приводит к тому, что строка кэша, содержащая данные, которые может использовать другой процессор, перезаписывается снова и снова. По сути, разные потоки заставляют друг друга ждать, вызывая пропуски кэша в этой ситуации. Смотрите также (спасибо @Matt за ссылку): Как и когда выровнять по размеру строки кэша?

Чрезвычайным признаком плохого кеширования в оперативной памяти (что, вероятно, не является тем, что вы имеете в виду в этом контексте) является так называемая порка. Это происходит, когда процесс постоянно генерирует сбои страниц (например, обращается к памяти, которой нет на текущей странице), которые требуют доступа к диску.

Ответ 2

В дополнение к ответу @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 градусов, а затем выполнять различные анализы, ограничивая недружественный код кэша только вращением.

Ответ 3

Оптимизация использования кеша во многом сводится к двум факторам.

Локальность ссылки

Первый фактор (к которому другие уже упоминали) - это локальность ссылки. На месте ссылки действительно есть два измерения: пространство и время.

  • Пространственное

Пространственный размер также сводится к двум вещам: во-первых, мы хотим плотно упаковать нашу информацию, поэтому дополнительная информация будет вписываться в ограниченную память. Это означает (например), что вам необходимо значительное улучшение вычислительной сложности, чтобы оправдать структуры данных на основе небольших узлов, соединенных указателями.

Во-вторых, нам нужна информация, которая будет обрабатываться вместе, также находящаяся вместе. Типичный кеш работает в "строках", что означает, что при доступе к некоторой информации другая информация в соседних адресах будет загружена в кеш с той частью, которую мы коснулись. Например, когда я касаюсь одного байта, кеш может загружать 128 или 256 байтов рядом с этим. Чтобы воспользоваться этим, вы, как правило, хотите, чтобы данные упорядочивались, чтобы максимизировать вероятность того, что вы также будете использовать другие данные, которые были загружены одновременно.

Для просто тривиального примера это может означать, что линейный поиск может быть гораздо более конкурентным с бинарным поиском, чем вы ожидали. Как только вы загрузили один элемент из строки кэша, остальная часть данных в этой строке кэша почти бесплатна. Бинарный поиск становится заметно быстрее, только когда данные достаточно велики, что двоичный поиск уменьшает количество доступных кэш-строк.

  • Время

Временной размер означает, что, когда вы выполняете некоторые операции над некоторыми данными, вы хотите (как можно больше) выполнять все операции над этими данными одновременно.

Поскольку вы отметили это как С++, я укажу на классический пример относительно недружественного дизайна с кэшем: 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

Обмен линиями

Второй важный фактор - избежать совместного использования строк. Чтобы понять это, нам, вероятно, нужно создать резервную копию и немного посмотреть, как организованы кэши. Простейшая форма кеша напрямую сопоставляется. Это означает, что один адрес в основной памяти может храниться только в одном конкретном месте в кеше. Если мы используем два элемента данных, которые сопоставляются с одним и тем же местом в кеше, он работает плохо - каждый раз, когда мы используем один элемент данных, другой должен быть сброшен из кеша, чтобы освободить место для другого. Остальная часть кеша может быть пуста, но эти элементы не будут использовать другие части кеша.

Чтобы предотвратить это, большинство кешей - это то, что называется "set associative". Например, в четырехпозиционном ассоциативно-ассоциативном кеше любой элемент из основной памяти может быть сохранен в любом из 4-х разных мест в кеше. Таким образом, когда кеш будет загружать элемент, он ищет наименее недавно используемый элемент 3 среди этих четырех, сбрасывает его в основную память и загружает новый элемент на свое место.

Проблема, вероятно, довольно очевидна: для кэша с прямым отображением два операнда, которые попадают в одно и то же расположение кэша, могут привести к плохому поведению. N-way set-ассоциативный кеш увеличивает число от 2 до N + 1. Организация кэша на более "путях" требует дополнительной схемы и обычно работает медленнее, поэтому (например) 8192-way set ассоциативный кеш - тоже редкое решение.

В конечном счете, этот фактор более сложно контролировать в переносном коде. Ваш контроль над местом размещения ваших данных обычно ограничен. Хуже того, точное сопоставление от адреса к кешу зависит от других аналогичных процессоров. В некоторых случаях, однако, может стоить делать такие вещи, как выделение большого буфера, а затем использовать только части того, что вы выделили, чтобы обеспечить совместное использование одних и тех же строк кеша (хотя вам, вероятно, потребуется определить точный процессор и действуйте соответственно для этого).

  • Ложное совместное использование

Там есть другой связанный элемент, называемый "ложным совместным использованием". Это возникает в многопроцессорной или многоядерной системе, где два (или более) процессора/ядра имеют данные, которые отделяются, но попадают в одну и ту же строку кэша. Это заставляет два процессора/ядра координировать их доступ к данным, хотя каждый из них имеет свой собственный отдельный элемент данных. Особенно, если эти два изменят данные в чередовании, это может привести к значительному замедлению, поскольку данные должны постоянно перекачиваться между процессорами. Это невозможно легко вылечить, организовав кэш более "путями" или что-то в этом роде. Основной способ предотвратить это состоит в том, чтобы гарантировать, что два потока редко (желательно никогда) не изменяют данные, которые могут находиться в одной и той же строке кэша (с теми же предостережениями о сложности управления адресами, на которых распределяются данные).


  • Те, кто хорошо знают С++, могут задаться вопросом, доступно ли это для оптимизации с помощью шаблонов выражений. Я уверен, что ответ: да, это можно сделать, и если бы это было так, это, вероятно, было бы довольно существенной победой. Однако я не знаю никого, кто сделал это, и, учитывая, как мало используется valarray, я был бы хотя бы немного удивлен, увидев, что кто-то тоже это сделает.

  • В случае, если кто-то задается вопросом, как valarray (разработанный специально для производительности) может быть настолько ошибочным, это сводится к одному: он был действительно разработан для таких машин, как более старые Crays, которые использовали быструю основную память и нет кеша. Для них это действительно был идеальный дизайн.

  • Да, я упрощаю: большинство кэшей не очень точно измеряют наименее недавно используемый товар, но они используют некоторую эвристику, которая должна быть близка к таковой, не сохраняя при этом полной отметки времени для каждого доступ.

Ответ 4

Добро пожаловать в мир Data-Oriented Design. Основная мантра - сортировка, удаление ветвей, пакетная обработка, удаление virtual звонков - все шаги к лучшей локации.

Поскольку вы пометили вопрос с C++, здесь обязательная типичная C++ фигня. Тони Альбрехт Подводные камни объектно-ориентированного программирования также являются отличным введением в предмет.

Ответ 5

Просто накапливаем: классический пример недружественного к кешу по сравнению с кеш-дружественным кодом - это "блокировка кеша" умножения матрицы.

Наивная матрица умножения выглядит так

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

Циклическая черепица очень тесно связана:

http://en.wikipedia.org/wiki/Loop_tiling

Ответ 6

Процессоры сегодня работают со многими уровнями каскадных областей памяти. Таким образом, у процессора будет куча памяти, которая сама по себе принадлежит к процессору. Он имеет очень быстрый доступ к этой памяти. Существуют разные уровни кеша, каждый из которых имеет более медленный доступ (и больше), чем следующий, до тех пор, пока вы не перейдете в системную память, которая не находится на процессоре, и относительно медленнее для доступа.

Логически, в набор инструкций CPU вы просто ссылаетесь на адреса памяти в гигантском виртуальном адресном пространстве. Когда вы обращаетесь к одному адресу памяти, CPU будет его извлекать. в прежние времена он получал бы только один адрес. Но сегодня процессор будет получать кучу памяти вокруг бит, который вы просили, и скопировать его в кеш. Он предполагает, что если вы попросите указать конкретный адрес, который, скорее всего, вы скоро попросите адрес поблизости. Например, если вы копировали буфер, который вы читали бы и записывали бы из последовательных адресов - один за другим.

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

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

Итак, пример мог бы представить, что вы хотели скопировать гигантскую двумерную таблицу. Он организован с рядом строк в последовательном порядке в памяти, а одна строка следующая справа после.

Если вы скопировали элементы по одной строке за раз слева направо - это было бы удобно для кэша. Если вы решили скопировать таблицу по одному столбцу за раз, вы скопировали бы тот же самый объем памяти - но это было бы недружественным кэшем.

Ответ 7

Необходимо уточнить, что не только данные должны быть кэшируемыми, но и столь же важны для кода. Это в дополнение к отраслевому предикату, переупорядочению команд, избегая фактических делений и других методов.

Как правило, чем плотнее код, тем меньше требуется для его хранения. Это приводит к тому, что для данных доступно больше строк кэша.

Код не должен вызывать функции повсюду, так как они, как правило, потребуют одну или несколько строк кэша, что приводит к меньшему количеству строк кэша для данных.

Функция должна начинаться с адреса, ориентированного на выравнивание строки. Хотя есть (gcc) ключи компилятора для этого, имейте в виду, что если функции очень короткие, может быть расточительным для каждого из них, чтобы занять всю строку кэша. Например, если три из наиболее часто используемых функций вписываются в одну строку с байтом в 64 байта, это менее расточительно, чем если каждая из них имеет свою собственную строку и приводит к двум линиям кэша, менее доступным для другого использования. Типичное значение выравнивания может быть 32 или 16.

Поэтому потратьте лишнее время, чтобы сделать код плотным. Протестируйте различные конструкции, скомпилируйте и просмотрите сгенерированный размер и профиль кода.

Ответ 8

Поскольку @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. */

Теперь после загрузки как массива структур (Row Layout), так и 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 смежных значений цен, хранящихся в памяти, поэтому для каждого шестнадцатого кортежа cache miss ocurs в случае расположения столбца.

Таким образом, макет столбца будет быстрее в случае заданного запроса и быстрее в таких агрегационных запросах на подмножество столбцов таблицы. Вы можете попробовать этот эксперимент для себя, используя данные TPC-H и сравнить время выполнения для обоих макетов. Кроме того, хорошая статья wikipedia для систем баз данных, ориентированных на столбцы.

Поэтому в системах баз данных, если рабочая нагрузка запроса известна заранее, мы можем хранить наши данные в макетах, которые будут соответствовать запросам в рабочей нагрузке и получать доступ к данным из этих макетов. В случае вышеприведенного примера мы создали макет столбца и изменили наш код, чтобы вычислить сумму, чтобы он стал дружественным к кэшу.

Ответ 9

Помните, что кэши не просто кэшируют непрерывную память. Они имеют несколько строк (по крайней мере 4), поэтому часто прерывается и перекрывающаяся память может быть сохранена так же эффективно.

То, что отсутствует во всех приведенных выше примерах, является измеренным эталоном. Существует много мифов о производительности. Если вы не измеряете это, вы не знаете. Не усложняйте свой код, если у вас нет заметного улучшения.