大数跨境
0
0

内存基础知识(十九)-- 奇怪的流量分布:在BankGroup层观测流量

内存基础知识(十九)-- 奇怪的流量分布:在BankGroup层观测流量 IT知识刺客
2025-11-21
1
导读:这是内存基础知识系列的第19篇,你可以在 基础软件开发 ,找到前18篇。
这是内存基础知识系列的第19篇,你可以在 基础软件开发 ,找到前18篇。
上一篇内存基础知识(十八)-- 虚拟地址/物理地址/内存地址, 有何不同 中,提到可以通过观测流量,破译“神秘的”编址规则。这一篇我们就实际动手试试。
再额外唠叨几句,我发现很多人不喜欢做实验。在探索底层原理的时候,没有什么比做实验更直接的方式了。
有了实验数据,可以避免很多无谓的争论。
最重要的,实验过程还能把自身所学全都串起来。用“大厂味”话说,这叫形成“闭环”。
下面,我首先以测试程序为抓手,通过逐步拆解测试程序,再进行实验、收集数据,最后分析数据、与内存原理印证,最终咱们一起“闭环”。
闭环之后,更进一步,和算法相结合,把底层硬件的潜力充分发挥,提供更强的算力,这叫“赋能"。
好了,赋能的事先放一放,先从抓手开始:解析内存流量测试程序。
首先,既然是测试内存流量,当然要尽量避免L1~L3 Cache的影响了。这很容易做到:
    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篇中,这里不多说了。

下面,就要查看CPU手册了,得到MC中BG层流量计数器的编号。
我还以Intel至强CPU为例,MC中BankGroup流量如图1所示:
图1 可在内存基础知识(十一)中找到 图1 URL
图1中是 RANK0 中 BG 0~3的流量计数器,event保持不变,都是0xB0,umask分别是0x11~0x14。
单是从这些计数器的文档中,就能看出不少信息了。比如这里umask取值范围在0x11~0x14间,说明针对BG的流量计数器有4个。那么,这款CPU 内存控制器所支持的Chip中,BG数量最多4个。
你要是插上条每Chip 8个BG的内存条,是点不亮这台服务器的。
图1是RANK 0中所有BG层的流量,下面再看看 RANK1 的BG层流量计数器:
图2
umask为0x11~0x14,和RANK 0相同,event为0xb1。
我不再截图了,每RANK中BG层流量计数器都相似,依次类推,RANK2的event为0xb2,RANK3的event为0xb3,等等。
RANK编号到7,RANK 0~7,也就是说,MC(内存控制器)中预留的RANK数量最多8个。
好,计数器编号都找出来了,光说不练假把事,开始闭环吧。
我的意思是,开始测试:
[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============================0 Performance counter stats for 'CPU(s) 0':            30,361      LLC-load-misses                                                         10,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的基础知识马上就要用到了);

不能说的秘密--内存竟然有隐藏带宽
内存基础知识(二)
内存基础知识(三)-- BANK
DDR5核心频率只有200MHz(兆赫兹):真的假的
内存基础知识(五)-- BankGroup前传
内存基础知识(六)-- BankGroup登场
内存基础知识(七)-- BankGroup正传
内存基础知识(八)-- 如何统计内存流量
内存基础知识(九)-- 内存通道(Channel)详解
内存基础知识(十)-- 内存控制器是怎么完成读操作的
内存基础知识(十一)-- 查询内存通道编号
内存基础知识(十二)-- 如何得到精确的流量数据
内存基础知识(十三)-- 在RANK层精细统计内存流量
内存基础知识(十四)-- 让内存条中的“页”现出原形

内存基础知识(十五)-- 使用perf分析内存流量瓶颈

内存基础知识(十六)-- Rank/Chip/BG/Bank,软硬结合彻底搞懂内存底层结构

内存基础知识(十七)-- 什么是内存地址

内存基础知识(十八)-- 虚拟地址/物理地址/内存地址, 有何不同

另外,我之前还写过一个 NUMA 系列,NUMA相关的基础知识,马上也要用到了:

NUMA的逆行人生:一文讲清什么是NUMA

NUMA的逆行人生:一文讲清什么是NUMA(第二弹)

NUMA的逆行人生:一文讲清什么是NUMA(第三弹)

NUMA的逆行人生:一文讲清什么是NUMA(第四弹)

NUMA的逆行人生:一文讲清什么是NUMA(第五弹)

NUMA的逆行人生:一文讲清什么是NUMA(大结局)

还有这个系列,讲CPU执行阶段流水线的,虽然和内存没有直接关系,但个人感觉相当不错,值得阅读:

跟着苍老师学体系结构:深入突破基础软件开发(一)

深入突破基础软件开发(二)

深入突破基础软件开发(三)

深入突破基础软件开发(大结局)

【声明】内容源于网络
0
0
IT知识刺客
基础软件开发 HPC(高性能计算) HPC数据库研发 数据库内核 向量计算 计算机体系结构 数据库 DBA CPU原理
内容 83
粉丝 0
IT知识刺客 基础软件开发 HPC(高性能计算) HPC数据库研发 数据库内核 向量计算 计算机体系结构 数据库 DBA CPU原理
总阅读26
粉丝0
内容83