设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

Nah Lock:一个无锁的内存分配器

2014-12-22 11:28| 发布者: joejoe0332| 查看: 3776| 评论: 0|原作者: gones945, daxiang, C梦, 无若, shines77|来自: oschina

摘要: 我实现了两个完全无锁的内存分配器:_nalloc 和 nalloc。 我用benchmark工具对它们进行了一组综合性测试,并比较了它们的指标值。与libc(glibc malloc)相比,第一个分配器测试结果很差,但是我从中学到了很多东西 ...

概述

  我实现了两个完全无锁的内存分配器:_nalloc 和 nalloc。  我用benchmark工具对它们进行了一组综合性测试,并比较了它们的指标值。


  与libc(glibc malloc)相比,第一个分配器测试结果很差,但是我从中学到了很多东西,然后我实现了第二个无锁分配器,随着核数增加至30,测试结果线性提高。核数增加至60,测试结果次线性提高,但是仅比tcmalloc好一点。


  想要安装,输入命令: git clone ~apodolsk/repo/nalloc,阅读 README文档。


背景

  内存分配器很重要,因为大多数的程序在使用它们,并且许多程序在大量使用它们.对于数以亿计的好程序而言,一个糟糕的分配器会是竞争的中心点;而一个好的分配器会是天上掉下来的替代品,用来扭转不良内存访问的程序为硬件友好的模式.


  我所知道的所有可伸缩的内存分配器,包括已存的无锁分配器,通过拆分地址空间到CPU或线程局部子堆(subheap),尝试将分配过程转化为数据并行问题.在最好的情况下,这会带来优化位置,减少错误共享和增强预处理的额外好处,因为每个线程访问的是线程私有的高速缓存线的邻近集合.


  在最糟糕的情况下,使用内存分配器的程序和分配器地址不是并行的,这样就需要小心的设计,来减少导致内存块在线程间转移的人为的或者显式的通讯.再者,能实现这点的可用选项,与减少内存碎片和内存崩溃的需要相冲突.


  这里无需描述时序算法.


  tcmalloc 和 jemalloc 是性能最好的通用分配器中的两个.ptmalloc是glibc默认的分配器.


分析和挑战

  对于这个项目是否和这个教程相匹配,我有些信心不足.所以,对这个问题,我尝试着提出一些非明显的分析.我在介绍里面提到过,所以你可以略过此处.


  我提到的"可变的数据并行"是一个新的难点.内存分配与我们在类中所见的所有问题形成鲜明的对比,因为它们可以在工作前做并行性分析.例如,在渲染器中,你可以通过这种方式处理,即允许数据不经过通讯来处理,这样来达到工作和间隔之间程序化的平衡.


  另一方面,一个并行分配器,由未知的依赖提供预分配的,模糊的工作负载,这些依赖需要未知数量的通讯来解决.由此,它需要扩大到不同的并行级别.


  实际上,这意味着我不得不花费大量的时间考虑,"如果工作负载强制线程大量通讯,这个选择有意义吗?是有意义还是无意义呢?".(在这个方向,我没有任何收获.根据我的测试,这些分配器的状态,看起来和最糟糕的情况一样.)


  令我非常兴奋的是,一些设计问题与web服务器非常类似。像服务器一样,在突发的工作负载中,分配器需要满足延迟和吞吐量的目标,这通过平衡资源使用的目标,如减少碎片和崩溃,来实现。“分配更多的节点,甚至比需要的还多”和“从全局堆中获取更多的页,甚至比需要的还多”有类似的开销和益处,随着而来“启动开销”的问题也会浮现。


  接下来提到的,是我碰到的没那么抽象的困难点。


__nalloc

介绍

__nalloc是"幼稚的",因为它可能是,我每个学期开始会用到的,几乎同样的基础设计.我假定瓶颈在同步控制,于是,我计划将一个快速的单线程算法,加入到高效的无锁包装器.如果我更倾向于分析,而不是"做听起来很优雅的事情",或者使用已存的只有大概轮廓的分配器,我所遇到的一些神奇的,暂未命名的问题,可能从一开始就会变得非常明显.

__nalloc和nalloc的目标分配大小均小于或等于1024B,这主要是为了让任务更简单.


主要想法如下:

  • 在每个局部线程子堆栈运行单线程的分配器 ("附有范围标识,以及易于分开与合并的隔离链表,").

  • 使用页的无锁栈,从全局堆中分配内存.

  • 使用属于每个线程的另一个无锁栈,返回移动的/"善变的"块到它们的原始线程.


下面是一个更详细的算法.如果觉得上面附加的描述言之有理,你可以略过此处.不管怎样,在最后提到的关于"善变的块"的部分很有意思:

  • 每个线程保留着各种大小空闲内存块的"空闲双链表".

  • 每个块位于一个"arena",它是一个页大小的块内存,地址和大小"自然对齐".线程通过从页的全局无锁栈获取更多的arena来填充空闲链表.

    • 当页的栈为空的时候,线程通过调用mmap()从OS分配一批页获取arena.

    • 每个新的arena保留这最大空间的单一块.

  • 在malloc()之中, 一个线程从栈里取出一个匹配请求大小的块.

    • 它削去多余的空间使之成为新的块.

    • 如果链表为空,它会尝试下一个最大的块,直到分配完毕而不得不获取新的arena.

  • 在free()之中, 一个线程会尽可能的合并空闲块及其相邻的部分.

    • 为了实现这点,每个块B需要4B的头信息来存储内存中"is_free"标识,B大小以及B的"左边"的大小.

    • 所有合并的相邻的空间都会从它们的空闲链表中删除.

  • 如果线程F释放了线程M分配的块B,线程F会将块B插入到一个与线程M相关联的"善变的"无锁栈.

    • 当线程M用完了块,它会在单个cmpxchg操作中取出整个栈的内容,然后将每个块加入到它的空闲链表.

    • 线程F如何找到这个链表呢?每个arena保留了线程"善变的"块的栈的指针.因为arena是自然对齐的,线程F可以根据线程B的地址计算线程B的arena的地址.

    • 每个arena也有一个内部栈存储"非自身占有的块".如果由于线程退出,而不得不在所有的块空闲之前释放arena,它会修改arena的"善变的块"指针指向"非自身占有的块"的栈.

基准测试

我编写了三个基准测试程序,分别在perf, gperftools和vtune中进行分析.

第一个测试中,每个线程随机分配,写入和释放到一个私有的内存池.

第二个测试中,线程分配到一个全局池,它实现为一个无锁栈的集合.

第三个测试中,单线程分配到一个全局池,其他线程从全局池中释放内存.

全部的工作负载在线程间保持恒定.分配大小限制到小于等于1024B.每个线程最大分配字节数受到限制,但是线程在释放旧的分配空间后,可以申请新的空间.全局时间使用gettimeofday()来计算时间间隔.

除了提到的,即将展示的图表是由第一个测试生成的.

按时间先后顺序优化和评论

我需要经常上下对齐地址.令人吃惊的是,我的align_up()和align_down()函数,只是一个增加或减少,以及取模运算,竟然占用了9%的运行时间.我为2的平方使用bitops来替换这些函数,之前的开销完全消除.它没有提升规模,但是我惊奇的发现,某种方式的算术中,div竟有如此大的差别.

我最初的设计是使用"arena"双链表而不是块.我原以为它会增加碎片和位置,以及,按顺序预取会耗尽arena.取而代之的是,线程通过O(n)的时间复杂度搜索成千上万个"不是足够满"的arena,寻找一个足够的连续空闲空间,来满足大的分配请求.像旋转arena之类的技巧起不到作用.

Arena初始化消耗了15%的运行时间.在相关的ASM中,最耗时的指令是第一个MOV到内存的指令.这可能会令人讨厌,但是我碰巧得知Linux过量使用了内存.那就是,mmap()会保留虚拟地址,但是,它假定你并非实际需要内存,同时,在你真正使用之前,你不会获取物理帧.我认为arena_init是页面故障处理,为了实现那些新的VM映射.有人告诉我一个mmap的标识来取消过度使用.

这个堵塞了页面故障,但是余下的工作只是移动到mmap().我实现了一个批量和预取的组合(每个arena的分配也进行了N个其它的分配),从那时起,mmap()的消耗小于1%.这样运行并不明显,但是看起来可能最耗时的部分在VM片段树的查找.内核对每个请求只处理其中之一--明显的分阶段处理系统调用以及加锁的开销.

在这点上,你可能会争论,使用mmap(NULL, MAP_ANOP,...),_nalloc只是将疑难的常见分配问题转移到内核.但是,有人不管在什么情况下,都不得不调用mmap().同时,如果你打算抢占式的使用mmap()分配自己管理的大块内存,你无法取消过度使用.只要mmap的开销不占支配地位,这样做就是正确的事情.这可能是,我读过的许多无锁分配器论文,也认同这个技巧的原因.好奇于内核是否可以进行无锁分配,是一件有趣的事情.我简短的尝试过,并以失败告终,Linux使用了锁.但是Alexia Massalin在Synthesis之中将锁去掉了.

另一个不那么正确的事情是,我从未将内存返还给系统.释放后再使用可能会让页的栈出栈变得极其脆弱.这和我在无锁栈里面评论里提到的Bug是一样的.这颇具讽刺,因为评论也指出我的项目是因这个问题而萌发.如果我要解决这个问题,这会是一个假想的方案:在栈上面使用引用计数,只有在arena出栈时,引用计数为0才释放.你可以通过跟踪引用计数最后为0的标签值来检查.然后,在尝试释放时,检查页出栈的标签值是否小于等于上面提到的标签值.

在向Kayvon发邮件声称"正确"后,我认识到有非常糟糕和明显的竞态条件:

  • 一个线程退出后修改arena"善变的"块指针为arena的非自身块的栈.

  • 同时,另一个线程读取之前的指针,并将自己压入刚刚死掉线程的"善变的"块的栈.

  • 页故障或崩溃.

nalloc的设计对此也是脆弱的.我找到的一个解决方案(在接下来描述)在此不起作用.你可以为已分配的块数进行引用计数,它可能封闭线程"善变的"块的栈.会有一个更明智的(无锁的)方式,而不是通常的加锁指令的方式,来满足罕见的极端情况吗?

结果

在这之后,一段长时间的修改Bug,下面是,理论上完美并行,无移动工作负载,在64核机器上的性能表现,:

这样, __nalloc和ptmalloc表现的一样差 (并不想我描述的那么差). 下面是gperftools的样本配置在GHC* 6核上面的性能表现:

*(在ALADDIN机器上,我没有获取到 libunwind/gperftools的配置)


  下面是根据Linux perf得到的硬件计数器报告:


 Performance counter stats for './natest -t 6 -o 10000':

       690,482,105 L1-dcache-loads
        67,097,600 L1-dcache-misses
         #    9.72% of all L1-dcache hits   [26.08%]
       523,794,050 L1-dcache-stores
        22,848,232 L1-dcache-misses
         5,618,859 L1-dcache-prefetches
           377,630 L1-dcache-misses
        29,608,157 LLC-loads
        14,276,385 LLC-misses
               #   48.22% of all LL-cache hits    [21.63%]
        45,153,262 LLC-stores
         8,554,146 LLC-misses
         6,247,900 LLC-prefetches
         2,285,035 LLC-misses
               389 page-faults                                               
     6,665,279,571 cpu-cycles
     4,921,627,270 stalled-cycles-frontend   #   73.84% frontend cycles idle
     2,489,718,064 stalled-cycles-backend    #   37.35% backend  cycles idle
        24,771,227 branch-misses
        22,731,022 cache-misses              #   46.787 % of all cache refs
        48,583,721 cache-references
     3,711,376,010 instructions              #    0.56  insns per cycle
                                             #    1.33  stalled cycles per

       0.415147303 seconds time elapsed

和tcmalloc对比:

 Performance counter stats for './tctest.sh -t 6 -o 10000':

       691,325,411 L1-dcache-loads
        18,485,376 L1-dcache-misses
         #    2.67% of all L1-dcache hits   [27.18%]
       403,827,409 L1-dcache-stores
         6,107,731 L1-dcache-misses
         1,874,221 L1-dcache-prefetches
           397,279 L1-dcache-misses
         5,351,265 LLC-loads
           799,161 LLC-misses
               #   14.93% of all LL-cache hits    [23.60%]
        13,872,244 LLC-stores
         2,803,597 LLC-misses
            58,189 LLC-prefetches
             6,720 LLC-misses
            15,043 page-faults                                                 
     1,679,857,462 cpu-cycles
       566,843,996 stalled-cycles-frontend   #   33.74% frontend cycles idle    [23.33%]
       515,528,620 stalled-cycles-backend    #   30.69% backend  cycles idle    [23.07%]
         5,163,499 branch-misses          
         2,378,294 cache-misses              #   26.742 % of all cache refs     [22.54%]
         8,893,414 cache-references
     2,850,831,772 instructions              #    1.70  insns per cycle        
                                             #    0.20  stalled cycles per insn [27.57%]

       0.106878690 seconds time elapsed

  需要指出的是,Linux sysfs在目标机器(ghc29)报告说到,当L1和L2对每个物理核心是局部缓存的时候,LL/L3缓存是共享的.

__nalloc是有内存限制的. 它和tcmalloc一样,最多产生3.9x 的LLC引用,同时,最多导致6.3x的缺失.例如,对于每个free(), merge_adjacent()获取3个块头部,并插入到全局空闲链表.对于每个malloc(), shave获取3个块头部,并从空闲链表弹出.另一方面,tcmalloc的配置显示,它只是压入和弹出一个单链表.


  最值得说的细节是,L1数据缓存加载只有9%的缺失,而在LLC中缺失了48%.这个效果在单线程的情况不存在.随着线程数量,分配数以及运行时间的增加,出现的概率也在增加.这样,_nalloc起初有足够的空间来适应工作集,然后就出现一些非预期的情况,给出上面的条件则更可能出现这样的效果.


  我认为这是全局块链表的隐含代价.随着运行时间和分配数量的增加,在链表中相邻的块更可能来自不同的arena,每个arena可能已经被强制退出共享的LLC.当它获取这些块的时候,大多数分配强制加载多个高速缓存线到shave.如果malloc返回块处于"arena"无序状态,则free()更可能在这些块调用时处于更糟糕的状态,并在合并时,获取更多的外部高速缓存线.


  这是纯单线程的效果--为什么在更多的线程的时候,情况会变得更糟糕呢?我认为,这是因为更多的线程导致访问超出了更多的arena.单线程程序可以为一个合适大小的块搜索所有的arena.但是在多个线程的情况下的一个线程,必须限制自身在子集之中,这样更可能使用mmap生成新的arena.


  所以假设处于可怜的境地,即便是单线程的情况下,而在多线程未充分利用的情况下,结果会更糟糕.(这并不是我所听到的它使用的碎片--碎片是连续块"存在",但是你不能访问它,因为地址空间是分离的).


  注意:在100%的并行基准测试中,不应该有冲突丢失--在同样的测试中,nalloc的低LLC丢失率反映了这种情况.回想一下,我应该通过某种方式来测试我的假设,而不是跳到一个新的分配器.


  更新:在单线程情况下,我通过增加分配数进行"测试",得到了相同的缺失率.所以,实际上,这意味着我的序列算法先天不足,多线程未充分利用(或者其它属性)的时候,即_nalloc获取LLC的时候,情况更糟糕.


  提到合并,除了全局链表,我找不到其它选择.我考虑过部分取消合并,保持单一的"当前arena"指针来检查,复杂度为O(1);或者,尝试提升链表中块的排序.但是418分配意味着,相比于改变简单的事情,增加复杂性看起来并不能获取益处.


  Alex的报告提到,它没有找到现代的合并访问分配器.我自己研究jemalloc,tcmalloc以及一些无锁malloc也没有任何发现.我觉得,我可能已经发现,为何每个人都避免合并访问了.在给定硬件后,他们一定认为这太幼稚太天真.




nalloc

介绍

我设计nalloc是为了低频率的,以一种更局部化的方式访问内存.

想法是使用同样的无锁包装器,但是使用"slab"分配来代替在arena间的合并访问.每个页/"slab"只提供单一大小的块.好处是,你无需存储头部,也无需检查邻近的块来分配或释放.你无需块的双链表,因为你不会"从中间删除".常见的分配是两次加载和一次存储,用来弹出有锁栈.缺点是,你会浪费空间.

算法:

  • 每个线程为每个大小的类保留"slab"的一个私有的有锁栈.

  • 自然对齐页大小的slab来自一个页的全局LF栈.

  • 每个slab包含一个线程私有的相同大小块的有锁栈.

  • malloc()从头slab检查,从slab的块的栈弹出.

    • 如果slab为空,则slab从slab栈弹出,不放入任何数据结构."唯一"引用它的是从它分配而来的块.

  • free()利用自然对齐得到块的slab,并将块压入slab的块的栈之中.

    • 如果slab的块的栈为空,则slab会根据它的大小添加回slab的栈.

  • 每个slab包含一个"善变的"块的LF栈.

    • 如果malloc()耗尽了一个slab,它将弹出slab的"善变的"块,并将它们压入slab的空闲块的栈.

    • free()会压入"善变的"块的栈,如果它检测到另一个线程拥有这个slab.如果它已经释放slab里面的最后一个块,同时在slab里面的所有块都是"善变的"块,则它将释放这个slab或为这个调用线程"窃取"它.

如果你不在栈里为cmpxchg做循环计数,则时间复杂度为O(1).


  最大的挑战是处理"善变的"块,如何以一种高效的方式,避免在_nalloc中的竞争或者其它竞争.考虑一些选择项(或者不考虑:可略过的),当线程M为slab S的所有者,同时线程F正在释放一个"善变的"块到S:

  • 假设F只是压入善变的块的栈.问题便是,M将free()当作一个信号,即S为非空.如果每个线程压入善变的块的栈,直到S的栈满,那么将不会再收到信号,同时S产生泄露.

  • 假设F尝试着以某种方式积极的发出信号,或者只是移动S回M的slab栈(让它们变成无锁状态).M可能退出,这样就会出现_nalloc面临的同样的问题.

    • 你可以将slab栈存储在一个比M存活期长的管理块上,同时,对有slab引用的块的数目进行引用计数.我实现了这里面的大部分内容--不像在合并访问,利用可以在slab中计算已分配的块的数目的特点,你可以避免在每个malloc和free上更新引用计数.但是,只是简单的使用互斥体去决断,会很复杂,也很困难.

  • M可以"搜索信号"而不只是等待.搜索时间复杂度为O(N).

  关键在于,M并非真正需要记录空slab.这样,在我上面描述的算法中,M有意的删除所有到S的引用,除了那些从S分配的块隐含的引用.如果F的插入会填满只包含善变的块的slab,那么M或者其它的任何线程都不会保留到S的引用,同时F可以对S任意操控.否则,F不能通知M,但M不会产生泄露,因为某个地方依旧保留着指向它的一个引用.

  开心的是,"窃取"是我从开始就想支持的内容.假设你有一个生产者-消费者模型,消费者也需要内存.如果消费者已经将所有内存加载到缓存了,那么将所有的内存返还给生产者真的有意义吗?同时,即使"窃取"对于工作负载来说是不合适的,如果最后一个要加载slab到缓存的线程,就是释放它的线程,那么这样的效果会更好.

  这个方案的代价是,你要么忍受冲突产生的丢失,要么为每个slab保留64B,用来将"善变的"块的栈从块的栈放入到不同的高速缓存线.对于大的slab,64B不算是太大的开销.

  为了避免竞争,当尝试着查明slab是否为空,或者它是否只包含"善变的"块,我需要直接在我的无锁栈中存储一个大小值.我从标签字段获取了32位来存储.

  当_nalloc中的arena链表没有工作的时候,slab栈会工作,因为每个slab可以保证满足一个特定大小的分配.它拥有我之前提到过的理论上的好处:在分配器以及使用内存的程序中的局部性,在分配器中的序列访问(查看下文),以及低碎片化(其它slab有更好的机会填充和释放).这是slab分配的一个优势,当我进行初始设计的时候我完全错过了--除了尝试和失败,我可能在任何情况下都不会发现它.

  不像_nalloc中清除"善变的"栈的时间复杂度O(N),当stack_popall()交换时,弹出"善变的"块的时间复杂度为O(1).关键在于知道slab块的栈为空,这样,你就可以简单的移动"善变的"块的栈的栈顶节点到它上面,而无需断开链表.

  Intel的x86优化指南(和原因)指出,预取实际上会检测序列访问.所以slab也保留了起始于slab基底部的连续块段的大小.它们将更倾向于从段而不是从栈分配和释放(实际上,它们应该更倾向于从栈分配--我在后面实现了).这具备额外的效果,即让slab初始化的时间复杂度为O(1).因为在slab初始化后,栈的遍历字段不需要写入每个块.

  未来的提升点可能在于使用两个栈--一个使用以前的顺序;一个严格按照降序,直到遇到连续段并添加。如果&second_stack->top处于同样的高速缓存线,那将花费很小的开销.(这听起来像更通用的算法的一个特殊情况.)

  对于非少量分配的情况,这个算法缺乏一些合理性.对于每个位于0B和(FOO * 4096)B之间的16B,你不会真正拥有一个slab栈,但是你也不希望用1024B的块来满足513B的请求.另外,大的块由于头部而产生大的开销,所以,在那种情况下,你希望有大的slab--但是,如果没有完全使用,或者过量使用处于关闭状态,那么大的slab会浪费更多的内存.未来的提升点可能是,当它了解到是否需要为了速度而交换空间,以及是否浪费的空间会成为总使用空间的重大碎片时,弹性的放开块大小或过量使用开关(或者,为slab"善变的"块的栈,触发保留一个高速缓存线).

  同时,通过一些修改,我认为,为了满足大的分配,使用空间有效的_nalloc可能变得合情合理.一个程序可能不会进行快速的大块分配而耗尽分配器,因为它会受制于用实际数据填充分配空间的时间(假设程序不会经常为大量事情一次性预分配空间).

  我在完成设计后的唯一的优化是,用算术方法计算分配的大小来替代大小类查找表.这消除了5%的运行时.回想一下,这可能是因为全局LF栈的错误共享--我用__aligned__(64)来标记栈,因为担心这点,但是我并未标记表.

结果

结果根据上述准则生成。

nalloc在100%并行情况下,最终表现如下。



  性能报告如下:

 Performance counter stats for './natest -t 6 -o 10000':

       519,955,974 L1-dcache-loads
        22,066,220 L1-dcache-misses
         #    4.24% of all L1-dcache hits   [26.59%]
       355,314,274 L1-dcache-stores
         5,272,972 L1-dcache-misses
         4,815,201 L1-dcache-prefetches
           396,406 L1-dcache-misses
         7,920,682 LLC-loads
         1,391,780 LLC-misses
               #   17.57% of all LL-cache hits    [22.11%]
        12,972,675 LLC-stores
         1,386,302 LLC-misses
            14,266 LLC-prefetches
             2,807 LLC-misses
               384 page-faults                                                 
     1,712,655,726 cpu-cycles                #    0.000 GHz
       692,724,604 stalled-cycles-frontend   #   40.45% frontend cycles idle
       492,748,671 stalled-cycles-backend    #   28.77% backend  cycles idle
         7,614,825 branch-misses
         2,800,160 cache-misses              #   23.218 % of all cache refs
        12,060,173 cache-references
     2,413,914,436 instructions              #    1.41  insns per cycle
                                             #    0.29  stalled cycles per insn [27.19%]
       0.105481932 seconds time elapsed

它仍然有内存限制,但不是很严重。 相比于前面 tcmalloc 的表现,nalloc 更少的加载和存储。

重复的,上面表明 一个完美并行度的固定负载测试,线程随机分配、读写私有池,且空闲块 <=1024。线程间没有块移动。


结果看起来很好。在不到500LOC(提供的超过2000行复杂代码),nalloc与jemalloc和tcmalloc相比,它们各自有10000和20000 LOC。主要的一点是,nalloc的目标仅仅是一个测试和一个标准,剩下的是为生产准备的。许多知识都有差异和类似之处。但是,我认为这可能就是nalloc被设计用于覆盖不同地情况的原因。

在这些测试中llalloc是Lockless公司的Lockless分配器,这并不是100%无锁编程(它有一个锁在全局堆中)。运行时使用相同的测试,它比nalloc在串行下块两倍,但是这只是说其在64核实现的25x加速度。当以两倍长度运行时,它实现了相对于nalloc的约40x的加速度。在我的基准中,假设一次每线程(once-per-thread)的开销必须被控制,在那个阶段(大约用42ms时间来完成(time-to-completion)),但是我没有研究。


  然而,我确信其它的分配器不会受到这的影响.但是,当问题大小增长的时候,它们表现的会更糟糕.同时,共享缓存可能会再次成为议题.

  真正的无锁分配器,NBMalloc,Streamflow,以及Maged Michael实现的alloc,在ALADDIN上面都编译失败了(在我的机器上有两个编译失败了).我觉得,现在,我不应该尝试去解决这个问题.




  对于64核的机器,完成时间的图表很难看懂,但是,我发现"工作"图表是并行和顺序执行特性的可读混合体.Y轴是线程数*完成时间(num_threads * time-to-completion).由于我期待着工作负载是平滑的分布,这就成为全部已做工作的一个大概统计.(相对于图中的曲线而言)好的斜率应该小于等于0.超过单一线程情况下(Y值)的Y值提供了近似的开销情况.

在第二个测试中,在线程分配和释放的全局池中,块的迁移非常高.

警告:这些测试有缺陷.请往下看.

在第三个测试中,单个线程生产块,供其它线程释放,块的迁移也非常高. 

在第三个测试中,单个线程生产块,供其它线程释放,块的迁移也非常高. (此句原文中出现重复)

随着线程间通过全局池发送块的通讯开销占据主导,测试2和测试3会产生缺陷.在6核机器上运行测试2, 76%的运行时间花费在nalloc之外,大多数操作位于无锁栈之上.我已经将池划分为多个栈,同时%100保证栈位于不同的高速缓存线.测试需要重写,用来隐式的转换块,或者只是降低频率.在这点上,我宁愿加入Alex R.的探求,通过块迁移来查明真正的工作负载.

课程回顾

  • 如果你已有一个奢华的解决方案,你需要提前了解你的问题可能的瓶颈.

  • 反过来,甚至当它只是可能的情况下,尝试和失败可能是唯一的方式,来更好的权衡利弊,并处理这些瓶颈.

  • 你不能心存侥幸的围绕快速单线程代码,来编写一个快速并行包装器.你必须关注并行程序的串行部分,因为它们之间可能通过一种隐晦的方式来竞争或沟通.这是SAXPY的教训(尽管SAXPY有一个不同的隐含议题),但是我已经忘记它了.

  • 如果你是理论上的完美并行,但是没有看到完美加速,考虑共享内存 和/或者 共享缓存 VS 工作集大小的效果.

  • LibreOffice图表在LibreOffice之外看起来更丑陋.

勘误表

"加速"可能是我的"性能"图表更具描述性的名字.

参考


酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部