Что такое «дружественный к кешу» код?

649

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

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

  • 26
    Это может дать вам подсказку: stackoverflow.com/questions/9936132/…
  • 3
    Также следует учитывать размер строки кэша. На современных процессорах это часто 64 байта.
Показать ещё 5 комментариев
Теги:
performance
caching
memory
cpu-cache

9 ответов

803
Лучший ответ

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

На современных компьютерах только структуры памяти самого низкого уровня (регистры) могут перемещать данные за один такт. Однако регистры очень дороги, и большинство ядер компьютеров имеют менее нескольких десятков регистров (всего от нескольких сотен до, возможно, тысячи байт). На другом конце спектра памяти (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 за ссылку): Как и когда выровнять по размеру строки кэша?

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

  • 23
    возможно, вы могли бы немного расширить ответ, объяснив, что в многопоточном коде данные также могут быть слишком локальными (например, ложное совместное использование)
  • 0
    Каждому уровню доступа нужен свой уровень кеша? L1 l2 l3 l4 есть ли l5?
Показать ещё 18 комментариев
133

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

  • 10
    @RafaelBaptista: ну, это классический :)
  • 0
    Err, это должно быть х <ширина?
Показать ещё 5 комментариев
75

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

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

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

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

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

Во-вторых, нам нужна информация, которая будет обрабатываться вместе, также находящаяся вместе. Типичный кеш работает в "строках", что означает, что при доступе к некоторой информации другая информация в соседних адресах будет загружена в кеш с той частью, которую мы коснулись. Например, когда я касаюсь одного байта, кеш может загружать 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, которые использовали быструю основную память и нет кеша. Для них это действительно был идеальный дизайн.

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

  • 1
    Мне нравятся дополнительные фрагменты информации в вашем ответе, особенно пример valarray .
  • 0
    +1 Наконец: простое описание заданной ассоциативности! РЕДАКТИРОВАТЬ дальше: Это один из самых информативных ответов на SO. Спасибо.
30

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

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

  • 1
    что вы подразумеваете под партией, можно не понять.
  • 5
    Пакетирование: вместо выполнения единицы работы над отдельным объектом, выполняйте ее для пакета объектов.
Показать ещё 5 комментариев
22

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

Наивная матрица умножается на

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==;k<N;i++) {
         for(jj=0;jj<itemsPerCacheLine; jj+) {
            dest[i][j+jj] += src1[i][k] * src2[k][j+jj];
         }
      }
   }
}

Если размер строки кеша составляет 64 байта, и мы работаем с 32-битными (4 байтовыми) поплавками, тогда в каждой строке кэша должно быть 16 элементов. И количество промахов в кеше через простое преобразование сокращается примерно в 16 раз.

Преобразования Fancier работают с 2D-плитами, оптимизируются для нескольких кешей (L1, L2, TLB) и т.д.

Некоторые результаты поиска "блокировки кеша" в googling:

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
    Людям, которые читают это, также может быть интересна моя статья о умножении матриц, где я протестировал «дружественный к кешу» алгоритм ikj и недружественный алгоритм ijk, умножив две матрицы 2000x2000.
13

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

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

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

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

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

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

4

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

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

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

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

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

2

Поскольку @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 для систем баз данных, ориентированных на столбцы.

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

0

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню