Windows для профессионалов

Кэш-линии


Если Вы хотите создать высокоэффективное приложение, работающее на многопроцессорных машинах, то просто обязаны уметь пользоваться кэш-линиями процессора (CPU cache lines). Когда процессору нужно считать из памяти один байт, он извлекает не только его, но и столько смежных байтов, сколько требуется для заполнения кэш-линии. Такие линии состоят из 32 или 64 байтов (в зависимости от типа процессора) и всегда выравниваются по границам, кратным 32 или 64 байтам. Кэш-линии предназначены для повышения быстродействия процессора. Обычно приложение работает с набором смежных байтов, и, если эти байты уже находятся в кэше, процессору не приходится снова обращаться к шине памяти, что обеспечивает существенную экономию времени.

Однако кэш-линии сильно усложняют обновление памяти в многопроцессорной среде. Вот небольшой пример:

  • Процессор 1 считывает байт, извлекая этот и смежные байты в свою кэш-линию.
  • Процессор 2 считывает тот же байт, а значит, и тот же набор байтов, что и процессор 1; извлеченные байты помещаются в кэш-линию процессора 2.
  • Процессор 1 модифицирует байт памяти, и этот байт записывается в его кэш-линию. Но эти изменения еще не записаны в оперативную память.
  • Процессор 2 повторно считывает тот же байт Поскольку он уже помещен в кэш-линию этого процессора, последний не обращается к памяти и, следова тельно, не "видит" новое значение данного байта.
  • Такой сценарий был бы настоящей катастрофой. Но разработчики чипов прекрасно осведомлены об этой проблеме и учитывают её при проектировании своих процессоров. В частности, когда один из процессоров модифицирует байты в своей кэш-линии, об этом оповещаются другие процессоры, и содержимое их кэш-линий объявляется недействительным. Таким образом, в примере, приведенном выше, после изменения байта процессором 1, кэш процессора 2 был бы объявлен недействительным. На этапе 4 процессор 1 должен сбросить содержимое своего кэша в оперативную память, а процессор 2 — повторно обратиться к памяти и вновь заполнить свою кэш-линию.
    Как видите, кэш-линии, которые, как правило, увеличивают быстродействие процессора, в многопроцессорных машинах могут стать причиной снижения производительности.

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

    Вот пример плохо продуманной структуры данных:

    struct CUSTINFO
    {

    DWORD dwCustomerID;
    // в основном "только для чтения1 int nBalanceDue,
    // для чтения и записи char szName[100],
    // в основном "только для чтения" FILETIME ttLastOrderDate;
    // для чтения и записи
    };

    А это усовершенствованная версия той же структуры.

    // определяем размер кэш-линии используемого процессора

    #ifdef _X86_
    #define CACHE_ALIGN 32
    #endif

    #ifdef _ALPHA_
    #define CACHE_ALIGN 64
    #endif

    #ifdef _IA64_
    #define CACHE_ALIGN ??
    #endif

    #define CACHE_PAD(Name, BytesSoFar) BYTE Name[CACHE_ALIGN - ((BytesSoFar) % CACHE_ALIGN)]

    struct CUSTINFO
    {
    DWORD dwCustomerID;
    // в осноеном "только для чтения"
    char szName[100];
    // в основном "только для чтения"

    // принудительно помещаем следующие элементы в другую кэш-линию
    CACHE_PAD(bPad1, sizeof(DWORD) + 100);

    int nBalanceDue;
    // для чтения и записи
    FILETIME ftLastOrderDate;
    // для чтения и записи

    // принудительно помещаем следующую структуру в другую кэш-линию
    CACHE_PAD(bPad2, sizeof(int) + sizeof(FILETIME));

    };

    Макрос CACHE_ALIGN неплох, но не идеален. Проблема в том, что байтовый размер каждого элемента придется вводить в макрос вручную, а при добавлении, перемещении или удалении элемента структуры — еще и модифицировать вызов макроса CACHE_PAD.В следующих версиях компилятор Microsoft C/C++ будет поддерживать новый синтаксис, упрощающий выравнивание элементов структур. Это будет что-то вроде __declepec(align(32)).

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


    Содержание раздела