内存及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)。

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Linux内存buffer/cache很大是因为Linux系统采用了一种称为页缓存(Page Cache)的机制来提高文件系统的性能。具体来说,当文件被读取到内存中时,Linux会将读取到的文件数据缓存内存的页缓存中,以便下次访问相同文件时可以直接从页缓存中获取,而不需要再次从磁盘中读取。这种机制可以大大提升系统IO的性能。 Linux内存buffer/cache的大小受到系统资源管理的影响,它会充分利用可用的内存空间来提供更高效的文件访问。当系统运行时,如果有大量的可用内存,并且没有其他进程需要使用,Linux会将这些多余的内存用于buffer/cache。这样一来,当需要从磁盘读取文件时,大部分情况下可以直接从内存中获取,而不必再次访问磁盘,从而大大提高系统的响应速度。 值得注意的是,虽然buffer/cache占用了大量内存,但这些内存并不是完全被占用,而是可以根据需要被释放的。当其他程序需要更多的内存时,Linux会自动将buffer/cache中的数据释放出来,以满足其他程序的需求。这个过程被称为自动缓存(Automatic caching),可以确保系统内存始终处于最佳状态。 总而言之,Linux内存buffer/cache很大是Linux系统为了提高文件系统的性能而采取的一种机制。通过将文件数据缓存内存中,可以减少对磁盘的读取操作,从而大大提升了系统的响应速度。同时,这些内存可以根据需要进行释放,确保系统内存始终处于最佳状态。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

zongy17

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

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

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

打赏作者

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

抵扣说明:

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

余额充值