我正在使用较高的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
也会这样吗?
非常感谢您!
答案 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
计数包括:
LLC-load-misses
是LLC-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中)。由于以下原因,预取器可能无效:
L1,L2和L3处的需求未命中次数很好地表明了预取器的性能。所有的L3未命中(由LLC-load-misses
计算)也必然是L2未命中,因此L2未命中的数目大于LLC-load-misses
。同样,所有需求L2缺失都必然是L1缺失。
在Ivy Bridge上,您可以使用LOAD_HIT_PRE.HW_PF
和CYCLE_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个周期,这比“最差”的替代方案要快得多。