RAM - не RAM, или Cache-Conscious Data Structures

remark remark

RAM — не RAM


Как это ни прискорбно признавать, но то, что мы называем RAM (random-access memory) уже давно не является RAM. По сути это уже память с последовательным доступом. Как магнитная лента. Здесь я имею в виду RAM не в смысле физическое устройство, а в смысле подсистема памяти компьютера, которая состоит из собственно основной памяти и нескольких уровней кэша.

Приблизительные цифры скорости доступа к различным уровням памяти на современных архитектурах:
Кэш L1 — 1-3 такта
Кэш L2 — 10-20 тактов
Основная память — 200-250 тактов

Что это означает для нас, программистов? То, что RAM — не RAM и абстракция "прозрачного" кэша начинает серьёзно "просвечивать". В пределе, при максимально плохом стечении обстоятельств при доступе к памяти, процессор с частотой 2.5 GHz фактически будет работать на частоте 10 MHz. Как Вам? В реальности, конечно, такого не будет даже в самых плохих условиях. Но разницу производительности в ~10 раз получить можно легко.

Умножение матриц

Возьмём вот такую простенькую функцию перемножения матриц:
void multiply(T* X, T* Y, T* Z, int size)
{
    for (int i = 0; i != size; ++i)
        for (int j = 0; j != size; ++j)
            for (int k = 0; k != size; ++k)
                Z[i*size+j] += X[i*size+k] * Y[k*size+j];
}


Запускаю для матриц размера 512 — результат 16.9 секунды.
Запускаю для матриц размера 513... делайте ставки, господа! Результат 4.8 секунды!

Причина кроется в паттерне прохода по матрице Y во внутреннем цикле. При размере матрицы 512 размер строки матрицы составляет 4096 байт (8-байтовые элементы). Соответственно адреса элементов Y[0][0] и Y[1][0] имеют одинаковые младшие 12 бит. Это вызывает постоянные конфликты в кэше с вытеснением старых данных.

Теперь для размера матрицы 513 попробуем попереставлять циклы местами. Результаты следующие:
k, j, i — 6.8 сек (для размера 512 время 34.1 сек)
j, k, i — 6.8 сек
i, j, k — 4.8 сек
j, i, k — 3.6 сек
k, i, j — 2.5 сек
i, k, j — 2.4 сек
Занятно. Итого: индексы (k, j, i), размер 512, время — 34.1; индексы (i, k, j), размер 513, время — 2.4. Ускорение — 14.2 раза. А вроде как ничего и не делали.
(матрицы перемножать с помощью этого кода скорее всего не стоит — лучше воспользоваться оптимизированными библиотеками, тут я хотел показать только эффекты от различных паттернов обращения к памяти)

Вкратце рецепт такой — по массивам надо проходить последовательно; учитывать количесто ассоциативных входов в кэш (обычно это 2 или 4); учитывать значащие биты в адресе, которые определяют индекс в кэше. Более подробно здесь:
http://en.wikipedia.org/wiki/CPU_cache

Cache-Conscious Binary Search

Это была разминка, теперь более интересные и нетривиальные вещи.
Как организовать быстрый бинарный поиск при большом объёме данных? Ответ теоретика — с помощью бинарного дерева. Ну ладно, простим ему это. Конечно, с помощью упорядоченного массива. На маленьком объеме данных это будет работать хорошо, но вот на большом — не очень. Что бы понять почему, надо рассмотреть как происходит перемещение "точки поиска" по массиву в вслучае бинарного поиска. Вначале точка устанавливается на середину (1/2), потом "скачет" либо на 1/4, либо на 3/4, потом — на 1/8, 3/8, 5/8, 7/8 и т.д. Т.е. вначале "скачки" очень большие и хаотические. При этом из каждой
загруженной кэш-линии используется в основном только один элемент, ближайшие к нему элементы не используются.
Попробуем исправить это, попробуем сделать т.ч. "скачки" были по-возможности маленькие, и при этом из каждой загруженной кэш-линии использовалось как можно больше элементов.
Упорядочиваем массив следующим образом. В первую кэш-линию (допустим в кэш-линию помещается 8 элементов) кладём серединный элемент 1/2, следом за ним кладём элементы, на которые мы можем перескачить с 1/2, т.е. 1/4 и 3/4. Далее кладём элементы, на которые мы можем перескачить с 1/4 и 3/4, т.е. 1/8, 3/8, 5/8, 7/8. Таким образом, используя элементы только из первой кэш-линии, мы можем сделать 3 "скачка", при этом из неё будет использовано 3 элемента, а не 1 как ранее.
Далее элементы пакуются аналогичным образом, т.е. в кэш линии лежит некий начальный элемент; элементы, на которые можно "перескачить" с него; и элементы, на которые можно перескачить с них и т.д.
На синтетических бенчмарках такой метод дают ускорение в 4-5 раз, в реальных приложениях есть сведения об ускорении на 25-40%.

Hot/Cold Data Splitting

Посмотрим, что ещё можно сделать с нашим бинарным поиском.
Допустим, в массиве у нас храняться следующие структуры:
struct X
{
  int key;
  unsigned char some_data [28];
};

При этом при поиске используется только поле key. Размер структуры 32 байта, т.е. массив будет большой и толстый, и при поиске в нем неминуемо в кэш будет загружаться и some_data, которая вобщем-то нам совершенно не нужна, пока мы не нашли один искомый элемент.
Что мы делаем — мы разбиваем нашу структуру на 2:
struct X_hot
{
  int key;
};
struct X_cold
{
  unsigned char some_data [28];
};

Кладём в массив, в котором мы производим поиск только X_hot, а X_cold выносим в отдельный параллельный массив, в котором элементы упорядочены так же, т.е. по номеру элемента X_hot без труда можно получить и элемент X_cold.
Теперь поисковый массив у нас маленький и тонкий, в данном случае меньше в 8 раз (возможно в таком виде он целиком уместится в кэш L2), поиск в нём идёт быстрее. А когда нашли индекс искомого элемента, то загружаем и один нужный X_cold со всеми остальными данными.

Управление кэшем

Кэш не полностью управляется процессором — можно попробовать поуправлять им вручную. Хотя обычно этого делать не стоит. Я вкраце опишу какие средства есть на современных процессорах Intel/AMD x86. В любом случае любое применение таких средств надо тщательно профилировать, т.к. априори предугадать все эффекты сложно.

prefetcht0 (t1, t2, nta)
В MSVC инструкцию prefetcht0 реализует следующий интринсик:
void _mm_prefetch(char * p , int hint ); // hint = _MM_HINT_T0, _MM_HINT_T1, _MM_HINT_T2, _MM_HINT_NTA

Команде передаётся адрес, данные по которому будут загружены в кэш (загружается, естественно вся кэш-линия — сейчас обычно 64 байта). Так же команде передаётся хинт относительно предполагаемого использования этих данных. _MM_HINT_T0, _MM_HINT_T1, _MM_HINT_T2 скорее всего не различаются и загружают данные в кэш L2. _MM_HINT_NTA более интересен, он говорит, что данные нужны на короткое время. При этом данные загружаются в кэш L1, и помечаются специальным тэгом, который говорит, что при вытеснении из L1 данные не надо перемещать в L2 — из просто надо "выкинуть". Т.о. не происходит т.н. "загрязнения" кэша. Очень полезно при потоковой обработке больших массивов.

movnti/movntq
В MSVC инструкцию movnti реализует следующий интринсик:
void _mm_stream_si32(int *p, int a)

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

clflush
void _mm_clflush(void const*p)

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

Почему не надо вручную управлять кэшем. Потому что процессор имеет аппаратный предвыборщик данных, который в большинстве случаев делает свою работу достаточно хорошо. Сделать лучше, чем он, можно. Но легче сделать хуже. Когда можно заниматься ручным управлением кэшем — при потоковой обработке больших массивов данных (в т.ч. и при просто копировании памяти). В общем случае данные подгружаются на 4-8 строк кэша вперёд с помощью инструкции prefetchnta, и сохраняются с помощью movnti/movntq. В таком случае можно получить ускорение в 10 раз и более.

Более подробно здесь:
Intel® 64 and IA-32 Architectures Software Developer’s Manual: Instruction Set Reference
http://www.intel.com/design/processor/manuals/253666.pdf
http://www.intel.com/design/processor/manuals/253667.pdf


Факультативное чтение по поводу Cache-Conscious Data Structures. В т.ч. описаны и другие приёмы — Structure Field Reorder, Cache Colouring, Cache-Conscious Heap Allocation.

Cache-Conscious Data Placement:
http://citeseer.ist.psu.edu/cache/papers/cs/130/http:zSzzSzwww.cse.ucsd.eduzSz~calderzSzpaperszSzASPLOS-98-CCDP.pdf/calder98cacheconscious.pdf

Cache-Conscious Structure Definition:
http://research.microsoft.com/~trishulc/papers/definition_distr.ps
(занятно — люди из Microsoft Research занимаются Java)

Cache-Conscious Structure Layout:
http://research.microsoft.com/~trishulc/papers/layout_distr.ps

Cache-conscious Frequent Pattern Mining on a Modern Processor:
http://www.vldb2005.org/program/paper/thu/p577-ghoting.pdf

Cache-Conscious Concurrency Control of Main-Memory Indexes on Shared-Memory Multiprocessor Systems:
http://www.vldb.org/conf/2001/P181.pdf

Cache-Conscious Allocation of Pointer-Based Data Structures Revisited with HW/SWPrefetching:
http://www.ece.wisc.edu/~wddd/2003/01_hallberg.pdf


SergH
SergH
25.04.2008 08:55
Здравствуйте, remark, Вы писали:

Слушай, ты бы не ленился, а писал статьи, а? А то нам в журнале печатать нечего, а ты тут фонтанируешь

R>Упорядочиваем массив следующим образом. В первую кэш-линию (допустим в кэш-линию помещается 8 элементов) кладём серединный элемент 1/2, следом за ним кладём элементы, на которые мы можем перескачить с 1/2, т.е. 1/4 и 3/4. Далее кладём элементы, на которые мы можем перескачить с 1/4 и 3/4, т.е. 1/8, 3/8, 5/8, 7/8. Таким образом, используя элементы только из первой кэш-линии, мы можем сделать 3 "скачка", при этом из неё будет использовано 3 элемента, а не 1 как ранее.


И получаем бинарное дерево, о котором говорил теоретик Правда, без указателей, но тем не менее.
remark
remark
25.04.2008 10:56
Здравствуйте, SergH, Вы писали:

SH>Здравствуйте, remark, Вы писали:


SH>Слушай, ты бы не ленился, а писал статьи, а? А то нам в журнале печатать нечего, а ты тут фонтанируешь



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


R>>Упорядочиваем массив следующим образом. В первую кэш-линию (допустим в кэш-линию помещается 8 элементов) кладём серединный элемент 1/2, следом за ним кладём элементы, на которые мы можем перескачить с 1/2, т.е. 1/4 и 3/4. Далее кладём элементы, на которые мы можем перескачить с 1/4 и 3/4, т.е. 1/8, 3/8, 5/8, 7/8. Таким образом, используя элементы только из первой кэш-линии, мы можем сделать 3 "скачка", при этом из неё будет использовано 3 элемента, а не 1 как ранее.


SH>И получаем бинарное дерево, о котором говорил теоретик Правда, без указателей, но тем не менее.



Упорядоченный массив сам по себе с определенной высоты — бинарное дерево...


SergH
SergH
26.04.2008 10:19
Здравствуйте, remark, Вы писали:

R>Я подозреваю, что на написание статей будет уходить значительно больше времени.


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

R>В любом случае, права на публикацию я готов предоставить. Если у кого есть время всё это оформить, то пожалуйста.


Добрый такой Сделал всю интересную часть работы, а теперь "кто хочет, можете поисправлять баги в моём проекте"
andy1618
andy1618
26.04.2008 03:03
SH>Слушай, ты бы не ленился, а писал статьи, а?

Во-во, у меня такая же мысль возникла — прогнать текст через спелчекер и в журнал какой-нибудь!
Alex Alexandrov
Alex Alexandrov
26.04.2008 08:11
Здравствуйте, SergH, Вы писали:

R>>Упорядочиваем массив следующим образом. В первую кэш-линию (допустим в кэш-линию помещается 8 элементов) кладём серединный элемент 1/2, следом за ним кладём элементы, на которые мы можем перескачить с 1/2, т.е. 1/4 и 3/4. Далее кладём элементы, на которые мы можем перескачить с 1/4 и 3/4, т.е. 1/8, 3/8, 5/8, 7/8. Таким образом, используя элементы только из первой кэш-линии, мы можем сделать 3 "скачка", при этом из неё будет использовано 3 элемента, а не 1 как ранее.


SH>И получаем бинарное дерево, о котором говорил теоретик Правда, без указателей, но тем не менее.


Получаем, действительно, бинарное дерево. А если еще точнее — heap в его классическом представлении.
remark
remark
26.04.2008 09:51
Здравствуйте, Alex Alexandrov, Вы писали:

AA>Здравствуйте, SergH, Вы писали:


R>>>Упорядочиваем массив следующим образом. В первую кэш-линию (допустим в кэш-линию помещается 8 элементов) кладём серединный элемент 1/2, следом за ним кладём элементы, на которые мы можем перескачить с 1/2, т.е. 1/4 и 3/4. Далее кладём элементы, на которые мы можем перескачить с 1/4 и 3/4, т.е. 1/8, 3/8, 5/8, 7/8. Таким образом, используя элементы только из первой кэш-линии, мы можем сделать 3 "скачка", при этом из неё будет использовано 3 элемента, а не 1 как ранее.


SH>>И получаем бинарное дерево, о котором говорил теоретик Правда, без указателей, но тем не менее.


AA>Получаем, действительно, бинарное дерево. А если еще точнее — heap в его классическом представлении.


heap is a specialized tree-based data structure that satisfies the heap property: if B is a child node of A, then key(A) ≥ key(B)

http://en.wikipedia.org/wiki/Heap_%28data_structure%29

Тут ключи и убывают и возрастают, т.е. если первый элемент 64, то второй — 32, третьий — 96


Alex Alexandrov
Alex Alexandrov
27.04.2008 02:09
Здравствуйте, remark, Вы писали:

R>Здравствуйте, Alex Alexandrov, Вы писали:


AA>>Здравствуйте, SergH, Вы писали:


R>>>>Упорядочиваем массив следующим образом. В первую кэш-линию (допустим в кэш-линию помещается 8 элементов) кладём серединный элемент 1/2, следом за ним кладём элементы, на которые мы можем перескачить с 1/2, т.е. 1/4 и 3/4. Далее кладём элементы, на которые мы можем перескачить с 1/4 и 3/4, т.е. 1/8, 3/8, 5/8, 7/8. Таким образом, используя элементы только из первой кэш-линии, мы можем сделать 3 "скачка", при этом из неё будет использовано 3 элемента, а не 1 как ранее.


SH>>>И получаем бинарное дерево, о котором говорил теоретик Правда, без указателей, но тем не менее.


AA>>Получаем, действительно, бинарное дерево. А если еще точнее — heap в его классическом представлении.


R>

R>heap is a specialized tree-based data structure that satisfies the heap property: if B is a child node of A, then key(A) ≥ key(B)

R>http://en.wikipedia.org/wiki/Heap_%28data_structure%29

R>Тут ключи и убывают и возрастают, т.е. если первый элемент 64, то второй — 32, третьий — 96

R>

Если совсем точно, то я имел в виду binary heap. В Википедии она упомянута здесь — http://en.wikipedia.org/wiki/Binary_heap. Картинка приведенная там в подразделе "Implementation" совпадает с описанием порядка, которые ты дал выше. Массив хранится в порядке обхода бинарного дерева в ширину.

R>Тут ключи и убывают и возрастают, т.е. если первый элемент 64, то второй — 32, третьий — 96


Ты здесь про свое описание говоришь, или про описание кучи? Еще раз, я упустил уточнение, что я имел в виду Binary Heap. Для бинарной кучи реализованной в виде массива действует правило

If the tree root item has index 0 (n tree elements are a[0] .. a[n−1]), then for each index i, element a[i] has children a[2i+1] and a[2i+2], and the parent a[floor((i−1)/2)]


Т.е. как раз и получается 1/2, 1/4, 3/4, 1/8, 3/8, 5/8, 7/8 вроде, нет?
remark
remark
28.04.2008 11:17
Здравствуйте, Alex Alexandrov, Вы писали:


AA>Если совсем точно, то я имел в виду binary heap. В Википедии она упомянута здесь — http://en.wikipedia.org/wiki/Binary_heap. Картинка приведенная там в подразделе "Implementation" совпадает с описанием порядка, которые ты дал выше. Массив хранится в порядке обхода бинарного дерева в ширину.


R>>Тут ключи и убывают и возрастают, т.е. если первый элемент 64, то второй — 32, третьий — 96


AA>Ты здесь про свое описание говоришь, или про описание кучи? Еще раз, я упустил уточнение, что я имел в виду Binary Heap. Для бинарной кучи реализованной в виде массива действует правило


AA>

AA>If the tree root item has index 0 (n tree elements are a[0] .. a[n−1]), then for each index i, element a[i] has children a[2i+1] and a[2i+2], and the parent a[floor((i−1)/2)]


AA>Т.е. как раз и получается 1/2, 1/4, 3/4, 1/8, 3/8, 5/8, 7/8 вроде, нет?



Не, тут немного другой порядок получается, не такой регулярный. Тут один порядок внутри кэш-линии, и другой порядок между кэш-линиями. Как бы binary heap из binary heap'ов. Как следствие и вначале, и в середине, и в конце дерева есть участки с очень короткими переходами. В binary heap переходы с каждом разом всё длиннее и длиннее.


Кодт
Кодт
13.05.2008 04:03
Здравствуйте, remark, Вы писали:

R>Не, тут немного другой порядок получается, не такой регулярный. Тут один порядок внутри кэш-линии, и другой порядок между кэш-линиями. Как бы binary heap из binary heap'ов. Как следствие и вначале, и в середине, и в конце дерева есть участки с очень короткими переходами. В binary heap переходы с каждом разом всё длиннее и длиннее.


Это не бинарное дерево и не куча.
Это больше похоже на Б-дерево (которое как раз было придумано для внешней памяти с блочным доступом).
... << RSDN@Home 1.2.0 alpha rev. 655>>
remark
remark
13.05.2008 05:56
Здравствуйте, Кодт, Вы писали:

К>Здравствуйте, remark, Вы писали:


R>>Не, тут немного другой порядок получается, не такой регулярный. Тут один порядок внутри кэш-линии, и другой порядок между кэш-линиями. Как бы binary heap из binary heap'ов. Как следствие и вначале, и в середине, и в конце дерева есть участки с очень короткими переходами. В binary heap переходы с каждом разом всё длиннее и длиннее.


К>Это не бинарное дерево и не куча.

К>Это больше похоже на Б-дерево (которое как раз было придумано для внешней памяти с блочным доступом).


Бинарное дерево — это частный случай Б-дерева, правильно?
То, что я описал, вроде не похоже на 2-Б-дерево... Б-дерево регулярно по всей высоте, а это — нет...


SiGMan / iO UpG
SiGMan / iO UpG
28.04.2008 04:22
Здравствуйте, remark, Вы писали:

R>RAM — не RAM


Есть такой проект Copenhagen STL, по оптимизации представления структур данных , в т.ч. и в плане Cache-Conscious'ness. http://www.cphstl.dk/
На сайте имеются интересные публикации и отчеты по теме:
http://www.cphstl.dk/WWW/papers.html
http://www.cphstl.dk/WWW/reports.html

Кодёнок
Кодёнок
28.04.2008 05:37
Здравствуйте, remark, Вы писали:

R>Факультативное чтение по поводу Cache-Conscious Data Structures. В т.ч. описаны и другие приёмы — Structure Field Reorder, Cache Colouring, Cache-Conscious Heap Allocation.


Кеш памяти предугадывает действия софта, софт предугадывает поведение кеша... я один кто думает, что что-то тут не так?
Sinclair
Sinclair
28.04.2008 06:35
Здравствуйте, Кодёнок, Вы писали:
Кё>Кеш памяти предугадывает действия софта, софт предугадывает поведение кеша... я один кто думает, что что-то тут не так?
Всё в порядке до тех пор, пока оба предугадывания направлены на достижение одной и той же цели. Характерный пример такой "кооперации без диалога" приведен в фильме "Место встречи изменить нельзя".
... << RSDN@Home 1.2.0 alpha rev. 677>>
Lazy Cjow Rhrr
Lazy Cjow Rhrr
28.04.2008 06:41
Кодёнок,

R>>Факультативное чтение по поводу Cache-Conscious Data Structures. В т.ч. описаны и другие приёмы — Structure Field Reorder, Cache Colouring, Cache-Conscious Heap Allocation.


Кё>Кеш памяти предугадывает действия софта, софт предугадывает поведение кеша... я один кто думает, что что-то тут не так?


Ещё одно подтверждение того, что уничтожение (или обход) слоя абстракции можно использовать для оптимизации.
Sinclair
Sinclair
29.04.2008 06:53
Здравствуйте, remark, Вы писали:

А почему не использовать B-tree, столь хорошо зарекомендовавшие себя в СУБД?
Есть же целый класс структур и алгоритмов, которые оптимизированы с учетом заметной стоимости подкачки "страницы" в "кэш".
... << RSDN@Home 1.2.0 alpha rev. 677>>
remark
remark
30.04.2008 01:25
Здравствуйте, Sinclair, Вы писали:

S>Здравствуйте, remark, Вы писали:


S>А почему не использовать B-tree, столь хорошо зарекомендовавшие себя в СУБД?

S>Есть же целый класс структур и алгоритмов, которые оптимизированы с учетом заметной стоимости подкачки "страницы" в "кэш".


Можно использовать B-tree. Реализация может быть разная. Но суть остаётся. Нельзя просто взять B-tree и получить требуемое поведение. Надо выбрать кол-во элементов в узле, исходя из того, сколько помещается в кэш-линию. Надо выравнивать узлы по кэш-линиям. Размер кэш-линии различается от платформы к платформе. Т.е. всё равно надо учитывать низкоуровневые детали кэша, это может быть достаточно непривычно для многих программистов.


WolfHound
WolfHound
30.04.2008 02:21
Здравствуйте, remark, Вы писали:

R>Можно использовать B-tree. Реализация может быть разная. Но суть остаётся. Нельзя просто взять B-tree и получить требуемое поведение. Надо выбрать кол-во элементов в узле, исходя из того, сколько помещается в кэш-линию. Надо выравнивать узлы по кэш-линиям. Размер кэш-линии различается от платформы к платформе. Т.е. всё равно надо учитывать низкоуровневые детали кэша, это может быть достаточно непривычно для многих программистов.

А как можно програмно узнать размер кеш линии?
А то у меня есть пара алгоритмов которым от этого знания может полегчать.
... << RSDN@Home 1.2.0 alpha rev. 745>>
remark
remark
30.04.2008 02:36
Здравствуйте, WolfHound, Вы писали:

WH>Здравствуйте, remark, Вы писали:


R>>Можно использовать B-tree. Реализация может быть разная. Но суть остаётся. Нельзя просто взять B-tree и получить требуемое поведение. Надо выбрать кол-во элементов в узле, исходя из того, сколько помещается в кэш-линию. Надо выравнивать узлы по кэш-линиям. Размер кэш-линии различается от платформы к платформе. Т.е. всё равно надо учитывать низкоуровневые детали кэша, это может быть достаточно непривычно для многих программистов.


WH>А как можно програмно узнать размер кеш линии?

WH>А то у меня есть пара алгоритмов которым от этого знания может полегчать.


Это платформенно-зависимо. Обычно процессор предоставляет такую информацию.
Например на x86 смотри инструкцию cpuid, с аргументом eax=4 можно получить всю информацию по кэшу. Так же там есть упрощенный вариант при eax=1 — в ebx биты 15-8. В MSVC смотри интринсик __cpuid(), в Intel C++ тоже есть что-то аналогичное.

На Windows (начиная с Server2003) есть функция GetLogicalProcessorInformation(), с помощью которой можно получить размер кэшей.


remark
remark
30.04.2008 02:43
Здравствуйте, remark, Вы писали:

R>Здравствуйте, WolfHound, Вы писали:


WH>>Здравствуйте, remark, Вы писали:


R>>>Можно использовать B-tree. Реализация может быть разная. Но суть остаётся. Нельзя просто взять B-tree и получить требуемое поведение. Надо выбрать кол-во элементов в узле, исходя из того, сколько помещается в кэш-линию. Надо выравнивать узлы по кэш-линиям. Размер кэш-линии различается от платформы к платформе. Т.е. всё равно надо учитывать низкоуровневые детали кэша, это может быть достаточно непривычно для многих программистов.


WH>>А как можно програмно узнать размер кеш линии?

WH>>А то у меня есть пара алгоритмов которым от этого знания может полегчать.


R>Это платформенно-зависимо. Обычно процессор предоставляет такую информацию.

R>Например на x86 смотри инструкцию cpuid, с аргументом eax=4 можно получить всю информацию по кэшу. Так же там есть упрощенный вариант при eax=1 — в ebx биты 15-8. В MSVC смотри интринсик __cpuid(), в Intel C++ тоже есть что-то аналогичное.

R>На Windows (начиная с Server2003) есть функция GetLogicalProcessorInformation(), с помощью которой можно получить размер кэшей.



Да, обычно сейчас на большинстве x86 это 64 байта.


R>

WolfHound
WolfHound
30.04.2008 02:58
Здравствуйте, remark, Вы писали:

R>Это платформенно-зависимо. Обычно процессор предоставляет такую информацию.

Печально все это. Возьня того не стоит.
Вот еслиб ктонибудь сделал кросплатформенную библиотечку с простым сишным интерфейсом.
Думаю народ был бы благодарен.
... << RSDN@Home 1.2.0 alpha rev. 745>>
longlivedeath
longlivedeath
15.05.2008 04:34
Здравствуйте, WolfHound, Вы писали:

WH>А как можно програмно узнать размер кеш линии?

WH>А то у меня есть пара алгоритмов которым от этого знания может полегчать.

Существует библиотека cacheprobe для этого дела — дипломная работа одного товарища из нашего института:
http://www.cs.umu.se/~dva99ggn/thesis.html
remark
remark
15.05.2008 05:13
Здравствуйте, longlivedeath, Вы писали:

L>Здравствуйте, WolfHound, Вы писали:


WH>>А как можно програмно узнать размер кеш линии?

WH>>А то у меня есть пара алгоритмов которым от этого знания может полегчать.

L>Существует библиотека cacheprobe для этого дела — дипломная работа одного товарища из нашего института:

L>http://www.cs.umu.se/~dva99ggn/thesis.html


Там же используется метод "зондирования". Мне кажется это не совсем то, чего хотелось бы. Там в выводах так и написано, что работает не на всех платформах. Хотелось бы именно платформенно-зависимого решения, но что б было портировано на большинство платформ. Хотя как ресёрч это, конечно, занятно...


VladD2
VladD2
15.05.2008 10:16
Здравствуйте, remark, Вы писали:

R>Можно использовать B-tree. Реализация может быть разная. Но суть остаётся. Нельзя просто взять B-tree и получить требуемое поведение.


Есть довольно четко определенный B+ tree где как раз учитываются особенности оговоренные в "заметке" (кстати, статью бы из нее сделать). В частности дерево состоит сегментов (bucket-ов) содержащих только ключи и ссылки на другие сегменты. Сегменты содержат близкие ключи, так что попадают в кэш. Ну, и сами сегменты обрабатываются как непрерывные части памяти, что опять же положительно влияет на попадание в кэш.

Так вот все это здорово, но есть одно "но". Если речь идет именно о поиске по ключу, а не о представлении данных в отсортированном (упорядоченном) виде, то все эти финтифлюшки резко проигрывают банальной хэш-таблице. Так что по любому применение бинарного поиска оправданно только в том случае если после поиска требуется перебор значений в упорядоченном виде. Откровенно говоря такие задача для больших объемов данных я встречал только в СУБД. Или скажем так, что логично было бы для их решения СУБД и использовать.
Sinclair
Sinclair
15.05.2008 11:05
Здравствуйте, VladD2, Вы писали:
VD>Так вот все это здорово, но есть одно "но". Если речь идет именно о поиске по ключу, а не о представлении данных в отсортированном (упорядоченном) виде, то все эти финтифлюшки резко проигрывают банальной хэш-таблице.
Не очень понятно, почему. "Банальная" хэш таблица вроде как достаточно плохо себя ведет по отношению к кэшу. Именно поэтому в СУБД применяются небанальные хэш-таблицы.
... << RSDN@Home 1.2.0 alpha rev. 677>>
remark
remark
23.05.2008 09:56
Здравствуйте, Sinclair, Вы писали:

S>Здравствуйте, VladD2, Вы писали:

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

S>Не очень понятно, почему. "Банальная" хэш таблица вроде как достаточно плохо себя ведет по отношению к кэшу. Именно поэтому в СУБД применяются небанальные хэш-таблицы.



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


Sinclair
Sinclair
28.05.2008 03:54
Здравствуйте, remark, Вы писали:
R>Имхо как раз хорошо. Мы сразу "тыкаем" в нужный элемент (или очень рядом). Этот элемент случайный, это да. Но с деревом то мы тоже доберёмся до этого же случайного элемента, только перед этим "пройдёмся" по ряду других "случайных" и не интересующих нас элементов.
Для read-only операций да. Но для них можно сделать вообще perfect hash, и добиться попаданий с первого раза при оптимальном использовании кэша.
А вот для пополняемых таблиц — дупа. Рехешинг, которым обычно решают проблему переполнения, приведет к полному сбросу кэша, и если таблица больше кэша процессора, то поведение будет крайне неприятным. B-trees, как и модификации хештаблиц для СУБД учитывают это, и стараются ограничить количество страниц, которые затрагиваются при всех операциях.
... << RSDN@Home 1.2.0 alpha rev. 677>>
Erop
Erop
28.05.2008 05:47
Здравствуйте, Sinclair, Вы писали:

S>А вот для пополняемых таблиц — дупа. Рехешинг, которым обычно решают проблему переполнения, приведет к полному сбросу кэша, и если таблица больше кэша процессора, то поведение будет крайне неприятным.


Закрытое хэширование?
Несколько поколений таблицы + рехешенг в фоне?
remark
remark
28.05.2008 09:10
Здравствуйте, Sinclair, Вы писали:

S>Здравствуйте, remark, Вы писали:

R>>Имхо как раз хорошо. Мы сразу "тыкаем" в нужный элемент (или очень рядом). Этот элемент случайный, это да. Но с деревом то мы тоже доберёмся до этого же случайного элемента, только перед этим "пройдёмся" по ряду других "случайных" и не интересующих нас элементов.

S>Для read-only операций да. Но для них можно сделать вообще perfect hash, и добиться попаданий с первого раза при оптимальном использовании кэша.

S>А вот для пополняемых таблиц — дупа. Рехешинг, которым обычно решают проблему переполнения, приведет к полному сбросу кэша, и если таблица больше кэша процессора, то поведение будет крайне неприятным. B-trees, как и модификации хештаблиц для СУБД учитывают это, и стараются ограничить количество страниц, которые затрагиваются при всех операциях.

Ну это уже что-то. По-крайней мере становится понятна мотивация разработчиков БД. Тем не менее, мне кажется, что в большей части остального ПО, это не существенно. Основная "тяжелая" операция для хэш-таблицы — это увеличение основной таблицы. Остальные могут быть выполнены достаточно локально. Имхо, обычно размер таблицы приходит в какое-то устоявшееся состояние. Например, храним в хэш-таблице пользовательские соединения/сессии. Начальный размер задаем, допустим, 4096. За несколько увеличений (удвоений) размер таблицы выйдет в "устоявшийся режим".

remark
remark
23.05.2008 09:54
Здравствуйте, VladD2, Вы писали:

VD>Так вот все это здорово, но есть одно "но". Если речь идет именно о поиске по ключу, а не о представлении данных в отсортированном (упорядоченном) виде, то все эти финтифлюшки резко проигрывают банальной хэш-таблице. Так что по любому применение бинарного поиска оправданно только в том случае если после поиска требуется перебор значений в упорядоченном виде. Откровенно говоря такие задача для больших объемов данных я встречал только в СУБД. Или скажем так, что логично было бы для их решения СУБД и использовать.



Я сам задаюсь этим вопросом
По-моему, деревьям уделяют чрезмерно много внимания, во-многих случаях действительно лучше было бы использовать хэш-таблицу.
Тем не менее различным видам деревьев в различного рода исследованиях уделяют очень много внимания. Это касается как и новых "типов" деревьев, так и оптимизаций типа Cache-Conscious. Большинство ресёрча, на котором я основывался, исходит от компаний типа Sun или Microsoft, и касается это всё по всей видимости оптимизаций как ран-таймов их виртуальных машин, так и оптимизации выполнения пользовательского кода на этих виртуальных машинах (я имею в виду Java и .NET соотв.).


Erop
Erop
28.05.2008 05:48
Здравствуйте, remark, Вы писали:

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

Да, причём в хэш-таблицах тоже не всё так уж просто и прямо, если честно.

R>Тем не менее различным видам деревьев в различного рода исследованиях уделяют очень много внимания. Это касается как и новых "типов" деревьев, так и оптимизаций типа Cache-Conscious. Большинство ресёрча, на котором я основывался, исходит от компаний типа Sun или Microsoft, и касается это всё по всей видимости оптимизаций как ран-таймов их виртуальных машин, так и оптимизации выполнения пользовательского кода на этих виртуальных машинах (я имею в виду Java и .NET соотв.).

Ну стандартные ассоциативные контейнеры все "деревянные"
remark
remark
28.05.2008 09:13
Здравствуйте, Erop, Вы писали:

E>Здравствуйте, remark, Вы писали:


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

E>Да, причём в хэш-таблицах тоже не всё так уж просто и прямо, если честно.

R>>Тем не менее различным видам деревьев в различного рода исследованиях уделяют очень много внимания. Это касается как и новых "типов" деревьев, так и оптимизаций типа Cache-Conscious. Большинство ресёрча, на котором я основывался, исходит от компаний типа Sun или Microsoft, и касается это всё по всей видимости оптимизаций как ран-таймов их виртуальных машин, так и оптимизации выполнения пользовательского кода на этих виртуальных машинах (я имею в виду Java и .NET соотв.).


E>Ну стандартные ассоциативные контейнеры все "деревянные"


Ты имеешь в виду С++? Будем надеяться, что в С++ это всё же исправят в будущем. В тех языках, о которых речь, — Java и .NET — афаик в стандартной библиотеке изначально присутствуют *не* только "деревянные" контейнеры.

Erop
Erop
28.05.2008 09:56
Здравствуйте, remark, Вы писали:

R>Ты имеешь в виду С++? Будем надеяться, что в С++ это всё же исправят в будущем.

Да их в науке о структурах данных за что-то любят. Наверное за универсальность.

R>В тех языках, о которых речь, — Java и .NET — афаик в стандартной библиотеке изначально присутствуют *не* только "деревянные" контейнеры.

А кто там ассоциативный и не "деревянный"?

R>
McSeem2
McSeem2
01.05.2008 06:16
Здравствуйте, remark, Вы писали:

R>Возьмём вот такую простенькую функцию перемножения матриц:

R>[. . .]

Большие матрицы, например, текстуры в видеопамяти, хранятся кусочками типа 16x16. Называется глупым словом Texture Swizzling. За счет этого, в среднем в 255 из 256 случаев получаем быстрый доступ, в 1 из 256 — медленный.

Все это конечно правильно, но в реальной жизни набор алгоритмов, легко поддающихся подобной оптиимзации крайне ограничен (как типичный пример — текстурирование в видеокартах). А те алгоритмы, которые плохо поддаются оптимизации, но хочется — превращаются в кошмар.
remark
remark
01.05.2008 06:58
Здравствуйте, McSeem2, Вы писали:

MS>Здравствуйте, remark, Вы писали:


R>>Возьмём вот такую простенькую функцию перемножения матриц:

R>>[. . .]

MS>Большие матрицы, например, текстуры в видеопамяти, хранятся кусочками типа 16x16. Называется глупым словом Texture Swizzling. За счет этого, в среднем в 255 из 256 случаев получаем быстрый доступ, в 1 из 256 — медленный.



Это связано с тем, что алгоритмы обработки текстур обычно требуют не отдельные строки матрицы, а прямоугольники изображения. Правильно?
Если да, то это и есть Cache-Conscious оптимизация.


Я не думаю, что это утверждение истинно в общем случае без части "например"

Большие матрицы, например, текстуры в видеопамяти, хранятся кусочками типа 16x16.


Ты наверное имел в виду просто:

Текстуры в видеопамяти хранятся кусочками типа 16x16.

?


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



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


Ka3a4oK
Ka3a4oK
14.05.2008 06:39
Здравствуйте, remark, Вы писали:

R>Здравствуйте, McSeem2, Вы писали:


MS>>Здравствуйте, remark, Вы писали:


R>>>Возьмём вот такую простенькую функцию перемножения матриц:

R>>>[. . .]

MS>>Большие матрицы, например, текстуры в видеопамяти, хранятся кусочками типа 16x16. Называется глупым словом Texture Swizzling. За счет этого, в среднем в 255 из 256 случаев получаем быстрый доступ, в 1 из 256 — медленный.



R>Это связано с тем, что алгоритмы обработки текстур обычно требуют не отдельные строки матрицы, а прямоугольники изображения. Правильно?

R>Если да, то это и есть Cache-Conscious оптимизация.

Если быть точнее, то опять сказывается вдияние порядка обхода. При текстурировании происходит сканирование текстуры не по строчке, а вдоль некторого отрезка.

http://files.rsdn.org/28975/текстура.GIF

Поэтому группируют данные внутри прямоугольных блоков. Надеюсь, ничего не напутал, я в этом деле не спец.
... << RSDN@Home 1.1.4 stable rev. 510>>
blacklion
blacklion
13.05.2008 08:37
Здравствуйте, remark, Вы писали:

R>clflush

R>
R>void _mm_clflush(void const*p) 
R>

R>Принудительная выгрузка из всех кэшей, всех процессоров заданной строки. Практическое применение этой команды для меня пока остаётся загадкой, поэтому ничего говорить не буду.
Как загадкой? А динамическая генерация кода (в частности -- Just in time compilation) и самомодифицирующийся код как частный случай, с которого всё началось когда ещё кэшей не было?

А статья такая была. Цикл статей от Криса Касперски в журнале (ныне покойном) "Программист". И даже книгу Крис издавал.

--
// Black Lion AKA Lev Serebryakov
remark
remark
13.05.2008 06:29
Здравствуйте, blacklion, Вы писали:

B>Здравствуйте, remark, Вы писали:


R>>clflush

R>>
R>>void _mm_clflush(void const*p) 
R>>

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

B>Как загадкой? А динамическая генерация кода (в частности -- Just in time compilation) и самомодифицирующийся код как частный случай, с которого всё началось когда ещё кэшей не было?


B>А статья такая была. Цикл статей от Криса Касперски в журнале (ныне покойном) "Программист". И даже книгу Крис издавал.



А в каком контексте clflush применяется при JIT компиляции?
Вроде ж как при модификации кода модификация будет распространяться по протоколу когерентности на общих основаниях
Насколько я помню из документации, если модифицируется выровненный блок не более 16 байт, то вообще никаких специальных мер предпринимать не надо.
В документации AMD я вижу упоминание clflush только в контексте устройств, которые не поддерживают протокол когерентности кэшей.
А в документации Intel — в контексте виртуализации...




Black Lion
Black Lion
14.05.2008 05:48
Здравствуйте, remark, Вы писали:

R>А в каком контексте clflush применяется при JIT компиляции?

R>Вроде ж как при модификации кода модификация будет распространяться по протоколу когерентности на общих основаниях
R>Насколько я помню из документации, если модифицируется выровненный блок не более 16 байт, то вообще никаких специальных мер предпринимать не надо.
R>В документации AMD я вижу упоминание clflush только в контексте устройств, которые не поддерживают протокол когерентности кэшей.
R>А в документации Intel — в контексте виртуализации...
Очистка кэша кода. У Intel он точно сам не флашится при записи в ту область, которая закэширована (причём ведь у интела в код-кэше ещё и микрооперации или как их там нынче велено называть хранятся). У AMD -- не помню, но вроде как тоже...
Оно надо не при любой динамической генерации, но иногда приходится.

--
// Black Lion AKA Lev Serebryakov
remark
remark
15.05.2008 07:28
Здравствуйте, Black Lion, Вы писали:

R>>А в каком контексте clflush применяется при JIT компиляции?

R>>Вроде ж как при модификации кода модификация будет распространяться по протоколу когерентности на общих основаниях
R>>Насколько я помню из документации, если модифицируется выровненный блок не более 16 байт, то вообще никаких специальных мер предпринимать не надо.
R>>В документации AMD я вижу упоминание clflush только в контексте устройств, которые не поддерживают протокол когерентности кэшей.
R>>А в документации Intel — в контексте виртуализации...

BL> Очистка кэша кода. У Intel он точно сам не флашится при записи в ту область, которая закэширована (причём ведь у интела в код-кэше ещё и микрооперации или как их там нынче велено называть хранятся). У AMD -- не помню, но вроде как тоже...

BL> Оно надо не при любой динамической генерации, но иногда приходится.


Вот всё, что я вижу в "Intel® 64 and IA-32 Architectures Software Developer’s Manual. Volume 3A: System Programming Guide, Part 1" по поводу Self- и Cross-Modifying кода. Тут нет никаких упоминаний ни clflush, ни необходимости ручного сброса кэша. Тут вроде написано как раз обратное, что записи в область кода сбрасываются в память сразу, L1I кэш даже не поддерживает состояния M и E (смотри раздел 10.4).

7.1.3 Handling Self- and Cross-Modifying Code
The act of a processor writing data into a currently executing code segment with
the intent of executing that data as code is called self-modifying code. IA-32
processors exhibit model-specific behavior when executing self-modified code,
depending upon how far ahead of the current execution pointer the code has been
modified.
As processor microarchitectures become more complex and start to speculatively
execute code ahead of the retirement point (as in P6 and more recent processor
families), the rules regarding which code should execute, pre- or post-modification,
become blurred. To write self-modifying code and ensure that it is compliant with
current and future versions of the IA-32 architectures, use one of the following
coding options:
(* OPTION 1 *)
Store modified code (as data) into code segment;
Jump to new code or an intermediate location;
Execute new code;
(* OPTION 2 *)
Store modified code (as data) into code segment;
Execute a serializing instruction; (* For example, CPUID instruction *)
Execute new code;
The use of one of these options is not required for programs intended to run on the
Pentium or Intel486 processors, but are recommended to insure compatibility with
the P6 and more recent processor families.
Self-modifying code will execute at a lower level of performance than non-self-modifying
or normal code. The degree of the performance deterioration will depend upon
the frequency of modification and specific characteristics of the code.
The act of one processor writing data into the currently executing code segment of a
second processor with the intent of having the second processor execute that data as
code is called cross-modifying code. As with self-modifying code, IA-32 processors
exhibit model-specific behavior when executing cross-modifying code, depending
upon how far ahead of the executing processors current execution pointer the code
has been modified.
To write cross-modifying code and insure that it is compliant with current and future
versions of the IA-32 architecture, the following processor synchronization algorithm
must be implemented:
(* Action of Modifying Processor *)
Memory_Flag ← 0; (* Set Memory_Flag to value other than 1 *)
Store modified code (as data) into code segment;
Memory_Flag ← 1;
(* Action of Executing Processor *)
WHILE (Memory_Flag ≠ 1)
Wait for code to update;
ELIHW;
Execute serializing instruction; (* For example, CPUID instruction *)
Begin executing modified code;
(The use of this option is not required for programs intended to run on the Intel486
processor, but is recommended to insure compatibility with the Pentium 4, Intel
Xeon, P6 family, and Pentium processors.)
Like self-modifying code, cross-modifying code will execute at a lower level of performance
than non-cross-modifying (normal) code, depending upon the frequency of
modification and specific characteristics of the code.
The restrictions on self-modifying code and cross-modifying code also apply to the
Intel 64 architecture.



10.6 SELF-MODIFYING CODE
A write to a memory location in a code segment that is currently cached in the
processor causes the associated cache line (or lines) to be invalidated. This check is
based on the physical address of the instruction. In addition, the P6 family and
Pentium processors check whether a write to a code segment may modify an instruction
that has been prefetched for execution. If the write affects a prefetched instruction,
the prefetch queue is invalidated. This latter check is based on the linear
address of the instruction. For the Pentium 4 and Intel Xeon processors, a write or a
snoop of an instruction in a code segment, where the target instruction is already
decoded and resident in the trace cache, invalidates the entire trace cache. The latter
behavior means that programs that self-modify code can cause severe degradation
of performance when run on the Pentium 4 and Intel Xeon processors.
In practice, the check on linear addresses should not create compatibility problems
among IA-32 processors. Applications that include self-modifying code use the same
linear address for modifying and fetching the instruction. Systems software, such as
a debugger, that might possibly modify an instruction using a different linear address
than that used to fetch the instruction, will execute a serializing operation, such as a
CPUID instruction, before the modified instruction is executed, which will automatically
resynchronize the instruction cache and prefetch queue. (See Section 7.1.3,
“Handling Self- and Cross-Modifying Code,” for more information about the use of
self-modifying code.)
For Intel486 processors, a write to an instruction in the cache will modify it in both
the cache and memory, but if the instruction was prefetched before the write, the old
version of the instruction could be the one executed. To prevent the old instruction
from being executed, flush the instruction prefetch unit by coding a jump instruction
immediately after any write that modifies an instruction.



10.4 CACHE CONTROL PROTOCOL
The L1 instruction cache in P6 family processors implements only the “SI” part of the
MESI protocol, because the instruction cache is not writable. The instruction cache
monitors changes in the data cache to maintain consistency between the caches
when instructions are modified. See Section 10.6, “Self-Modifying Code,” for more
information on the implications of caching instructions.



17.28.1 Self-Modifying Code with Cache Enabled
On the Intel486 processor, a write to an instruction in the cache will modify it in both
the cache and memory. If the instruction was prefetched before the write, however,
the old version of the instruction could be the one executed. To prevent this problem,
it is necessary to flush the instruction prefetch unit of the Intel486 processor by
coding a jump instruction immediately after any write that modifies an instruction.
The P6 family and Pentium processors, however, check whether a write may modify
an instruction that has been prefetched for execution. This check is based on the
linear address of the instruction. If the linear address of an instruction is found to be
present in the prefetch queue, the P6 family and Pentium processors flush the
prefetch queue, eliminating the need to code a jump instruction after any writes that
modify an instruction.





Я в замешательстве, и по прежнему не вижу применений для clflush.

Ka3a4oK
Ka3a4oK
14.05.2008 06:39
Жесть.
... << RSDN@Home 1.1.4 stable rev. 510>>
remark
remark
12.06.2008 11:54
Здравствуйте, remark, Вы писали:

R>

RAM — не RAM



Занятно — ровно через месяц после моего поста Герб Саттер написал практически то же самое в DDJ:
Maximize Locality, Minimize Contention

Cache-Conscious Design

Locality is a first-order issue that trumps much of our existing understanding of performance optimization. Most traditional optimization techniques come after "locality" on parallel hardware (although a few are still equally or more important than locality, such as big-Oh algorithmic complexity for example).

Arrange your data carefully by following these three guidelines, starting with the most important:

First: Keep data that are not used together apart in memory. If variables A and B are not protected by the same mutex and are liable to be used by two different threads, keep them on separate cache lines. (Add padding, if necessary; it's a great way to "waste" memory to make your code run faster.) This avoids the invisible convoying of false sharing (ping-pong) where in the worst case only one contending thread can make progress at all, and so typically trumps other important cache considerations.

Second: Keep data that is frequently used together close together in memory. If a thread that uses variable A frequently also needs variable B, try to put A and B in the same cache line if possible. For example, A and B might be two fields in the same object that are frequently used together. This is a traditional technique to improve memory performance in both sequential and concurrent code, which now comes second to keeping separate data apart.

Third: Keep "hot" (frequently accessed) and "cold" (infrequently accessed) data apart. This is true even if the data is conceptually in the same logical object. This helps both to fit "hot" data into the fewest possible cache lines and memory pages and to avoid needlessly loading the "colder" parts. Together these effects reduce (a) the cache footprint and cache misses, and (b) the memory footprint and virtual memory paging.



А теперь ещё нашёл, что за 2 дня до моего поста появилась аналогичная статья на RapidMind:
Why the Future isn’t Flat

The constant-access-time “flat” memory illusion is as important as the serial illusion. In a flat memory, every location in memory can be accessed with exactly the same cost, independent of the order of access. For decades, this simple cost model has been used as an implicit assumption in the design of computer programs. Even theoretical computer science, which analyzes the best-case asymptotic complexity of algorithms for solving various abstract problems, is primarily based on this illusion. Unfortunately, it is just an illusion, although computer architects have been able to develop many clever mechanisms to maintain it. However, the latency of accessing a random word in external main memory (DRAM) is quite slow compared to processor speed, by two orders of magnitude or more. A computer using a memory system consisting only of DRAM would be intolerably slow, so modern machines instead have a memory hierarchy, where copies of certain parts of the memory space are kept in faster, smaller cache memories. If the most frequently accessed data can be kept in the fastest cache memories, then on average a low access cost can be achieved. The memory hierarchy exploits the fact that typical programs exhibit spatial and temporal locality. This mechanism has been reasonably successful at maintaining the flat memory illusion in serial computers. However, even in serial computers significant performance gains are possible by designing programs using a more realistic memory model. For example, it is worthwhile to use data structures and algorithms that are designed to have high levels of spatial and temporal locality.



Кто у кого списывает?... не понятно
В любом случае пользователи RSDN получают актуальнейший контент в русском изложении


R>

Cyberax
Cyberax
13.06.2008 12:11
Здравствуйте, remark, Вы писали:

R>Кто у кого списывает?... не понятно

R>В любом случае пользователи RSDN получают актуальнейший контент в русском изложении
Блин. Кто опять машиной времени балуется!!
remark
remark
13.06.2008 12:27
Здравствуйте, Cyberax, Вы писали:

C>Здравствуйте, remark, Вы писали:


R>>Кто у кого списывает?... не понятно

R>>В любом случае пользователи RSDN получают актуальнейший контент в русском изложении
C>Блин. Кто опять машиной времени балуется!!

Видимо ребята из RapidMind. У Саттера честно ушёл месяц на перевод и публикацию