在NUMA架构上可扩展分配大型(8MB)内存区域

时间:2012-12-10 15:14:48

标签: c++ memory-management parallel-processing tbb numa

我们目前正在使用TBB流程图,其中a)并行过滤器处理数组(与偏移并行)并将处理结果放入中间向量(在堆上分配;大多数向量将增长到8MB) 。然后将这些矢量传递给节点,然后节点根据它们的特性(在a中确定)对这些结果进行后处理。由于资源同步,每个特征只能有一个这样的节点。我们编写的原型在UMA架构上运行良好(在单CPU Ivy Bridge和Sandy Bridge架构上测试)。但是,该应用程序无法在我们的NUMA架构(4 CPU Nehalem-EX)上扩展。我们将问题归结为内存分配并创建了一个最小的示例,其中我们有一个并行管道,它只是从堆中分配内存(通过8MB块的malloc,然后memset 8MB区域;类似于初始原型所做的)达到一定的记忆力。我们的研究结果如下:

  • 在UMA架构上,应用程序与管道使用的线程数呈线性关系(通过task_scheduler_init设置)

  • 在我们将应用程序固定到一个插槽(使用numactl)时,在NUMA架构上,我们看到相同的线性放大

  • 在我们使用多个套接字的NUMA架构中,我们的应用程序运行时间随套接字数量的增加而增加(负线性比例 - “向上”)

对我们来说,这就像堆争用一样。我们到目前为止尝试的是将英特尔的TBB可扩展分配器替换为glibc分配器。但是,单个套接字上的初始性能比使用glibc更差,在多个套接字上性能不会变差但也没有变得更好。我们使用tcmalloc,hoard分配器和TBB的缓存对齐分配器获得了相同的效果。

问题是,是否有人遇到类似的问题。堆栈分配对我们来说不是一个选项,因为我们希望在管道运行后保持堆分配的向量。一个堆如何在多个线程的NUMA体系结构上有效地分配MB大小的内存区域?我们真的希望保持动态分配方法,而不是预先分配内存并在应用程序中管理它。

我使用numactl为各种执行附加了perf stats。 Interleaving / localalloc无任何影响(QPI总线不是瓶颈;我们通过PCM验证,QPI链路负载为1%)。我还添加了一个描绘glibc,tbbmalloc和tcmalloc结果的图表。

perf stat bin / prototype 598.867

'bin / prototype'的性能计数器统计信息:

  12965,118733 task-clock                #    7,779 CPUs utilized          
        10.973 context-switches          #    0,846 K/sec                  
         1.045 CPU-migrations            #    0,081 K/sec                  
       284.210 page-faults               #    0,022 M/sec                  
17.266.521.878 cycles                    #    1,332 GHz                     [82,84%]
15.286.104.871 stalled-cycles-frontend   #   88,53% frontend cycles idle    [82,84%]
10.719.958.132 stalled-cycles-backend    #   62,09% backend  cycles idle    [67,65%]
 3.744.397.009 instructions              #    0,22  insns per cycle        
                                         #    4,08  stalled cycles per insn [84,40%]
   745.386.453 branches                  #   57,492 M/sec                   [83,50%]
    26.058.804 branch-misses             #    3,50% of all branches         [83,33%]

   1,666595682 seconds time elapsed

perf stat numactl --cpunodebind = 0 bin / prototype 272.614

'numactl --cpunodebind = 0 bin / prototype'的性能计数器统计信息:

   3887,450198 task-clock                #    3,345 CPUs utilized          
         2.360 context-switches          #    0,607 K/sec                  
           208 CPU-migrations            #    0,054 K/sec                  
       282.794 page-faults               #    0,073 M/sec                  
 8.472.475.622 cycles                    #    2,179 GHz                     [83,66%]
 7.405.805.964 stalled-cycles-frontend   #   87,41% frontend cycles idle    [83,80%]
 6.380.684.207 stalled-cycles-backend    #   75,31% backend  cycles idle    [66,90%]
 2.170.702.546 instructions              #    0,26  insns per cycle        
                                         #    3,41  stalled cycles per insn [85,07%]
   430.561.957 branches                  #  110,757 M/sec                   [82,72%]
    16.758.653 branch-misses             #    3,89% of all branches         [83,06%]

   1,162185180 seconds time elapsed

perf stat numactl --cpunodebind = 0-1 bin / prototype 356.726

'numactl --cpunodebind = 0-1 bin / prototype'的性能计数器统计信息:

   6127,077466 task-clock                #    4,648 CPUs utilized          
         4.926 context-switches          #    0,804 K/sec                  
           469 CPU-migrations            #    0,077 K/sec                  
       283.291 page-faults               #    0,046 M/sec                  
10.217.787.787 cycles                    #    1,668 GHz                     [82,26%]
 8.944.310.671 stalled-cycles-frontend   #   87,54% frontend cycles idle    [82,54%]
 7.077.541.651 stalled-cycles-backend    #   69,27% backend  cycles idle    [68,59%]
 2.394.846.569 instructions              #    0,23  insns per cycle        
                                         #    3,73  stalled cycles per insn [84,96%]
   471.191.796 branches                  #   76,903 M/sec                   [83,73%]
    19.007.439 branch-misses             #    4,03% of all branches         [83,03%]

   1,318087487 seconds time elapsed

perf stat numactl --cpunodebind = 0-2 bin / protoype 472.794

'numactl --cpunodebind = 0-2 bin / prototype'的性能计数器统计信息:

   9671,244269 task-clock                #    6,490 CPUs utilized          
         7.698 context-switches          #    0,796 K/sec                  
           716 CPU-migrations            #    0,074 K/sec                  
       283.933 page-faults               #    0,029 M/sec                  
14.050.655.421 cycles                    #    1,453 GHz                     [83,16%]
12.498.787.039 stalled-cycles-frontend   #   88,96% frontend cycles idle    [83,08%]
 9.386.588.858 stalled-cycles-backend    #   66,81% backend  cycles idle    [66,25%]
 2.834.408.038 instructions              #    0,20  insns per cycle        
                                         #    4,41  stalled cycles per insn [83,44%]
   570.440.458 branches                  #   58,983 M/sec                   [83,72%]
    22.158.938 branch-misses             #    3,88% of all branches         [83,92%]

   1,490160954 seconds time elapsed

最小的例子:用g ++编译 - 4.7 std = c ++ 11 -O3 -march = native;使用numactl执行--cpunodebind = 0 ... numactl --cpunodebind = 0-3 - 使用CPU绑定我们得到以下结果:1个CPU(速度x),2个CPU(速度~x / 2),3个CPU(速度) ~x / 3)[速度=越高越好]。所以我们看到的是性能随着CPU的数量而恶化。内存绑定,交错(--interleave = all)和--localalloc在这里没有任何影响(我们监控所有QPI链接,并且每个链接的链接负载低于1%)。

#include <tbb/pipeline.h>
#include <tbb/task_scheduler_init.h>
#include <chrono>
#include <stdint.h>
#include <iostream>
#include <fcntl.h>
#include <sstream>
#include <sys/mman.h>
#include <tbb/scalable_allocator.h>
#include <tuple>

namespace {
// 8 MB
size_t chunkSize = 8 * 1024 * 1024;
// Number of threads (0 = automatic)
uint64_t threads=0;
}

using namespace std;
typedef chrono::duration<double, milli> milliseconds;

int main(int /* argc */, char** /* argv */)
{
   chrono::time_point<chrono::high_resolution_clock> startLoadTime = chrono::high_resolution_clock::now();
   tbb::task_scheduler_init init(threads==0?tbb::task_scheduler_init::automatic:threads);
   const uint64_t chunks=128;
   uint64_t nextChunk=0;
   tbb::parallel_pipeline(128,tbb::make_filter<void,uint64_t>(
         tbb::filter::serial,[&](tbb::flow_control& fc)->uint64_t
   {
      uint64_t chunk=nextChunk++;
      if(chunk==chunks)
         fc.stop();

      return chunk;
   }) & tbb::make_filter<uint64_t,void>(
         tbb::filter::parallel,[&](uint64_t /* item */)->void
   {
        void* buffer=scalable_malloc(chunkSize);
        memset(buffer,0,chunkSize);
   }));

   chrono::time_point<chrono::high_resolution_clock> endLoadTime = chrono::high_resolution_clock::now();
   milliseconds loadTime = endLoadTime - startLoadTime;
   cout << loadTime.count()<<endl;
}

关于英特尔TBB论坛的讨论:http://software.intel.com/en-us/forums/topic/346334

2 个答案:

答案 0 :(得分:4)

针对所述问题的简短更新和部分答案: 对mallocscalable_malloc的调用不是瓶颈,瓶颈是由memset分配的内存触发的页面错误。 glibc malloc与其他可扩展分配器(如Intel的TBB scalable_malloc)之间没有区别:对于大于特定阈值的分配(如果没有free d,通常为1MB;可以由{定义} {1}})内存将由anoymous mmap分配。最初,地图的所有页面都指向一个预先编辑且只读的内核 - 内部页面。当我们记忆内存时,会触发异常(介意内核页面是只读的)和页面错误。此时将打开一个新页面。小页面是4KB,因此对于我们分配和写入的8MB缓冲区,这将发生2048次。我测量的是这些页面故障在单插槽机器上并不昂贵,但在具有多个CPU的NUMA机器上变得越来越昂贵。

到目前为止我提出的解决方案:

  • 使用大页面:帮助但只会延迟问题

  • 使用预先分配和预先出现故障(madvisememset + mmap)内存区域(内存池)并从那里分配:帮助但不一定要这样做

  • 解决Linux内核中的可伸缩性问题

答案 1 :(得分:2)

第二次更新(结束问题):

使用3.10内核再次分析示例应用程序。

16GB数据的并行分配和存储的结果:

小页面:

  • 1 socket:3112.29 ms
  • 2 socket:2965.32 ms
  • 3 socket:3000.72 ms
  • 4 socket:3211.54 ms

巨页:

  • 1 socket:3086.77 ms
  • 2 socket:1568.43 ms
  • 3 socket:1084.45 ms
  • 4 socket:852.697 ms

可伸缩分配问题现在似乎已经解决 - 至少对于大页面来说是这样。