for(j = 0; j < loop_number_1; j++){for(i = 0, arr1 = arr; i < loop_number_2; i++){k += *arr1;arr1 = (TYPE *)(((char *)arr1) + step_size); // 地址向后加至少 64字节}}
内存流量测试程序片段1
第 6 行的地址后移操作,至少移64字节就可以避免Cache的影响。
因为Cache Line Size通常都是64字节。
Cache Line Size是 Cache 层概念。内存系列完结后,我计划就开讲Cache系列,这里先提一下,Cache也不是一个字节一个字节读写的,也要按“块”读写。Cache的块叫Line,Line的大小通常都是64字节。
注:我接触过的,只有飞腾 CPU 的Cache Line Size是128字节。
你读任意一字节内存,CPU会把它所在的64字节,读入各级Cache。如果下一地址和前一地址不超过64字节,就有可能Cache Hit。
因此,第6行中的step_size,应该是64字节的倍数。
(注:不是从你读的内存地址开始的64字节,而是它所属的某一个Line。)
“测试程序片段1“中第3到7行的内循环,如图1所示:
图1 step_size 为 64
就是以step_size为步幅,跳着遍历一块内存。
如果step_size是64字节的倍数,第一次执行内循环,是不会Cache Hit的,所有的读请求,都会到达内存层。但第二次执行内循环可就不一定了。
为了解决这个问题,在内循环中加一个Cache清理:
for(j = 0; j < loop_number_1; j++){for(i = 0, arr1 = arr; i < loop_number_2; i++){k += *arr1;__asm__ __volatile__("clflush (%0)\n\t""lfence\n\t": :"r"(arr1) :);arr1 = (TYPE *)(((char *)arr1) + step_size); // 地址向后加至少 64字节}}
测试程序片段2 -- 增加Cache清理
第6~11行,就是Cache的刷新指令:clflush,它会修改Cache Line头的标志,将Line改为Invidate(无效)状态。
如果Line是个Dirty的脏Line,也就是被修改过,clflush还会触发“写内存”操作,先写内存,再标记为无效Line。
如果你是其他x64平台CPU,如AMD,要查阅一下手册,找找AMD对应的指令是什么了。clflush是Intel平台的指令。
第9行的lfence指令,是为了“挡住”后续指令。因为CPU是乱序执行的,这里的lfence可以保证clflush先执行。这一块放在Cache系列中再详细讨论吧。
好了,测试程序就到这儿了。简单吧,后面我再把完整的代码粘上来。
还有,在开始测试前,一定要按照 内存基础知识(十二)-- 如何得到精确的流量数据 中的步骤,关闭指定Core的内存预取。详细的参阅第12篇中,这里不多说了。
[root@rdma101 ff]# perf stat -C 0 -e LLC-load-misses,'uncore_imc_0/event=0xb0,umask=0x11/','uncore_imc_0/event=0xb0,umask=0x12/','uncore_imc_0/event=0xb0,umask=0x13/','uncore_imc_0/event=0xb0,umask=0x14/' ./mr3_2 0 0 64 10000 3Old CPU: 42==========Parent PID is 1003214==========PID is 1003215-----------SUB Process at CPU: 0----------TSC: 8576744 0============================0Performance counter stats for 'CPU(s) 0':30,361 LLC-load-misses10,138 uncore_imc_0/event=0xb0,umask=0x11/20,133 uncore_imc_0/event=0xb0,umask=0x12/358 uncore_imc_0/event=0xb0,umask=0x13/373 uncore_imc_0/event=0xb0,umask=0x14/0.006184890 seconds time elapsed[root@rdma101 ff]#
测试结果-1
先说一下 perf 命令的选项:
“-C 0”,只统计0号Core中的结果,因为我在测试程序中使用了CPU亲和性函数 sched_setaffinity(),将测试程序固定在 0号 Core中运行,所以这里使用-C 0,只打开Core 0中计数器。
少统计一些Core,可以少一些杂质数据。
为了使用测试数据更准确,我使用MSR寄存,关掉了0号Core的“预取”。在内存基础知识(十二)-- 如何得到精确的流量数据中,有关于这一块的详细说明。
正是因为我只关掉了某个Core的预取,我才要限定测试程序运行时的CPU。
在-C 0后面,就是我打开的计数器了。其中 LLC-load-misses 是CPU中计数器,LLC是Last Level Cache,最后一级Cache,也就是L3 Cache了。这个计数器是统计L3 Miss次数的。
只有L3 Miss了,内存请求才会到达内存层。如果这个计数器数值太低,说明测试程序有问题,内存请求都Cache Hit了,没走到内存层。
再后面是4个内存控制器(MC)中的计数器,它们是这一篇的主角了,对照图1、图2中的手册,可知:
'uncore_imc_0/event=0xb0,umask=0x11/':通道 0 RANK 0 BG 0
'uncore_imc_0/event=0xb0,umask=0x12/':通道 0 RANK 0 BG 1
'uncore_imc_0/event=0xb0,umask=0x13/':通道 0 RANK 0 BG 2
'uncore_imc_0/event=0xb0,umask=0x14/':通道 0 RANK 0 BG 3
“测试结果-1”中的 perf 命令,是打开0号通道、0号RANK中BG 0~3的流量计数器,统计RANK 0中所有BG的流量。
perf 命令解释完了,再来看测试程序的参数吧:
./mr3_2 0 0 64 10000 3
内存流量测试程序
第一个0,是在 0 号 Core 中分配内存。
第二个0,在 Core 0中运行程序。
可以调用CPU亲合性函数,指定在那里分配内存、执行进程。这一块下一篇我们再详述。
第三个参数,64,是step_size,64字节。
第4个参数,10000,是外循环次数,也就是“测试程序片段2 -- 增加Cache清理”中的loop_number_1。
第5个参数,3,内循环次数,它是loop_number_2。
简单来说,"内存流量测试程序“就是循环一万次,反复读如下三个字:
图2
通过统计流量,可以清楚的看到如图3的结果:
图3
偏移0处的字,以即它所在的64字节的Line,也就是程序内存块中的第一个64字节,来自于BG 1。
程序内存块中的第二个64字节,偏移64的Line,来自于BG 0。
第三个Line,并没有使用BG3或BG4,而是回到了BG 1。
限于篇幅,今天就到这里吧。这会带来一个奇怪的问题,下一篇中我们将详细描述,这里先提一下。
在内存基础知识(十四)中,我们观察到RANK层的页大为8192字节,之后的几篇中,我们详细分析了8192B这个数字的来历。
它来自于内存最底层结构:Memory Array(Sub Array)。Memory Array中 Row Buffer大小为1024bit,结合一个RANK中BANK数、一个BANK中Memory Array数,计算出RANK层一个页大小为8192字节。
但是,上面的测试说明,内存流量并非来自RANK中的一组BANK,而是来自两组。理解这一点,是后续赋能的基础。更进一步的解读,放到下一篇吧。
最后附上内存系列前18篇,包括NUMA系列(NUMA的基础知识马上就要用到了);
内存基础知识(十六)-- Rank/Chip/BG/Bank,软硬结合彻底搞懂内存底层结构
内存基础知识(十八)-- 虚拟地址/物理地址/内存地址, 有何不同
另外,我之前还写过一个 NUMA 系列,NUMA相关的基础知识,马上也要用到了:
还有这个系列,讲CPU执行阶段流水线的,虽然和内存没有直接关系,但个人感觉相当不错,值得阅读:

