内存复制基准测试的吞吐量分析

时间:2019-01-15 00:00:52

标签: c performance x86 intel perf

我正在使用较高的copy自变量(〜1GB)对以下size函数(不是很漂亮!)进行基准测试:

void copy(unsigned char* dst, unsigned char* src, int count)
{
    for (int i = 0; i < count; ++i)
    { 
         dst[i] = src[i];
    }
}

我在Xeon E5-2697 v2上使用GCC 6.2和-O3 -march=native -mtune-native构建了这段代码。

只为您查看由gcc在我的机器上生成的程序集,我将在内部循环中生成的程序集粘贴到这里:

movzx ecx, byte ptr [rsi+rax*1]
mov byte ptr [rdi+rax*1], cl
add rax, 0x1
cmp rdx, rax
jnz 0xffffffffffffffea

现在,由于我的LLC容量约为25MB,而我要复制的容量约为1GB,因此该代码受内存限制很有意义。 perf通过大量停顿的前端周期来确认这一点:

        6914888857      cycles                    #    2,994 GHz                    
        4846745064      stalled-cycles-frontend   #   70,09% frontend cycles idle   
   <not supported>      stalled-cycles-backend   
        8025064266      instructions              #    1,16  insns per cycle        
                                                  #    0,60  stalled cycles per insn

我的第一个问题是每条指令约0.60个停顿周期。对于这样的代码来说,这似乎是一个很小的数字,因为它不会一直缓存数据,因此始终访问LLC / DRAM。由于LLC延迟为30个周期,而主内存约为100个周期,如何实现?

我的第二个问题是有关的;似乎预取器做得相对不错(不足为奇,它是一个数组,但仍然):我们有60%的时间是LLC而不是DRAM。但是,其他时间失败的原因是什么?哪个带宽/非核心部分导致此预取器无法完成其任务?

          83788617      LLC-loads                                                    [50,03%]
          50635539      LLC-load-misses           #   60,43% of all LL-cache hits    [50,04%]
          27288251      LLC-prefetches                                               [49,99%]
          24735951      LLC-prefetch-misses                                          [49,97%]

最后但并非最不重要的一点:我知道Intel可以流水线指令;带有存储操作数的mov也会这样吗?

非常感谢您!

2 个答案:

答案 0 :(得分:3)

TL; DR:未融合的域中总共有5 uops(请参阅:Micro fusion and addressing modes)。 Ivy Bridge上的循环流检测器无法在循环主体边界上分配uops(请参阅:Is performance reduced when executing loops whose uop count is not a multiple of processor width?),因此需要两个周期来分配一个迭代。在双插槽Xeon E5-2680 v2上,该循环实际上以2.3c / iter的速度运行(每个插槽10个内核,而您的12个内核),因此接近前端瓶颈所能达到的最佳性能。

预取器的性能非常好,并且在大多数情况下,循环不受内存限制。每2个周期复制1个字节非常慢。 (gcc做得很差,应该给您一个循环,该循环可以每个时钟运行1次迭代。没有配置文件引导的优化,即使-O3也不启用-funroll-loops,但是有一些技巧可能已经使用过(例如将负索引向上计数到零,或者相对于存储对负载进行索引并增加目标指针),这会使循环下降到4微秒。)

平均而言,每次迭代所产生的.3周期要比前端瓶颈平均慢 ,这可能是由于预取失败(可能在页面边界)时的停顿,或者是由于页面错误和TLB未命中此测试在.data部分的静态初始化内存上运行。


循环中有两个数据依赖项。首先,存储指令(特别是STD uop)取决于加载指令的结果。其次,存储和加载指令均取决于add rax, 0x1。实际上,add rax, 0x1也取决于自己。由于add rax, 0x1的等待时间是一个周期,因此循环性能的上限是每次迭代1个周期。

由于存储(STD)取决于负载,因此无法在负载完成之前从RS进行分派,这需要至少4个周期(如果命中L1)。此外,只有一个端口可以接受STD uops,但在Ivy Bridge上每个周期最多可以完成两个负载(特别是在两个负载到L1高速缓存中驻留的行且没有存储体冲突的情况下),导致其他竞争。但是,RESOURCE_STALLS.ANY显示RS实际值永远不会满。 IDQ_UOPS_NOT_DELIVERED.CORE计算未使用的发布槽的数量。这等于所有插槽的36%。 LSD.CYCLES_ACTIVE事件表明,大多数情况下LSD用于传递uops。但是,LSD.CYCLES_4_UOPS / LSD.CYCLES_ACTIVE =〜50%表示在大约50%的周期中,不到4 oups被传送到RS。由于次优分配吞吐量,RS不会满载。

stalled-cycles-frontend计数对应于UOPS_ISSUED.STALL_CYCLES,该计数计数由于前端停顿和后端停顿而引起的分配停顿。我不了解UOPS_ISSUED.STALL_CYCLES与周期数和其他事件的关系。

LLC-loads计数包括:

  • 所有需求负载 requests 到L3,无论请求在L3中是命中还是未命中,如果是命中,则不管数据源如何。这也包括来自页面遍历硬件的需求负载请求。我不清楚目前是否计入了来自下一页预取器的加载请求。
  • 由L2预取器生成的所有硬件预取数据读取请求,其中目标行将放置在L3中(即,在L3中或在L3和L2中,但不仅在L2中)。不包括仅将线路放置在L2中的硬件L2预取器数据读取请求。请注意,L1预取器的请求会转到L2并产生影响,并可能触发L2预取器,即它们不会跳过L2。

LLC-load-missesLLC-loads的子集,并且仅包括L3中错过的那些事件。两者都是按核心计算的。

计数请求(高速缓存行的粒度)与计数加载指令或加载uops(使用MEM_LOAD_UOPS_RETIRED.*)之间存在重要区别。 L1和L2都将挤压加载请求缓存到同一缓存行,因此L1中的多个未命中可能会导致对L3的单个请求。

如果所有存储和装入都命中L1高速缓存,则可以实现最佳性能。由于您使用的缓冲区大小为1GB,因此循环最多可导致1GB / 64 =〜17M L3需求负载请求。但是,您的LLC-loads测量值为83M,可能要大得多,这可能是由于问题中所显示的循环以外的代码所致。另一个可能的原因是您忘记使用后缀:u仅计算用户模式事件。

我对IvB和HSW的测量表明,LLC-loads:u与17M相比可以忽略不计。但是,大多数L3负载都是未命中的(即LLC-loads:u =〜LLC-loads-misses:u)。 CYCLE_ACTIVITY.STALLS_LDM_PENDING表明,负载对性能的总体影响可以忽略不计。另外,我的测量结果表明,该循环在IvB上的运行速度为2.3c / iter(在HSW上为1.5c / iter),这表明每2个周期发出一次负载。我认为次最佳分配吞吐量是造成这种情况的主要原因。请注意,几乎不存在4K别名条件(LD_BLOCKS_PARTIAL.ADDRESS_ALIAS)。所有这些意味着预取器在隐藏大多数负载的内存访问延迟方面做得很好。


IvB上的计数器,可用于评估硬件预取器的性能:

您的处理器具有两个L1数据预取器和两个L2数据预取器(其中一个可以同时预取到L2和/或L3中)。由于以下原因,预取器可能无效:

  • 未满足触发条件。这通常是因为尚未识别出访问模式。
  • 预取器已触发,但预取到了无用的行。
  • 预取器已触发到有用的行,但是在使用前已替换了该行。
  • 预取器已被触发到一条有用的行,但是需求请求已经到达缓存并丢失。这意味着需求请求的发布速度比预取器及时响应的能力要快。您可能会遇到这种情况。
  • 预取器已被触发到一条有用的行(在缓存中不存在),但是由于没有MSHR可用于保留该请求,因此必须删除该请求。您可能会遇到这种情况。

L1,L2和L3处的需求未命中次数很好地表明了预取器的性能。所有的L3未命中(由LLC-load-misses计算)也必然是L2未命中,因此L2未命中的数目大于LLC-load-misses。同样,所有需求L2缺失都必然是L1缺失。

在Ivy Bridge上,您可以使用LOAD_HIT_PRE.HW_PFCYCLE_ACTIVITY.CYCLES_*性能事件(除了未命中事件)来进一步了解预取器的性能以及评估其对性能的影响。衡量CYCLE_ACTIVITY.CYCLES_*事件很重要,因为即使未命中计数看起来很高,也不一定意味着未命中是性能下降的主要原因。

请注意,L1预取器无法发出推测性RFO请求。因此,大多数到达L1的写操作实际上都会丢失,需要在L1上为每个缓存行分配一个LFB,并可能在其他级别进行分配。


我使用的代码如下。

BITS 64
DEFAULT REL

section .data
bufdest:    times COUNT db 1 
bufsrc:     times COUNT db 1

section .text
global _start
_start:
    lea rdi, [bufdest]
    lea rsi, [bufsrc]

    mov rdx, COUNT
    mov rax, 0

.loop:
    movzx ecx, byte [rsi+rax*1]
    mov byte [rdi+rax*1], cl
    add rax, 1
    cmp rdx, rax
    jnz .loop

    xor edi,edi
    mov eax,231
    syscall

答案 1 :(得分:2)

  

我的第一个问题是每条指令约0.60个停顿周期。对于这样的代码来说,这似乎是一个很小的数字,因为它不会一直缓存数据,因此始终访问LLC / DRAM。由于LLC延迟为30个周期,而主内存约为100个周期,如何实现?

     

我的第二个问题是有关的;似乎预取器做得相对不错(不足为奇,它是一个数组,但仍然):我们有60%的时间是LLC而不是DRAM。但是,其他时间失败的原因是什么?哪个带宽/非核心部分导致此预取器无法完成其任务?

使用预取器。具体来说,取决于它是哪个CPU,可能会有一个“ TLB预取器”来获取虚拟内存转换,再加上一个高速缓存行预取器,它将从RAM中获取数据到L3中,再加上一个L1或L2预取器从L3中获取数据。

请注意,缓存(例如L3)在物理地址上工作,其硬件预取器在检测和预取对物理地址的顺序访问时起作用,并且由于虚拟内存管理/分页,物理访问在页面边界处“几乎从不”。因此,预取器会在页面边界处停止预取,并可能需要进行三个“未预取”访问才能从下一页开始预取。

还请注意,如果RAM速度较慢(或代码速度更快),则预取器将无法跟上,您将停滞不前。对于现代的多核计算机,RAM通常足够快,可以容纳一个CPU,但不能满足所有CPU的需求。这意味着在“受控测试条件”之外(例如,当用户同时运行50个进程并且所有CPU都占用RAM时),基准测试将完全错误。还有诸如IRQ,任务切换和页面错误之类的东西可能/将要干扰(尤其是在计算机处于负载状态时)。

  

最后但并非最不重要的一点:我知道Intel可以流水线指令;带有内存操作数的mov也是如此吗?

是;但是涉及到内存(例如mov)的普通mov byte ptr [rdi+rax*1], cl也将受到“使用存储转发的写入顺序”内存排序规则的限制。

请注意,有多种方法可以加快复制速度,包括使用非临时存储(故意破坏/绕过内存排序规则),使用rep movs(经过特别优化以在整个缓存行中工作,可能),使用更大的片段(例如,AVX2一次复制32个字节),自己进行预取(尤其是在页面边界)以及刷新缓存(以便在完成复制后,缓存中仍然包含有用的东西)。 / p>

但是,相反的做法要好得多-故意使大型副本非常慢,以便程序员注意到它们很烂,并且被“强迫”试图找到避免进行副本的方法。避免复制20 MiB可能要花费0个周期,这比“最差”的替代方案要快得多。