计时方式。我可能会设置它,以便通过命令行参数选择测试,这样我就可以用perf stat ./unaligned-test
,并获取性能计数器结果,而不仅仅是每个测试的挂钟时间。这样,我就不必关心涡轮/节能,因为我可以在核心时钟周期中进行测量。 (与gettimeofday
/ rdtsc
参考周期,除非您禁用涡轮增压和其他频率变化。)
您仅测试吞吐量,而不测试延迟,因为没有任何负载是相关的。
您的缓存数量将比您的内存数量差,但您可能不会意识到这是因为您的缓存数量可能是由于缓存数量的瓶颈造成的分割加载寄存器 https://stackoverflow.com/questions/46480015/vipt-cache-connection-between-tlb-cache/46480702?noredirect=1#comment96369512_46480702处理跨越缓存行边界的加载/存储。对于顺序读取,高速缓存的外层仍然总是会看到对整个高速缓存行的一系列请求。只有从 L1D 获取数据的执行单元才需要关心对齐。要测试非缓存情况下的未对齐情况,您可以进行分散负载,因此缓存行拆分需要将两个缓存行带入 L1。
Cache lines are 64 bytes wide1, so you're always testing a mix of cache-line splits and within-a-cache-line accesses. Testing always-split loads would bottleneck harder on the split-load microarchitectural resources. (Actually, depending on your CPU, the . Recent Intel CPUs can fetch any unaligned chunk from inside a cache line, but that's because they have special hardware to make that fast. Other CPUs may only be at their fastest when fetching within a naturally-aligned 16 byte chunk or something. @BeeOnRope says that AMD CPUs may care about 16 byte and 32 byte boundaries https://stackoverflow.com/questions/45128763/unaligned-access-speed-on-x86-64/45129784?noredirect=1#comment77232619_45129784.)
你不是在测试存储→负载转发根本不。对于现有测试以及可视化不同比对结果的好方法,请参阅此 stuffdcow.net 博客文章:x86 处理器中的存储到加载转发和内存消歧 http://blog.stuffedcow.net/2014/01/x86-memory-disambiguation/.
通过内存传递数据是一个重要的用例,未对齐 + 缓存行分割可能会干扰某些 CPU 上的存储转发。要正确测试这一点,请确保测试不同的错位,而不仅仅是 1:15(向量)或 1:3(整数)。 (您当前仅测试相对于 16B 对齐的 +1 偏移量)。
我忘记了它是否只是用于存储转发,或者用于常规加载,但是当负载均匀地跨缓存行边界(8:8 向量,也可能是 4:4 或 2:2)分割时,惩罚可能会更少整数分割)。你应该测试一下这个。 (我可能会想到P4lddqu
或核心2movqdu
)
Intel优化手册 https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf具有大的错位表与从宽存储转发到完全包含在其中的窄重新加载的存储转发。在某些 CPU 上,当宽存储自然对齐时,这在更多情况下有效,即使它不跨越任何缓存行边界。 (也许在 SnB/IvB 上,因为它们使用具有 16B 存储体的存储 L1 缓存,并且这些存储体之间的分割可能会影响存储转发。
我没有重新检查手册,但如果你真的想通过实验来测试它,那么你应该寻找它。)
这提醒我,未对齐的负载更有可能引发 SnB/IvB 上的缓存存储体冲突(因为一个负载可能会触及两个存储体)。但是您不会看到来自单个流的加载,因为访问相同的银行same一个周期内连线两次就可以了。它只访问同一个银行不同的不可能在同一周期内发生的线路。 (例如,当两次内存访问间隔为 128 字节的倍数时。)
您不会尝试测试 4k 页面分割。它们比常规缓存行分割慢,因为它们还需要两个TLB https://en.wikipedia.org/wiki/Translation_lookaside_buffer检查。 (不过,Skylake 将其从约 100 个周期的惩罚改进为超出正常加载使用延迟的约 5 个周期的惩罚)
你测试失败movups
在对齐的地址上,所以你不会检测到movups
慢于movaps
在 Core 2 及更早版本上,即使内存在运行时对齐也是如此。 (我觉得不统一mov
即使在 Core 2 中,加载最多 8 个字节也可以,只要它们不跨越缓存行边界即可。我不知道您需要查看多旧的 CPU 才能发现缓存行中非矢量加载的问题。它只是 32 位 CPU,但您仍然可以使用 MMX 或 SSE,甚至 x87 测试 8 字节负载。P5奔腾 https://en.wikipedia.org/wiki/P5_%28microarchitecture%29然后保证对齐的 8 字节加载/存储是原子的,但是P6 https://en.wikipedia.org/wiki/P6_(microarchitecture)更新的保证只要不跨越缓存行边界,缓存的 8 字节加载/存储都是原子的。与 AMD 不同的是,即使在可缓存内存中,8 字节边界对于原子性保证也很重要。为什么自然对齐变量的整数赋值在 x86 上是原子的? https://stackoverflow.com/questions/36624881/why-is-integer-assignment-on-a-naturally-aligned-variable-atomic-on-x86)
去看看阿格纳·雾 http://agner.org/optimize/的内容,以了解更多有关未对齐负载如何变慢的信息,并编写测试来练习这些情况。事实上,Agner 可能不是最好的资源,因为他的微体系结构指南主要侧重于通过管道获得微指令。只是简单提及缓存行分割的成本,没有深入讨论吞吐量与延迟。
也可以看看:缓存线分割,取两个 http://web.archive.org/web/20120417184641/http://x264dev.multimedia.cx/archives/96,来自 Dark Shikari 的博客(x264 首席开发人员),谈论 Core2 上的未对齐加载策略:检查对齐情况并为块使用不同的策略是值得的。
脚注1如今,64B 缓存行是一个安全的假设。 Pentium 3 及更早版本有 32B 线。 P4 有 64B 行,但它们经常以 128B 对齐的方式传输。 http://www.osronline.com/article.cfm?article=273我想我记得读过 P4 实际上在 L2 或 L3 中有 128B 行,但也许这只是成对传输的 64B 行的扭曲。7-CPU 明确表示 P4 130nm 的两级缓存都有 64B 行 http://www.7-cpu.com/cpu/P4-130.html.
现代 Intel CPU 具有相邻行 L2“空间”预取,同样倾向于拉入 128 字节对齐对的另一半,这在某些情况下可能会增加错误共享。x86-64 的缓存填充大小应该为 128 字节吗? https://stackoverflow.com/questions/72126606/should-the-cache-padding-size-of-x86-64-be-128-bytes展示了一个实验来证明这一点。
也可以看看uarch-长凳 https://github.com/travisdowns/uarch-bench结果。显然有人已经编写了一个测试程序来检查相对于缓存行边界的每个可能的未对齐情况。
我在 Skylake 桌面 (i7-6700k) 上的测试
寻址模式会影响加载使用延迟,正如英特尔在其优化手册中所描述的那样。我用整数测试mov rax, [rax+...]
, 与movzx/sx
(在这种情况下,使用加载的值作为索引,因为它太窄而不能成为指针)。
;;; Linux x86-64 NASM/YASM source. Assemble into a static binary
;; public domain, originally written by [email protected] /cdn-cgi/l/email-protection.
;; Share and enjoy. If it breaks, you get to keep both pieces.
;;; This kind of grew while I was testing and thinking of things to test
;;; I left in some of the comments, but took out most of them and summarized the results outside this code block
;;; When I thought of something new to test, I'd edit, save, and up-arrow my assemble-and-run shell command
;;; Then edit the result into a comment in the source.
section .bss
ALIGN 2 * 1<<20 ; 2MB = 4096*512. Uses hugepages in .bss but not in .data. I checked in /proc/<pid>/smaps
buf: resb 16 * 1<<20
section .text
global _start
_start:
mov esi, 128
; mov edx, 64*123 + 8
; mov edx, 64*123 + 0
; mov edx, 64*64 + 0
xor edx,edx
;; RAX points into buf, 16B into the last 4k page of a 2M hugepage
mov eax, buf + (2<<20)*0 + 4096*511 + 64*0 + 16
mov ecx, 25000000
%define ADDR(x) x ; SKL: 4c
;%define ADDR(x) x + rdx ; SKL: 5c
;%define ADDR(x) 128+60 + x + rdx*2 ; SKL: 11c cache-line split
;%define ADDR(x) x-8 ; SKL: 5c
;%define ADDR(x) x-7 ; SKL: 12c for 4k-split (even if it's in the middle of a hugepage)
; ... many more things and a block of other result-recording comments taken out
%define dst rax
mov [ADDR(rax)], dst
align 32
.loop:
mov dst, [ADDR(rax)]
mov dst, [ADDR(rax)]
mov dst, [ADDR(rax)]
mov dst, [ADDR(rax)]
dec ecx
jnz .loop
xor edi,edi
mov eax,231
syscall
然后运行
asm-link load-use-latency.asm && disas load-use-latency &&
perf stat -etask-clock,cycles,L1-dcache-loads,instructions,branches -r4 ./load-use-latency
+ yasm -felf64 -Worphan-labels -gdwarf2 load-use-latency.asm
+ ld -o load-use-latency load-use-latency.o
(disassembly output so my terminal history has the asm with the perf results)
Performance counter stats for './load-use-latency' (4 runs):
91.422838 task-clock:u (msec) # 0.990 CPUs utilized ( +- 0.09% )
400,105,802 cycles:u # 4.376 GHz ( +- 0.00% )
100,000,013 L1-dcache-loads:u # 1093.819 M/sec ( +- 0.00% )
150,000,039 instructions:u # 0.37 insn per cycle ( +- 0.00% )
25,000,031 branches:u # 273.455 M/sec ( +- 0.00% )
0.092365514 seconds time elapsed ( +- 0.52% )
在这种情况下,我正在测试mov rax, [rax]
,自然对齐,因此周期 = 4*L1-dcache-loads。 4c 延迟。我没有禁用涡轮增压或类似的东西。由于内核没有发生任何事情,因此内核时钟周期是最好的测量方法。
-
[base + 0..2047]
:4c 加载使用延迟、11c 缓存行分割、11c 4k 页分割(即使在同一大页内)。看当基址+偏移量与基址位于不同页面时是否会受到惩罚? https://stackoverflow.com/questions/52351397/is-there-a-penalty-when-baseoffset-is-in-a-different-page-than-the-base欲了解更多详细信息:如果base+disp
结果位于不同的页面base
,必须重放加载 uop。
- 任何其他寻址模式:5c 延迟、11c 缓存行分割、12c 4k 分割(甚至在大页内)。这包括
[rax - 16]
。造成差异的不是 disp8 与 disp32。
因此:大页面无助于避免页面分割惩罚(至少当 TLB 中的两个页面都很热时)。高速缓存行分割使寻址模式变得无关紧要,但“快速”寻址模式对于正常加载和页分割加载的延迟要低 1c。
4k 分割处理比以前好得多,请参阅 @harold 的数据,其中 Haswell 的 4k 分割延迟约为 32c。 (较旧的 CPU 可能比这更糟糕。我认为 SKL 之前应该有约 100 个周期的惩罚。)
吞吐量(无论寻址模式如何),通过使用除rax
所以负载是独立的:
- 无分裂:0.5c。
- CL 分裂:1c。
- 4k 分割:~3.8 至 3.9c(much比 Skylake 之前的 CPU 更好)
相同的吞吐量/延迟movzx/movsx
(包括 WORD 分割),正如预期的那样,因为它们是在加载端口中处理的(与某些 AMD CPU 不同,其中还有 ALU uop)。
依赖于缓存行分割加载的 Uop 从 RS(保留站)重播。计数器uops_dispatched_port.port_2
+ port_3
= 2x 数量mov rdi, [rdi]
,在另一个测试中使用基本相同的循环。 (这是一种依赖负载的情况,不受吞吐量限制。)在 AGU 生成线性地址之前,CPU 无法检测到拆分负载。
我之前认为拆分加载本身会重播,但这是基于指针追逐测试,其中每个加载都依赖于先前的加载。如果我们放一个imul rdi, rdi, 1
在循环中,我们会得到额外的端口 1 ALU 计数,因为它会被重播,而不是负载。
分割加载只需分派一次,但我不确定它后来是否借用同一加载端口中的一个周期来访问另一个缓存行(并将其与保存在该加载端口内的分割寄存器中的第一部分结合起来。 ) 或者启动另一条线路的需求负载(如果 L1d 中不存在该线路)。
无论细节如何,即使避免负载重播,缓存行拆分负载的吞吐量也会低于非拆分负载。 (无论如何,我们没有测试指针追逐。)
也可以看看IvyBridge 上指针追逐循环中附近的依赖存储对性能产生奇怪的影响。添加额外的负载会加快速度吗? https://stackoverflow.com/questions/54084992/weird-performance-effects-from-nearby-dependent-stores-in-a-pointer-chasing-loop有关 uop 重播的更多信息。 (但请注意,这是针对 uops依赖于负载,而不是负载 uop 本身。在该问答中,相关的微指令也主要是负载。)
缓存未命中加载不会itself需要重播以在准备好时“接受”传入数据,仅依赖于 uops。查看聊天讨论加载操作在调度、完成或其他时间时是否从 RS 中释放? https://chat.stackoverflow.com/rooms/206639/discussion-on-question-by-beeonrope-are-load-ops-deallocated-from-the-rs-when-th. This https://godbolt.org/z/HJF3BN https://godbolt.org/z/HJF3BNi7-6700k 上的 NASM 测试用例显示,无论 L1d 命中还是 L3 命中,所调度的负载 uops 数量都是相同的。但调度的 ALU uops 数量(不包括循环开销)从每个负载 1 个增加到每个负载约 8.75 个。当加载数据可能从 L2 缓存到达时,调度程序会积极地安排消耗数据的微指令在周期中分派(然后看起来非常积极),而不是等待一个额外的周期来查看它是否到达。
我们还没有测试当有其他独立但较年轻的工作可以在输入肯定已准备好的同一端口上完成时,重播有多积极。
SKL有两个硬件page-walk单元,这可能与4k split性能的大幅提升有关。即使没有 TLB 未命中,较旧的 CPU 也必须考虑到可能存在的情况。
有趣的是,4k 分割吞吐量是非整数。我认为我的测量有足够的精度和可重复性来说明这一点。请记住这是与everyload 是 4k 分割,并且没有其他工作正在进行(除了在一个小的 dec/jnz 循环内)。如果您在实际代码中遇到过这种情况,那么您就做错了。
我对为什么它可能是非整数没有任何可靠的猜测,但显然对于 4k 分割,微架构上必须发生很多事情。它仍然是缓存行分割,并且必须检查 TLB 两次。