Распределение памяти в соответствии с NUMA - PullRequest
9 голосов
/ 16 ноября 2011

В системах Linux, библиотека pthreads предоставляет нам функцию (posix_memalign) для выравнивания кэша, чтобы предотвратить ложное совместное использование. И чтобы выбрать конкретный узел архитектуры NUMA, мы можем использовать библиотеку libnuma. Что я хочу, так это то, что нужно обоим. Я связываю определенные потоки с некоторыми определенными процессорами и хочу выделить локальные структуры данных для каждого потока из соответствующего узла NUMA, чтобы уменьшить задержку операций с памятью для потоков. Как я могу это сделать?

Ответы [ 2 ]

11 голосов
/ 26 ноября 2011

Функции 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).

7 голосов
/ 17 ноября 2011

Если вы просто хотите получить функциональность выравнивания вокруг распределителя 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.

...