CLFLUSH如何处理尚未在缓存中的地址?

18

我们尝试使用Intel CLFLUSH指令在Linux用户空间刷新进程的缓存内容。

我们创建了一个非常简单的C程序,首先访问一个大数组,然后调用CLFLUSH来刷新整个数组的虚拟地址空间。我们测量CLFLUSH刷新整个数组所需的延迟时间。程序中数组的大小是一个输入参数,我们将输入从1MB变化到40MB,步长为2MB。

我们理解CLFLUSH应该会刷新缓存中的内容。因此,我们期望看到刷出整个数组的延迟随着数组大小的增加呈线性增长,而在数组大小大于20MB(我们程序的LLC大小)后,延迟应该停止增加。

但是,实验结果非常出人意料,如图所示。当数组大小大于20MB时,延迟不会停止增加。

我们想知道如果地址不在缓存中,CLFLUSH是否可能在刷新地址之前将其带入缓存?我们还尝试在Intel软件开发手册中搜索,没有找到任何关于CLFLUSH如何处理不在缓存中的地址的说明。

enter image description here

下面是我们用来绘制图表的数据。第一列是数组的大小(KB),第二列是刷出整个数组所需的延迟时间(秒)。

任何建议/意见都将不胜感激。

[修改]

以前的代码是不必要的。虽然性能类似,但CLFLUSH可以在用户空间更轻松地完成。因此,我删除了混乱的代码以避免混淆。

SCENARIO=Read Only
1024,.00158601000000000000
3072,.00299244000000000000
5120,.00464945000000000000
7168,.00630479000000000000
9216,.00796194000000000000
11264,.00961576000000000000
13312,.01126760000000000000
15360,.01300500000000000000
17408,.01480760000000000000
19456,.01696180000000000000
21504,.01968410000000000000
23552,.02300760000000000000
25600,.02634970000000000000
27648,.02990350000000000000
29696,.03403090000000000000
31744,.03749210000000000000
33792,.04092470000000000000
35840,.04438390000000000000
37888,.04780050000000000000
39936,.05163220000000000000

SCENARIO=Read and Write
1024,.00200558000000000000
3072,.00488687000000000000
5120,.00775943000000000000
7168,.01064760000000000000
9216,.01352920000000000000
11264,.01641430000000000000
13312,.01929260000000000000
15360,.02217750000000000000
17408,.02516330000000000000
19456,.02837180000000000000
21504,.03183180000000000000
23552,.03509240000000000000
25600,.03845220000000000000
27648,.04178440000000000000
29696,.04519920000000000000
31744,.04858340000000000000
33792,.05197220000000000000
35840,.05526950000000000000
37888,.05865630000000000000
39936,.06202170000000000000

1
不幸的是,Agner Fog在他的指令表中没有测试clflush。可能即使实际上没有任何操作,它也会在uops方面产生显着的成本或有限的吞吐量。您应该查看perf计数器(使用perf)。ocperf.py是一个很好的perf包装器,它为uop计数器添加了符号名称。 - Peter Cordes
@PeterCordes,然而,为什么当没有任务时延迟会增加呢?我通过编辑问题发布了代码,希望它能显示一些内部问题? - Mike
1
我看到Linux内核中的clflush_cache_range已经针对Skylake进行了优化,并且在clflush循环之前/之后包含了内存屏障,因为它使用了一个函数,如果CPU支持clflushopt,则将其热补丁到clflushopt。内存屏障不是免费的,也许你看到的一些成本就是来自这里?我猜你在用户空间也得到了类似的结果。如果是这样,内存屏障的成本就无法解释了,因为你在用户空间版本中没有使用MFENCE - Peter Cordes
我可能只是在用户空间测试运行clflush。这样更容易进行分析,并且可以轻松地对整个数组进行操作。如果我们不试图刷新进程的代码,它会消除一整层复杂性。我不确定TLB在系统调用上下文中的工作原理。内核是否使用与用户空间相同的页表来访问用户内存?我的猜测是“是”,但如果不是,在内核中执行将产生额外的TLB缺失。 - Peter Cordes
1
@我不存在我不曾存在哇,你怎么能记得我两年前问的问题!太神奇了! - Mike
显示剩余9条评论
2个回答

10
你想查看Skylake的新优化指南,Intel推出了另一个版本的clflush,称为clflush_opt,它是弱有序的,在你的情况下性能会更好。请参见此处的第7.5.7节 - http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf 一般而言,CLFLUSHOPT的吞吐量比CLFLUSH高,因为如上所述和第7.5.6节中描述的那样,CLFLUSHOPT会按照较小的一组内存流量进行排序。CLFLUSHOPT的吞吐量也会有所变化。使用CLFLUSHOPT时,刷新已修改的缓存行将比刷新非修改状态的缓存行成本更高。与CLFLUSH相比,CLFLUSHOPT对于任何一致性状态的缓存行都提供了性能优势。与CLFLUSH相比,CLFLUSHOPT更适合刷新大缓冲区(例如大于几KB)。在单线程应用程序中,使用CLFLUSHOPT刷新缓冲区可能比使用Skylake微架构的CLFLUSH快9倍。该部分还解释了刷新已修改数据较慢的原因,这显然来自写回惩罚。
关于延迟的增加,你是否在测量整个地址范围和每行clflush所需的总时间?在这种情况下,即使它超过了LLC大小,你也会线性依赖于数组大小。即使行不存在,clflush也必须由执行引擎和内存单元处理,并查找每行的整个缓存层次结构,即使该行不存在。

我同意clflush将通过执行引擎和内存管理单元进行操作,然而,如果我们看一下图中的只读行,在数组超出LLC大小边界时,延迟增加的速度比数组较小时的延迟更快。这意味着clflush需要更多时间来"刷新"不在缓存中的地址?这对我来说相当令人惊讶... - Mike
1
你用的是什么CPU?这可能是跨插槽/NUMA效应吗?另外,请发布代码(或至少一个简单版本)。 - Leeor
@MikeXu:可能是TLB缺失了吗?不太可能,因为你可能从malloc中得到了anon hugepages。在缓存可以告诉地址是否被缓存之前,它仍然必须将虚拟地址转换为物理地址。就像我在问题上评论的那样,请检查perf计数器。您按照编写数组的顺序进行clflush,还是按相反的顺序进行?按相反的顺序,前面的约20MiB仍会命中缓存。 - Peter Cordes
1
@Leeor,我正在使用Intel(R) Xeon(R) CPU E5-2618L v3 @ 2.30GHz运行;这台机器具有NUMA架构。它有两个NUMA节点。但我想知道如何/哪些跨插座/NUMA效应可能会导致这种行为?我现在将代码的简化版本添加到问题部分。 - Mike
@PeterCordes,我们可能没有按照写入数组的顺序刷新缓存。我们是以随机顺序编写数组的,但是我们按照任务结构体中vma的线性地址递增的顺序刷新任务的缓存。至于TLB缺失,我发现Haswell处理器(我的进程所用)有1K个L2 TLB条目,可以覆盖1K * 4KB(页面大小)= 4MB。因此,如果是TLB缺失,应该在4MB数组大小处看到延迟斜率隆起而不是20MB数组大小。我说得对吗? - Mike

7

这并没有解释只读图中的膝盖,但是解释了为什么它不会停滞。


我没能在本地测试中看到热缓存和冷缓存情况之间的差异,但我找到了一个关于clflush性能的数字:

这个AIDA64指令延迟/吞吐量基准库将单插槽Haswell-E CPU (i7-5820K)clflush吞吐量列为每~99.08周期一次。它没有说明这是针对重复相同地址还是其他操作的。

所以即使不需要执行任何工作,clflush也不是完全免费的。它仍然是一个微代码指令,因为通常不是CPU工作负载的重要部分而没有进行大量优化。

Skylake正准备改变这种情况,支持连接到内存控制器的持久内存:在Skylake (i5-6400T)上,测得吞吐量为:

  • clflush:每~66.42个周期一次
  • clflushopt:每~56.33个周期一次

也许当一些行实际上是需要刷新的脏缓存时,clflushopt更有优势,也许当L3由其他核心执行相同操作时。或者他们只是想让软件尽快使用弱序版,然后再对吞吐量进行更大的改进。这种情况下它快了约15%,这还不错。


我从问题中的数据确认,Xeon 2618L v3上RW的clflush需要91ns来刷新缓存行,这与您的数据一致。我猜测上面链接中提供的insn延迟也是基于R和W请求混合测量clflush延迟。我想你是对的!clflush可能需要比我们想象中更多的工作来刷新缓存行.. :-( - Mike
@MikeXu:这些是吞吐量,不是延迟。要测量延迟,也许可以从clflush后的缓存行加载?另一个你可以测量clflush的东西,那个基准没有测量,就是它对周围代码的影响有多大。即每100个“add”指令执行一次“clflush”是否会降低“add”的吞吐量?或者是加载/存储而不是添加。这可能主要取决于“clfush”需要多少uop。它可能是相当多的。大多数慢操作都是多uop的。几乎只有“divps” /“sqrtps”是单uop但不完全流水线化的。 - Peter Cordes
2
显然,只要刷新区域的大小相当小,cflushcflushopt的成本就可以很低(例如每行几个周期或两个周期)。请参见此答案中的图表。因此,行为真的非常奇怪 - 在几K之后,成本会飙升。您的测试和其他测试发现大于50个周期,可能使用了这些更大的缓冲区,或者存在某些其他差异,例如高速缓存行不存在于某个层次结构中。 - BeeOnRope

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接