NUMA感知缓存对齐的内存分配

Mus*_*gin 10 linux malloc caching pthreads numa

在linux系统中,pthreads库为我们提供了一个用于缓存对齐的函数(posix_memalign),以防止错误共享.并且要选择arhitecture的特定NUMA节点,我们可以使用libnuma库.我想要的是需要两者的东西.我将某些线程绑定到某些某些处理器,并且我希望为来自相应NUMA节点的每个线程分配本地数据结构,以减少线程的内存操作延迟.我怎样才能做到这一点?

Rob*_*its 11

libnuma中的numa_alloc _*()函数分配整个内存页,通常为4096字节.高速缓存行通常为64字节.由于4096是64的倍数,因此从numa_alloc _*()返回的任何内容都将在缓存级别进行memaligned.

但要注意numa_alloc _*()函数.它在手册页上说它们比相应的malloc()慢,我确信它是真的,但我发现的更大的问题是同时从numa_alloc _*()同时分配在很多内核上同时运行遭受大规模的争用问题.在我的情况下,使用numa_alloc_onnode()替换malloc()是一种清洗(通过使用本地内存获得的所有内容都被增加的分配/空闲时间抵消); tcmalloc比两者都快.我一次在32个线程/核心上执行了数千个12-16kb malloc.时间实验表明,不是numa_alloc_onnode()的单线程速度导致我的进程花费大量时间执行分配,这导致锁定/争用问题成为可能的原因.我采用的解决方案是numa_alloc_onnode()大块内存一次,然后根据需要将其分发到每个节点上运行的线程.我使用gcc atomic builtins来允许每个线程(我将线程连接到cpus)从每个节点上分配的内存中获取.如果你愿意,可以按照缓存行大小对齐发布的分配:我这样做.即使是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));

}
Run Code Online (Sandbox Code Playgroud)

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
};
Run Code Online (Sandbox Code Playgroud)

和nm-> ptr [node]使用libnuma函数numa_alloc_onnode()建立.

我通常也在结构中存储允许的节点信息,因此my_malloc()可以在不进行函数调用的情况下检查节点请求是否合理; 我也检查nm存在,这个大小是明智的.函数__sync_fetch_and_add()是一个gcc内置原子函数; 如果你没有用gcc编译,你还需要别的东西.我使用原子,因为在我有限的经验中,它们比高线程/核心计数条件下的互斥量快得多(如4P NUMA机器).

  • 这对于阅读你的答案的人来说非常有用:`numa_alloc _*()`使用`mmap(2)`syscall来分配内存,然后使用`mbind(2)`syscall在新映射的区域上设置NUMA策略.`malloc(3)`分配器通常映射大块内存(竞技场),然后将它们细分为块而不需要任何内核干预.这就是为什么使用`numa_alloc _*()`来执行许多小分配,从性能的角度来看是非常不容易的. (2认同)

Mys*_*ial 8

如果您只是希望获得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); 
}
Run Code Online (Sandbox Code Playgroud)

当你使用它时,你需要调用my_NUMA_free任何分配的东西my_NUMA_malloc.