缓存行访问延迟测量相关技术问题咨询
背景
为测量单缓存行访问延迟,定义了64字节的cache_line结构体(包含next索引字段和填充字段,匹配目标CPU的缓存行尺寸),并构建了随机排序的结构体数组。基于Chips and Cheese的代码实现测试程序后,在L1缓存32kB(对应512行)、L2缓存256kB(对应4096行)、L3缓存12MB(对应196608行)的6核CPU上得到测试结果:当缓存行完全适配L1或L2时访问时间恒定,但适配L3时访问时间递增。针对该场景的三个技术问题解答如下:
问题1:L3适配时访问时间递增是否因6核心共享拆分导致?
是的,这是典型的NUCA(非统一缓存架构)特性导致的现象。现代多核CPU的L3缓存普遍采用分片设计,整个L3被拆分为多个独立的缓存分片,每个分片对应1~2个物理核心。核心访问本地分片的L3缓存延迟更低,访问远程分片的延迟则会高出不少。
当测试数组刚好能放进L3但规模较大时,缓存行会被分散到不同的L3分片当中。循环访问时,会交替命中本地和远程分片,随着访问的缓存行数量增加,远程分片访问的占比逐渐上升,最终导致整体平均访问延迟递增。而L1、L2是每个核心独占的私有缓存,所有访问都在本地核心的缓存内完成,因此延迟保持恒定。
问题2:修改循环体读取padding[0]字段是否仅增加一次L1缓存访问开销?
不会。因为cache_line结构体本身就是一个完整的64字节缓存行,next字段和padding[0]字段属于同一个缓存行。当你通过next索引定位到目标结构体时,整个缓存行已经被加载到L1缓存中,读取padding[0]只是访问同一缓存行内的字节,不会额外触发L1缓存请求——缓存是以行为单位加载的,只要缓存行存在于L1中,访问行内任意数据都不会产生额外的缓存访问开销,仅会多消耗几个寄存器读取的时钟周期(远低于缓存访问延迟)。
问题3:如何避免编译器优化移除预读取的512个缓存行操作,以单独测量L2缓存访问时间?
可以采用以下几种可靠的方法阻止编译器优化未显式使用的内存读取:
- 使用volatile变量承接读取结果:定义一个
volatile uint64_t dummy;全局变量,每次读取缓存行数据后执行dummy += read_value;。编译器不会优化对volatile变量的操作,因此会保留所有读取步骤。 - 嵌入空汇编块标记数据被使用:比如在GCC/Clang下,读取数据后执行
asm volatile("" : : "r"(read_value));,告诉编译器该数据参与了汇编操作,不能被优化移除。 - 让读取结果参与最终输出:累计所有预读取的缓存行数据,最后将累计值打印到控制台(即使你不需要这个结果)。编译器为了生成正确的输出,会保留所有读取操作。
- 插入内存屏障指令:在x86架构下,可以使用
_mm_mfence()或_mm_lfence()内存屏障,阻止编译器重排或删除内存访问操作。注意这种方法会引入少量额外开销,需合理控制使用位置。
内容的提问来源于stack exchange,提问作者Franks

