带你学习《深入理解计算机系统》程序性能优化探讨(5)——高速缓存、存储器山与矩阵乘法优化

        这一节内容将综合(3)和(4),讨论高速缓存相关的程序优化。

 

一、牛B完了的存储器山

        一个程序从存储系统中读数据的速率被称为读吞吐量或读带宽。如果一个程序在s秒的时间段内读n个字节,那么读吞吐量就是n/s,一般用MB/s作为单位。

        第(4)节中讨论的时间局部性和空间局部性。从缓存块大小来权衡,时间局部性和空间局部性似乎刚好成反比,但当我们全面的讨论整个存储层次性能时会发现,其实两种局部性可能各自有各自的规律。

        如果我们把一次性读取的数据量的大小称为工作集,把上一次和下一次读取数据的之间存储距离称为步长,如果我想获得工作集和步长这两种因素影响下程序的读吞吐量,会是什么结果呢?教材中给出实现代码,但我不打算在这里全部贴出来,因为详细的代码分析涉及到教材后面章节里的K次最优测量方法←_←内啥,我自己还没看懂,准确的说是暂时没耐着性子看下去,好像天书,等哪天有心情看懂了,再补上代码实现也不迟,那我们就先把存储器山的图贴出来吧:

 

        我们看到,存储器山由三个坐标确定点位,底面右边的坐标是工作集大小(wording set size),按字节计数;底面左边的坐标是步长(stride),按字计数。这里可以把字理解成4字节;垂直的坐标就是读吞吐量,根据吞吐量的大小判断程序性能的优劣。注意到这个存储器山针对的CPU型号已经写在上面了,这个CPU里面分别有大小2*32kb、块大小64字节的高速缓存L1,指令和数据缓存都是这么大。另外还有6M大小的高速缓存L2。

 

        首先把步长作为常数,来看工作集32k大小时的情况。一次性处理32k字节的数据,由于L1有32k的指令缓存和32k数据缓存,因此,32k工作集的数据是完全可以存入L1缓存的,因此可以看到此时它的吞吐量处于最优状态。奇怪的是,当工作集坐标往右减小时,山峰没有继续升高,而是急速降低,你可以明显的看到,从32k之后的下降陡坡,这是为什么?其实,这里需要了解测试程序的结构:

#define MINBYTES (1 << 12)  /* 最小工作集 ranges from 4 KB */

#define MAXBYTES (1 << 25)  /*最大工作集 ... up to 32 MB */
#define MAXSTRIDE 30           /*最大步长*/


    for (size = MAXBYTES; size >= MINBYTES; size >>= 1) {
for (stride = 1; stride <= MAXSTRIDE; stride++) {
   printf("%.1f\t", run(size, stride, Mhz));
}

        上面这个是生成存储器山的核心循环,第一层循环是遍历工作集大小,从32MB开始两倍变小,直到4kB为止;第二层循环是步进,从1到30字(4字节)。有了这两个测试关键参数传入存储器山生成函数run里,就能将这座山遍历出来。

        好了,当第一层size取值小于32k,而步长又大于20时,由于工作集size太小,因此每一个工作集处理时间会非常短,但正因为工作集太小,而步长又太大,数据集越不够连续,命中的情况就越遭,因此惩罚也就越大,主要的时间就消耗在不命中处罚和循环本身的开销上,因此山高峰背后那个明显下降,其实并不能反映出L1高速缓存的真实性能。

        我们再来看看这座山,很明显,工作集越小或者步长越小,对应的都是山峰越高,也就是吞吐量越好,从循环主题上看,无论是多大的步长,工作集越小,越可能在L1或者L2缓存中多停留,那么在第二层循环遍历时,现成缓存数据被重复调用的可能性就越大, 所以时间局部性就越好;(比如工作集大小为1,第一次读1,缓存123,第二次第三次就有现成的2、3用,但如果工作集大小为4,缓存装不下4个数,就算装下123下一次也用不上,因为下次读的是5678,那么每次都没现成的,都是冷不命中,并且还要往更慢的下层缓存要数据,当然时间局部性就很差了)

        另一方面,工作集不变时,你步长越小,在第二层遍历内部,上一次循环的数据被下一次循环共享的几率就越大,而步长越大,共享机会当然就越小,所以空间局部性就越差。(比如步长为2,那么第一次读1、2,缓存了1234,第二次读3、4时就有现成的;如果步长为4,那么每次几乎都没现成的了) 

        正因为有上面的分析,我们看到,空间局部性的斜坡相对连续,而时间局部性的斜坡就泾渭分明,体现了L1、L2和内存的读吞吐量之间巨大的性能差别。

 

        另外我们还注意到,L1数据缓存是32k,所以它在工作集超过32k时急速下降,符合预期。但L2大小是6M,那么我们预测的吞吐量也应该在工作集为6M时下降才对,但从我图上标注的两条线来看,下降处居然是工作集为4M的地方,why?按书上的解释是,L2不像L1那样把指令缓存和数据缓存独立区分,L2的数据和指令统一缓存在一起,因此虽然大小是6M,但你数据可不能独享这6M的空间,分2M给指令缓存也是可以理解的……

        还有个有趣的现象,我们单独来看L2区域的空间局部性。我们假定工作集大小固定在512kB,也就是L2区域的中间部位,我们看到,步长从0~16的部分,山体沿着步长的下坐标的方向有明显降趋势,这也符合之前对空间局部性的预期——步长越长,空间局部性越差!步长越长,被L1命中的可能性越低。但是从步长16开始往后走,坡度消失,几乎变成飞机场,这是为啥呢?原来,步长为16字,对应的就是64字节,这刚好是L1的块大小,也就是说,之前L1的一个块里就可能缓存了几个步进大小的数据,一次访问可能导致后面的数据在L1缓存块上命中,现在步进超过16,也就是超过64字节的步进,那么L1上的每一个块数据都不可能再有命中的存在,都必须从L2得到服务,因此吞吐量完全取决于L2传送到L1的性能,因此保持不变。

        存储器山是反映特定系统的时间和空间局部性的山,对于高级别的程序员,一旦有了这样的信息,他就会尽可能的使得自己的程序中频繁使用的字是从L1中存取,同时还要让尽可能多的字从L1高速缓存中访问到。这就分别利用时间局部性和空间局部性。

 

二、简单应用

        作为关心性能的程序员,知道对存储器层次结构各个部分访问时间的粗略估计值是很重要的。根据上面这匹山,估计下列这些位置读出一个4字节所需的时间,以CPU周期为单位(T9300的频率是2.5GHz):

        1、在芯片上的L1 d-cash;

        2、在芯片上的L2 d-cash;

        3、在主存上(工作集大小16M,步长=16),读吞吐率为80MB/s

 

        为什么讲这看似简单的题呢?也许从中能发现概念上的一个模糊点,至少我模糊得很,如果你也模糊,那恰好可以跟着理一理!

        第一个问, L1上读取4字节的时间,以CPU周期为单位。我们看L1的峰值吞吐率是10000MB/s,也就是10GB/s,而CPU的频率是2.5GHz,因此4个字节的读取时间就是(2.5/10)*4 = 1周期,也就是接近一周期。

        首先明确下,关于G和M的参数描述中是(1GHZ=10^3MHZ=10^6KHZ= 10^9HZ)的关系,而不是1024。OK,关于上面这个式子,你完全理解透了没?反正我刚开始是一头雾水,这里详细纠结下周期和频率的概念。

        下面是我面对纠结时的思考步骤:

                ①:CPU频率是2.5GHz,说明一秒钟运行2.5G次,那么一个周期就是1/2.5G秒,∵L1的峰值吞吐率是10GB每秒,现在CPU一个周期又是1/2.5G秒,那么一个周期的吞吐量就是两者相乘:10/2.5=4B,也就是说,CPU一个周期内的吞吐量是4字节——我去,这典型的是撞上答案!(当然,如果算出来是8B,我会用8/4算出2周期答案)

                ②:上面是先算出单位周期的吞吐量,然后与4字节相除,得出周期数,现在从频率出发来考虑:既然L1吞吐率是10GB每秒,那么每一个字节的耗时就是(1/10G)秒,4个字节耗时自然就是(4/10G)秒,既然算出了总耗时,接着就是把它转化成CPU周期数就OK了。怎么转换呢?考虑到CPU频率是2.5GHz,说明一秒钟运行2.5G次,现在我有(4/10G)秒,两者相乘,就能算出总的运行次数:(2.5G/10G)*4 = 1次,这意味着什么?意味着(4/10G)秒的这个时间量,刚好是CPU运行一次所需的时间,也就是CPU的周期!因此得出答案1周期!(当然,如果算出来是2次,那就说明答案是2周期!)

                ③:以上两种方法都得出正确答案,但有存在两个问题,其一,不可能每次算个时间周期数都要整这么麻烦;其二,感觉没有彻彻底底的理解清楚什么吞吐率频率和周期之间的关系,有云里雾里的错觉。那么接下来我就好好的整理了一下概念。

        周期T:每

  • 16
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值