You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

二维矩阵遍历顺序调整后perf统计cache-references升高原因

缓存感知编程实验中cache-references计数异常问题

实验背景

我偶然发现了一个介绍缓存感知编程的GitHub仓库,它关联了一篇同主题的博客文章。文章对比了数组内数据分布与缓存行大小的不同匹配关系下,perf输出中cache-references和cache-misses的数量差异。我希望自行完成类似实验,编写的初始行优先遍历代码如下:

// depth_first.c
#include <stdint.h>
#include <stdlib.h>
#define ROWS 100000
#define COLS 64

int main() {
    uint8_t (*mat)[COLS] = malloc(sizeof(uint8_t[ROWS][COLS])); // 分配二维矩阵

    uint8_t sum = 0;

    for(int row_idx = 0; row_idx < ROWS; row_idx++) {
        for(int col_idx = 0; col_idx < COLS; col_idx++) {
            sum += mat[row_idx][col_idx];
        }
    }

    free(mat);
}

注:我清楚malloc后未初始化数组属于未定义行为,但我不关心实际运算取值,且判断该操作不会影响缓存性能测试结果。

我动态分配了uint8_t类型的二维数组mat,维度为100000行、单行列宽64字节,数组内存布局如下:

[ Row 0, 64 bytes ][ Row 1, 64 bytes ][ Row 2, 64 bytes ]...[ Row 99999, 64 bytes ]

每一行占用连续内存空间,我特意选择64字节作为列宽——该值与我所用CPU的缓存行大小一致,单行数据可以完整放入单个缓存行。

实验设计

我准备了两个版本的遍历逻辑:

  • 深度优先(行优先)遍历:访问完第一行的所有列元素后再移动到第二行,即上述初始代码的逻辑
  • 广度优先(列优先)遍历:先遍历所有行的第一个元素,再遍历所有行的第二个元素,以此类推,对应循环代码如下:
// breadth_first.c 循环部分
for(int col_idx = 0; col_idx < COLS; col_idx++) { // 列索引作为外层循环
    for(int row_idx = 0; row_idx < ROWS; row_idx++) { // 行索引作为内层循环
        sum += mat[row_idx][col_idx];
    }
}

我使用无优化选项编译两个版本的代码:

gcc -O0 breadth_first.c -o breadth_first
gcc -O0 depth_first.c -o depth_first

之后使用perf工具开展测试:

perf stat -e cache-references,cache-misses ./breadth_first
perf stat -e cache-references,cache-misses ./depth_first

得到的测试输出如下(多次运行的数值和占比仅有小幅波动):

Performance counter stats for './breadth_first':

        12 654 452      cache-references:u                                          
           106 456      cache-misses:u            #    0,841 % of all cache refs    

       0,015068004 seconds time elapsed

       0,015102000 seconds user
       0,000000000 seconds sys



 Performance counter stats for './depth_first':

           213 178      cache-references:u                                          
             5 901      cache-misses:u            #    2,768 % of all cache refs    

       0,026617312 seconds time elapsed

       0,026690000 seconds user
       0,000000000 seconds sys

预期与实际结果偏差

我原本预期两个版本的cache-references数量相近,列优先版本的cache-misses数量、占比更高。做出该预期的依据是两段代码逻辑等价,对二维数组的总内存访问次数完全一致。

但实际测试结果中,列优先版本的cache-references数量出现了大幅增长,尽管cache-misses绝对数量也有所上升,但列优先版本的缓存未命中占比反而更低(我推测大概率是因为总引用数基数过大,单独的占比数据不具备参考价值)。

环境补充

测试所用CPU为AMD品牌,我已经在内核源码中找到了perf事件ID到硬件事件ID的对应规则,目前正在查阅AMD官方处理器编程参考文档的对应章节,确认底层硬件事件的具体定义。


问题原因解答

核心原因是对perf通用事件cache-references的计数逻辑存在认知偏差,结合AMD处理器的缓存层级设计,具体可以拆为三点:

  • cache-references不是总内存/缓存访问计数
    这个通用perf事件没有跨架构的统一定义,在AMD x86平台上,它映射的是L2缓存的接入请求数,既不统计L1缓存命中的访问,也不覆盖所有内存层级的操作。之前假设它统计所有缓存访问,是推导错误的根源。对应的cache-misses事件,在AMD平台上统计的是L2缓存未命中、需要向L3/内存发起请求的次数,也不是全局的缓存未命中总数。

  • 两种遍历模式下L1缓存的命中率差异巨大,直接决定L2请求数

    • 行优先遍历是连续地址访问:每访问1个字节,硬件预取器会直接把整段64字节缓存行拉到L1数据缓存,后续同缓存行的63次访问全部在L1命中,根本不需要向L2发请求,只有每次缓存行换入、预取的时候才会产生L2访问,因此统计到的cache-references数值极低。
    • 列优先遍历的访问步长为64字节:每次访问都跳转到下一行的同列位置,也就是每一次访问都属于不同的缓存行。AMD消费级处理器的L1数据缓存通常为32KB,仅能容纳512个64字节缓存行,完全装不下遍历过程中跳着访问的10万个缓存行,因此绝大多数访问都会在L1未命中,必须向L2发起读请求,这些请求全部被计入cache-references,直接导致该数值暴涨到千万级。
  • 低未命中率来自L2层的硬件预取
    列优先遍历的访问是固定64字节的规则步长,L2缓存的硬件流预取器很容易识别这个访问模式,提前把后续需要的缓存行拉到L2中,因此虽然发往L2的请求总数多,但绝大多数都能在L2命中,只有极少部分需要向下访问L3或内存,最终算出来的未命中占比反而更低——这个占比的分母是L2请求数,不是总内存访问数,自然和预期的结果不符。

另外补充两个实验设计的问题:

  • 用-O0编译的代码循环寄存器分配、地址计算的开销占比极高,最终统计的运行时间参考价值很低,看到列优先版本运行时间更短就是编译优化没开导致的偏差。做缓存相关实验至少要开-O2优化,同时要避免编译器把遍历循环直接优化掉,可以把sum声明为volatile,或者最终打印sum的值。
  • 未初始化的malloc内存默认映射到系统的零页写时复制区域,虽然不影响缓存计数逻辑,但部分平台的预取器对零页有特殊优化,最好还是给数组填个固定值再测试。

如果要统计真正的L1总访问数,不要用通用的cache-references事件,要查询所用AMD CPU型号的原生PMU事件,比如L1数据缓存访问事件,这类原生事件的计数两个版本应该基本一致,都在640万左右(100000行*64列的总访问次数)。


内容的提问来源于stack exchange,提问作者msaw328

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 03:51:19