Распределение памяти в соответствии с NUMA
В системах Linux, библиотека pthreads предоставляет нам функцию (posix_memalign) для выравнивания кэша, чтобы предотвратить ложное совместное использование. И чтобы выбрать конкретный узел архитектуры NUMA, мы можем использовать библиотеку libnuma. Что я хочу, так это то, что нужно обоим. Я связываю определенные потоки с некоторыми определенными процессорами и хочу выделить локальные структуры данных для каждого потока из соответствующего узла NUMA, чтобы уменьшить задержку операций с памятью для потоков. Как я могу это сделать?
2 ответа
Если вы просто хотите получить функциональность выравнивания вокруг распределителя NUMA, вы можете легко создать свой собственный.
Идея состоит в том, чтобы назвать неприсоединившихся malloc()
с немного больше места. Затем верните первый выровненный адрес. Чтобы иметь возможность освободить его, вам нужно хранить базовый адрес в известном месте.
Вот пример. Просто замените имена тем, что подходит:
pint // An unsigned integer that is large enough to store a pointer.
NUMA_malloc // The NUMA malloc function
NUMA_free // The NUMA free function
void* my_NUMA_malloc(size_t bytes,size_t align, /* NUMA parameters */ ){
// The NUMA malloc function
void *ptr = numa_malloc(
(size_t)(bytes + align + sizeof(pint)),
/* NUMA parameters */
);
if (ptr == NULL)
return NULL;
// Get aligned return address
pint *ret = (pint*)((((pint)ptr + sizeof(pint)) & ~(pint)(align - 1)) + align);
// Save the free pointer
ret[-1] = (pint)ptr;
return ret;
}
void my_NUMA_free(void *ptr){
if (ptr == NULL)
return;
// Get the free pointer
ptr = (void*)(((pint*)ptr)[-1]);
// The NUMA free function
numa_free(ptr);
}
Чтобы, когда вы используете это, вам нужно позвонить my_NUMA_free
за все, что выделено my_NUMA_malloc
,
Функции 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, чтобы позволить каждому потоку (я прикрепляю потоки к процессору) захватывать память, выделенную на каждом узле. Вы можете выровнять размеры кэша по размеру строки, как они сделаны, если хотите: я делаю. Такой подход не позволяет даже 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[node] устанавливается с помощью функции libnuma numa_alloc_onnode().
Обычно я также сохраняю информацию о допустимых узлах в структуре, поэтому my_malloc() может проверить, что запросы узлов являются разумными, не вызывая функции; Я также проверяю, что nm существует, и этот размер имеет смысл. Функция __sync_fetch_and_add() является встроенной атомарной функцией gcc; если вы не компилируете с gcc, вам нужно что-то еще. Я использую атомы, потому что в моем ограниченном опыте они намного быстрее мьютексов в условиях большого числа потоков / ядер (как на машинах 4P NUMA).