Функции numa_alloc _ * () в libnuma выделяют целые страницы памяти, обычно 4096 байт.Строки кэша обычно составляют 64 байта.Поскольку 4096 кратно 64, все, что возвращается из numa_alloc _ * (), будет уже выровнено на уровне кэша.
Однако остерегайтесь функций numa_alloc _ * ().На странице руководства написано, что они медленнее, чем соответствующие malloc (), что, я уверен, верно, но гораздо большая проблема, которую я обнаружил, заключается в том, что одновременные выделения из numa_alloc _ * () выполняются на множестве ядер одновременнострадают от массовых споров.В моем случае замена malloc () на numa_alloc_onnode () была стиркой (все, что я получил, используя локальную память, было компенсировано увеличением выделения / свободного времени);tcmalloc был быстрее, чем любой.Я выполнял тысячи 12-16kb malloc одновременно на 32 потоках / ядрах.Эксперименты по срокам показали, что не однопоточная скорость numa_alloc_onnode () была ответственна за большое количество времени, которое мой процесс потратил на выполнение выделений, что оставляло проблемы блокировки / конфликта как вероятную причину.Решение, которое я принял, состоит в том, чтобы numa_alloc_onnode () обрабатывал большие куски памяти один раз, а затем распределял его по потокам, работающим на каждом узле, по мере необходимости.Я использую встроенные функции gcc atomic, чтобы каждый поток (я прикрепляю потоки к процессору) извлекал память из каждого узла.Вы можете выровнять размеры кэша по размеру строки, как они сделаны, если хотите: я делаю.Такой подход не позволяет даже tcmalloc (который поддерживает потоки, но не поддерживает NUMA - по крайней мере, версия Debain Squeeze, похоже, этого не делает).Недостатком этого подхода является то, что вы не можете освободить отдельные дистрибутивы (ну, во всяком случае, без лишней работы), вы можете освободить только все базовые выделения на узлах.Однако, если это временное локальное пространство для вызова функции или вы можете указать точно, когда эта память больше не нужна, тогда этот подход работает очень хорошо.Это помогает, если вы можете предсказать, сколько памяти вам нужно выделить на каждом узле, очевидно.
@ nandu: я не буду публиковать полный исходный код - он длинный и местами привязан к чему-то другому, что я делаю, что делает егоменее чем совершенно прозрачный.То, что я опубликую, является несколько сокращенной версией моей новой функции malloc (), чтобы проиллюстрировать основную идею:
void *my_malloc(struct node_memory *nm,int node,long size)
{
long off,obytes;
// round up size to the nearest cache line size
// (optional, though some rounding is essential to avoid misalignment problems)
if ((obytes = (size % CACHE_LINE_SIZE)) > 0)
size += CACHE_LINE_SIZE - obytes;
// atomically increase the offset for the requested node by size
if (((off = __sync_fetch_and_add(&(nm->off[node]),size)) + size) > nm->bytes) {
fprintf(stderr,"Out of allocated memory on node %d\n",node);
return(NULL);
}
else
return((void *) (nm->ptr[node] + off));
}
, где struct node_memory равна
struct node_memory {
long bytes; // the number of bytes of memory allocated on each node
char **ptr; // ptr array of ptrs to the base of the memory on each node
long *off; // array of offsets from those bases (in bytes)
int nptrs; // the size of the ptr[] and off[] arrays
};
и nm-> ptr [узел] устанавливается с помощью функции libnuma numa_alloc_onnode ().
Обычно я также храню информацию о допустимых узлах в структуре, поэтому my_malloc () может проверить, что запросы узлов являются разумными, не вызывая функции;Я также проверяю, что нм существует, и этот размер имеет смысл.Функция __sync_fetch_and_add () является встроенной атомарной функцией gcc;если вы не компилируете с gcc, вам нужно что-то еще.Я использую атомы, потому что в моем ограниченном опыте они намного быстрее мьютексов в условиях большого числа потоков / ядер (как на машинах 4P NUMA).