内存及Cache带宽测试

内存带宽测试

常见的内存带宽测试有STREAM、babel-stream或者lmbench,可以学习它们是怎么写的。这里采用类似STREAM的方式(即通过四种kernel:copy, scale, add, triad来测试)。

多次测试(NTIMES=20),去掉前3次的值,取最小时间。计时函数采用clock_gettime获得纳秒级别的精度。

#include <time.h> // For struct timespec, clock_gettime, CLOCK_MONOTONIC
double wall_time()
{
    struct timespec t;
    clock_gettime(CLOCK_MONOTONIC, &t);
    return 1. * t.tv_sec + 1.e-9 * t.tv_nsec;
}

copy和scale的读写内存大小分别为2 * sizeof(DATA_TYPE) * ARRAY_SIZE字节,add和triad的为3 * sizeof(DATA_TYPE) * ARRAY_SIZE字节.
计算内存带宽。

for (itest = 0; itest < NTIMES; itest++) {
    times[itest][0] = wall_time();
    GPTLstart("copy");
    for (i = 0; i < ARRAY_SIZE; i++)
        c[i] = a[i];
    GPTLstop("copy");
    times[itest][0] = wall_time() - times[itest][0];

    times[itest][1] = wall_time();
    GPTLstart("scale");
    for (i = 0; i < ARRAY_SIZE; i++)
        b[i] = scalar * c[i];
    GPTLstop("scale");
    times[itest][1] = wall_time() - times[itest][1];

    times[itest][2] = wall_time();
    GPTLstart("add");
    for (i = 0; i < ARRAY_SIZE; i++)
        c[i] = a[i] + b[i];
    GPTLstop("add");
    times[itest][2] = wall_time() - times[itest][2];

    times[itest][3] = wall_time();
    GPTLstart("triad");
    for (i = 0; i < ARRAY_SIZE; i++)
        a[i] = b[i] + scalar * c[i];
    GPTLstop("triad");
    times[itest][3] = wall_time() - times[itest][3];
}

查看STREAM官方的说明,在做内存带宽测试时,需要注意所使用的每个数组的大小,至少要是CPU的L3 Cache的4倍。以保证每次访存没有被cache兜住,都要从内存中读。

另外在写的时候,发现如果四个kernel内的变量依赖不妥当设置的话(例如,每次都是从数组a中读,向数组c中写),则会得到快得飞起的时间和极大的内存带宽。这是不合理的,是因为开启-O3的编译选项时,编译器激进优化掉了中间所有的读写操作。所以最好设置成,每一个kernel的读,恰好是前一个kernel的写。

测试选取课题组的机器,单节点内有两块Intel Xeon Gold 6132 CPU。每块CPU有14个计算核心,L1D、L2和L3 cache分别为32KB、1024KB和19712KB(单CPU内14个核心共用L3)。根据网上公开的参数资料,该款CPU的最大理论内存带宽为:
在这里插入图片描述

根据L3 Cache的大小,设置数组大小为100000000,double的数据类型,每个数组占用762.9 MB。使用单核的测试结果如下:
在这里插入图片描述
用满节点内两个CPU的28个计算核心,将每个核的结果累加得到总的带宽:
在这里插入图片描述
为了确定测试的可靠性,利用GPTL和PAPI库在每次调用kernel的位置插装,统计PMU事件(PAPI_L1_DCMPAPI_L1_ICMPAPI_L2_TCMPAPI_L3_TCM分别对应L1D、L1I、L2、L3的缺失次数):
在这里插入图片描述
从事件统计结果来看,L1DCM≈L2_TCM=L3_TCM,可见几乎所有的访存在三级Cache系统里都是缺失的。

Cache带宽测试

为了一次性把三级Cache的带宽都测试出来,想到了之前看《Computer System:A Programmer’s Perspective》时看到的Memory Mount的测试方法,通过使用不同大小的工作集、不同长短的遍历步长,统计对应的读带宽情况,然后得到以(size, stride)二维自变量、带宽因变量的类似山脉的图。这样可一目了然整个Cache系统的读带宽情况。

前面所用的类似STREAM的测试,是读、写混合,四种kernel的设置也更符合实际应用场景。而此处的Memory Mount则是将读、写分开测,测试方法参考了Test-Driving Intel Xeon Phi这篇文章,写法也比较简单。

读带宽测试kernel:

void test_read(int elems, int stride) {
    // unroll
    int i;
    int stride2 = 2 * stride,
        stride3 = 3 * stride,
        stride4 = 4 * stride,
        stride5 = 5 * stride,
        stride6 = 6 * stride;
        // stride7 = 7 * stride;
        // stride8 = 8 * stride;
    DATA_TYPE acc0 = 0, acc1 = 0, acc2 = 0, acc3 = 0, acc4 = 0, acc5 = 0;//, acc6 = 0;//, acc7 = 0;
    volatile DATA_TYPE sink;
    int limit = elems - stride6;
    for (i = 0; i < limit; i += stride6) {
        acc0 += data[i];
        acc1 += data[i+stride];
        acc2 += data[i+stride2];
        acc3 += data[i+stride3];
        acc4 += data[i+stride4];
        acc5 += data[i+stride5];
        // acc6 += data[i+stride6];
        // acc7 += data[i+stride7];
    }
    for (; i < elems; i += stride)
        acc0 += data[i];
    sink = (acc0 + acc1) + (acc2 + acc3) + (acc4 + acc5);// + (acc6);// + acc7);
}

需要注意的是,同样为了避免编译器过度优化导致根本没有执行核心代码,将最后的读的结果赋给了一个volatile型变量。在不做循环展开时,发现测出来的带宽相当小,甚至写带宽还比读带宽高出将近一倍,因此将循环展开,每次在循环体内执行多条访存语句。一方面减少了循环指令数的执行开销;一方面也可能是类似这篇文章分析prefetch效果的原因时所说的,经典的tomasulo算法的流水线CPU,其取指部件按指令顺序将指令放入保留站(RS)中并设置依赖关系,当CPU在RS中看到了指令且相应功能部件还有空余时,那么就能执行该条指令。所以这里的循环展开相当于缩减了(彼此间无依赖的)访存指令的间隔,使CPU尽快地发起访存指令,更高效地利用部件。实验发现循环展开6次效果最好(检查了4,6,8三种),可能也对应于前述的该款CPU的内存控制器有6通道。

写带宽测试kernel的写法是类似的:

void test_write(int elems, int stride) {
    // unroll
    int i;
    int stride2 = 2 * stride,
        stride3 = 3 * stride,
        stride4 = 4 * stride;
        // stride5 = 5 * stride,
        // stride6 = 6 * stride;
    DATA_TYPE scalar = 3.0;
    int limit = elems - stride4;
    for (i = 0; i < limit; i += stride4) {
        data[i]         = scalar;
        data[i+stride ] = scalar;
        data[i+stride2] = scalar;
        data[i+stride3] = scalar;
        // data[i+stride4] = scalar;
        // data[i+stride5] = scalar;
    }
    for (; i < elems; i += stride)
        data[i] = scalar;
}

测试的工作集大小范围从最小的16KB(L1D Cache的一半大小),以2的幂次往上增加,直到最大的512MB(远大于L3 Cache 19.25MB)。每个工作集大小,均采用从1到12变化的步长进行测试。工作集越小,时间局部性越好;步长越小,空间局部性越好。

下图为的单核的Memory Mount图,左图为读带宽,右图为写带宽。
在这里插入图片描述
从图中可见,垂直于sizes轴的有三道山脊,分别对应于L1D cache,L2 cache和L3 cache的大小,它们将山划分为四个区域,分别对应工作集完全落在L1D cache,L2 cache,L3 cache和主存内所获得的带宽。在每个区域内,都是一个带宽逐渐下降的斜坡,代表随着步长的变化,空间局部性下降。沿着strides=1获得剖面图如下,更清晰地看出这个差异:
在这里插入图片描述
山脊的位置,即读带宽骤跌的位置,32KB,1024KB均符合CPU的实际cache大小。注意此时一颗CPU内仅有一个进程,其能使用所有的L3 cache(为19.25MB),故第三次的读带宽骤跌发生在工作集大小为16MB和32MB之间。因此可知在以1为步长读取时,在这个场景下,L1D cache的带宽为37.1GB/s,L2的带宽为29.3GB/s,L3的带宽为21.5GB/s,主存的带宽为10.7GB/s左右。主存的值与之前的读写混合的测试结果在一个量级上。在其它步长处做剖面图,也能得到类似趋势,但整个图的绝对值会降低一些。 那么有一个问题,谁才是各级Cache及主存的 (尽可能)真实的 读带宽呢?

确实在不同的计算场景下(如之前的四个读写混合的kernel),会得到不同的带宽值。个人猜测,如果希望每次访问都是从特定的目标处(e.g., L1D,L2或L3)取值,那么应该使每次访问在目标处之上的所有cache中都miss掉,而在目标处命中。例如,Cache系统的块大小为64字节(对应8个double),如果以步长为1进行读取,则每次miss之后,L1会从下面的cache中调上一个块,所以接下来有7次L1 hit,这对于测量L2的读带宽显然是有“欺骗性”的。因此理论上,应将步长设置至少为8时,配合上合适的工作集大小(完全放入L2 cache内),测得的才是真正的L2的带宽。实际上,步长大于8时,也并不是就在一定步长范围内的读带宽基本不变,这可能是不同级cache之间使用的块大小不一样所致。所以以下增大步长范围来观察。

事实上,之前的以strides=1得到的带宽剖面图中,可能含有由于硬件预取(自动识别顺序的、步长为1的引用模式)带来的“水分”。虽然它看起来没有CSAPP书中展示的Intel Core i7系统的在strides=1时那么平坦。

L1带宽

L1之上再无cache,因此取16KB的工作集大小即可。如下图所示,其读带宽随步长增加而一直降低,这点似乎无法解释。
在这里插入图片描述

L2带宽

对于L2,工作集大小为64KB,128KB,256KB和512KB时均能放入L2中,又不至于被L1容纳。下图可见步长大于8时,读带宽基本不变,大致为16.6GB/s。
在这里插入图片描述

L3带宽

对于L3,工作集大小为64KB,128KB,256KB和512KB时均能放入L3中,又不至于被L2容纳。下图可见步长大于8时,读带宽基本不变,大致为2.5GB/s。但对于2MB的工作集,在步长变大到12以上时的反常情况也是无法解释的。
在这里插入图片描述

主存带宽

对于主存,工作集大小为32MB,64MB,128MB,256MB,512MB时均不至于被L3容纳。下图可见步长大于16时,读带宽基本不变,大致为1GB/s。对于较小的工作集,在步长变大到12以上时的反常情况也无法解释。
在这里插入图片描述
以上都是对纯读的带宽的分析,对另一半图的纯写的带宽的分析同理,不再赘述。

下图为单节点内用满28个计算核心的Memory Mount图(总的累计的带宽),左图为读带宽,右图为写带宽。
在这里插入图片描述
值得一提的是,由于14个核心共用一个CPU内的L3 cache,平均每核只有1.375MB(比每核单独使用的L2的1024KB大不了多少),因此读带宽的骤跌的位置在512KB到2MB之间。
在这里插入图片描述

同时从内存山的图中可以看到,当用满核心时,落在主存区域的以步长为1的读获得的带宽在205GB/s左右,这与之前看到的Intel Xeon Gold 6132的单CPU的理论内存带宽119.21GB/s的指标很接近了(单节点内两颗CPU)。

### 如何使用 STREAM 进行内存带宽性能测试 #### 准备工作 在准备阶段,确保所使用的环境满足以下条件: - 使用飞腾2000+ CPU 和 麒麟信安操作系统作为测试平台[^1]。 - 安装必要的开发工具链,包括编译器(如 GCC 或 Intel 编译器),以便能够编译 Stream 基准测试代码。 #### 获取并编译 Stream 测试程序 下载最新的 Stream 版本,并按照官方说明进行编译。通常情况下,可以通过 CMake 工具简化这一过程。对于特定于系统的优化,建议查阅相关文档来调整 Makefile 文件中的参数设置[^5]。 ```bash git clone https://github.com/jeffhammond/STREAM.git cd STREAM make CC=gcc FC=gfortran ``` #### 执行测试前的配置 为了获得更贴近实际应用情况的结果,在执行之前应当考虑以下几个方面: - **数据规模的选择**:确保分配的数据数组大小超过 L3 Cache 的容量,从而减少缓存命中带来的影响[^2]。 - **多线程支持**:如果目标架构支持多核或多线程处理,则可以在编译时启用 OpenMP 支持,利用多个核心同时运行以提高吞吐率[^4]。 - **BIOS 设置**:进入 BIOS 修改某些默认设定,比如关闭节能模式、开启 Turbo Boost 功能等,这些都可以帮助提升最终得分表现。 #### 启动测试与记录结果 完成上述准备工作之后就可以正式开始测验了。最简单的做法就是直接调用可执行文件;如果有特殊需求的话也可以传递额外参数给它。每次试验结束后都会打印出详细的统计信息到标准输出流中去[^3]。 ```bash ./stream_c.exe ``` 或者指定更多选项来自定义行为: ```bash OMP_NUM_THREADS=8 ./stream_c.exe -n 100000000 ``` 这里 `-n` 参数指定了向量长度,默认值可能不足以覆盖整个物理 RAM 空间,因此适当增大此数值有助于获取更加真实的测量值。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zongy17

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值