草庐IT

c++ - NUMA 架构上大 (8MB) 内存区域的可扩展分配

coder 2023-05-31 原文

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

  • 在 UMA 架构上,应用程序随着管道使用的线程数线性扩展(通过 task_scheduler_init 设置)

  • 在 NUMA 架构上,当我们将应用程序固定到一个套接字(使用 numactl)时,我们会看到相同的线性扩展

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

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

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

我使用 numactl 附加了各种执行的性能统计信息。 Interleaving/localalloc 没有任何效果(QPI 总线不是瓶颈;我们验证了使用 PCM,QPI 链路负载为 1%)。我还添加了一个图表,描述了 glibc、tbbmalloc 和 tcmalloc 的结果。

性能统计 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 绑定(bind)我们有以下发现:1 个 CPU(速度 x)、2 个 CPU(速度 ~ x/2)、3 个 CPU(速度~ x/3) [速度=越高越好]。所以我们看到的是性能随着 CPU 数量的增加而恶化。内存绑定(bind)、交错 (--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

最佳答案

对所描述问题的简短更新和部分答案: 对 mallocscalable_malloc 的调用不是瓶颈,瓶颈是由 memset 分配的内存触发的页面错误。 glibc malloc 和 Intel 的 TBB scalable_malloc 等其他可扩展分配器没有区别:用于大于特定阈值的分配(通常为 1MB,如果没有 free code>d; 可以由 madvise 定义)内存将由匿名 mmap 分配。最初,映射的所有页面都指向一个内核内部页面,该页面是预置的和只读的。当我们 memset 内存时,这会触发异常(注意内核页面是只读的)和页面错误。此时将 0ed 一个新页面。小页面为 4KB,因此对于我们分配和写入的 8MB 缓冲区,这将发生 2048 次。我测量的是,这些页面错误在单插槽机器上并没有那么昂贵,但在具有多个 CPU 的 NUMA 机器上变得越来越昂贵。

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

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

  • 使用预分配和预故障(memsetmmap + MAP_POPULATE)内存区域(内存池)并分配从那里:有帮助,但不一定想这样做

  • 解决 Linux 内核中的可扩展性问题

关于c++ - NUMA 架构上大 (8MB) 内存区域的可扩展分配,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13803697/

有关c++ - NUMA 架构上大 (8MB) 内存区域的可扩展分配的更多相关文章

  1. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  2. ruby-on-rails - 如何优雅地重启 thin + nginx? - 2

    我的瘦服务器配置了nginx,我的ROR应用程序正在它们上运行。在我发布代码更新时运行thinrestart会给我的应用程序带来一些停机时间。我试图弄清楚如何优雅地重启正在运行的Thin实例,但找不到好的解决方案。有没有人能做到这一点? 最佳答案 #Restartjustthethinserverdescribedbythatconfigsudothin-C/etc/thin/mysite.ymlrestartNginx将继续运行并代理请求。如果您将Nginx设置为使用多个上游服务器,例如server{listen80;server

  3. Ruby Koans about_array_assignment - 非平行与平行分配歧视 - 2

    通过ruby​​koans.com,我在about_array_assignment.rb中遇到了这两段代码你怎么知道第一个是非并行赋值,第二个是一个变量的并行赋值?在我看来,除了命名差异之外,代码几乎完全相同。4deftest_non_parallel_assignment5names=["John","Smith"]6assert_equal["John","Smith"],names7end45deftest_parallel_assignment_with_one_variable46first_name,=["John","Smith"]47assert_equal'John

  4. ruby-on-rails - Ruby 中的内存模型 - 2

    ruby如何管理内存。例如:如果我们在执行过程中采用C程序,则以下是内存模型。类似于这个ruby如何处理内存。C:__________________|||stack|||------------------||||------------------|||||Heap|||||__________________|||data|__________________|text|__________________Ruby:? 最佳答案 Ruby中没有“内存”这样的东西。Class#allocate分配一个对象并返回该对象。这就是程序

  5. ruby - 在 Ruby 中重新分配常量时抛出异常? - 2

    我早就知道Ruby中的“常量”(即大写的变量名)不是真正常量。与其他编程语言一样,对对象的引用是唯一存储在变量/常量中的东西。(侧边栏:Ruby确实具有“卡住”引用对象不被修改的功能,据我所知,许多其他语言都没有提供这种功能。)所以这是我的问题:当您将一个值重新分配给常量时,您会收到如下警告:>>FOO='bar'=>"bar">>FOO='baz'(irb):2:warning:alreadyinitializedconstantFOO=>"baz"有没有办法强制Ruby抛出异常而不是打印警告?很难弄清楚为什么有时会发生重新分配。 最佳答案

  6. ruby - 使用 `+=` 和 `send` 方法 - 2

    如何将send与+=一起使用?a=20;a.send"+=",10undefinedmethod`+='for20:Fixnuma=20;a+=10=>30 最佳答案 恐怕你不能。+=不是方法,而是语法糖。参见http://www.ruby-doc.org/docs/ProgrammingRuby/html/tut_expressions.html它说Incommonwithmanyotherlanguages,Rubyhasasyntacticshortcut:a=a+2maybewrittenasa+=2.你能做的最好的事情是:

  7. ruby - 如何计算 Liquid 中的变量 +1 - 2

    我对如何计算通过{%assignvar=0%}赋值的变量加一完全感到困惑。这应该是最简单的任务。到目前为止,这是我尝试过的:{%assignamount=0%}{%forvariantinproduct.variants%}{%assignamount=amount+1%}{%endfor%}Amount:{{amount}}结果总是0。也许我忽略了一些明显的东西。也许有更好的方法。我想要存档的只是获取运行的迭代次数。 最佳答案 因为{{incrementamount}}将输出您的变量值并且不会影响{%assign%}定义的变量,我

  8. ruby-on-rails - 缺失区域;使用 :region option or export region name to ENV ['AWS_REGION' ] - 2

    我知道还有其他相同的问题,但他们没有解决我的问题。我不断收到错误:Aws::Errors::MissingRegionErrorinBooksController#create,缺少区域;使用:region选项或将区域名称导出到ENV['AWS_REGION']。但是,这是我的配置开发.rb:config.paperclip_defaults={storage::s3,s3_host_name:"s3-us-west-2.amazonaws.com",s3_credentials:{bucket:ENV['AWS_BUCKET'],access_key_id:ENV['AWS_ACCE

  9. ruby - 使对象的行为类似于 ruby​​ 中并行分配的数组 - 2

    假设您在Ruby中执行此操作:ar=[1,2]x,y=ar然后,x==1和y==2。是否有一种方法可以在我自己的类中定义,从而产生相同的效果?例如rb=AllYourCode.newx,y=rb到目前为止,对于这样的赋值,我所能做的就是使x==rb和y=nil。Python有这样一个特性:>>>classFoo:...def__iter__(self):...returniter([1,2])...>>>x,y=Foo()>>>x1>>>y2 最佳答案 是的。定义#to_ary。这将使您的对象被视为要分配的数组。irb>o=Obje

  10. 键删除后 ruby​​ 哈希内存泄漏 - 2

    你好,我无法成功如何在散列中删除key后释放内存。当我从哈希中删除键时,内存不会释放,也不会在手动调用GC.start后释放。当从Hash中删除键并且这些对象在某处泄漏时,这是预期的行为还是GC不释放内存?如何在Ruby中删除Hash中的键并在内存中取消分配它?例子:irb(main):001:0>`ps-orss=-p#{Process.pid}`.to_i=>4748irb(main):002:0>a={}=>{}irb(main):003:0>1000000.times{|i|a[i]="test#{i}"}=>1000000irb(main):004:0>`ps-orss=-p

随机推荐