Кэш-линии
Если Вы хотите создать высокоэффективное приложение, работающее на многопро цессорных машинах, то просто обязаны уметь пользоваться кэш-линиями процессо ра (CPU cache lines). Когда процессору нужно считать из памяти один байт, он извле кает не только сго, но и столько смежных байтов, сколько требуется для заполнения кэш-линии Такие линии состоят из 32 или 64 байтов (в зависимости от типа процес сора) и всегда выравниваются по границам, кратным 32 или 64 байтам. Кэш-линии предназначены для повышения быстродействия процессора. Обычно приложение работает с набором смежных байтов, и, если эти байты уже находятся в кэше, про цессору не приходится снова обращаться к шине памяти, что обеспечивает существен ную экономию времени.
Однако кэш-линии сильно усложняют обновление памяти в многопроцессорной среде. Вот небольшой пример:
Такой сценарий был бы настоящей катастрофой. Но разработчики чипов прекрас но осведомлены об этой проблеме и учитывают cc при проектировании своих про цессоров В частности, когда один из процессоров модифицирует байты в своей кэш линии, об этом оповещаются другие процессоры, и содержимое их кэш-линий объяв ляется недействительным. Таким образом, в примере, приведенном выше, после из менения байта процессором 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:
Лучше всего, когда данные используются единственным потоком (самый про стой способ добиться этого — применять параметры функций и локальные пе ременные) или одним процессором (это реализуется привязкой потока к оп ределенному процессору). Если Бы пойдете по такому пути, можете вообще забыть о проблемах, связанных с кэш-линиями.