编译器优化技术

      存在多种编译器优化技术来间接影响缓存的使用模式。下面仅举几例,且均假定编译器采用行主序(Row-major order)存储数组:

1. 循环交换(Loop Interchange)

考虑一个对二维数组a[100][5000]的循环处理

a [ 100 ] [ 5000 ] =... //初始化
for (j = 0 ; j < 5000 ; j =j + 1 ) {
  for (i = 0 ; i < 100 ; i =i + 1 ) {
    a [i ] [j ] = 2 * a [i ] [j ] ;
  }
}

如果源代码的外循环遍历行,而内循环遍历列,则总是会造成大量的缓存失效。这是因为当失效时,缓存从内存中抓取的整个数据块几乎都是同行不同列的数据,而这些数据在接下来的内循环中完全无法被重复利用。

通过循环交换改进如下

a [ 100 ] [ 5000 ] =... //初始化
for (i = 0 ; i < 100 ; i =i + 1 ) {
  for (j = 0 ; j < 5000 ; j =j + 1 ) {
    a [i ] [j ] = 2 * a [i ] [j ] ;
  }
}

这样,缓存因为a[i][0]失效而从内存中抓取的数据块实际上覆盖了a[i][0]到a[i][7]的全部数据(假定使用32字节大小的缓存块,每个整型值占四字节)。这样后边连续七次内循环均可告命中。

2. 循环合并(Loop fusion)

考虑下边的代码

a [ 1000 ] =... //初始化
for (i = 0 ; i < 1000 ; i =i + 1 ) {
  a [i ] = 2 * a [i ] ;
}
for (i = 0 ; i < 1000 ; i =i + 1 ) {
  b [i ] = a [i ] + CONSTANT ;
}

如果编译器可以证明两个循环体可以合并到一个基本块而不影响程序结果,则应该进行合并。这是因为,通过合并,原来第二个循环的语句在访问内存时必然会命中。

合并后的代码

a [ 1000 ] =... //初始化
for (i = 0 ; i < 1000 ; i =i + 1 ) {
  a [i ] = 2 * a [i ] ;
  b [i ] = a [i ] + CONSTANT ; //对a[i]的访问必然命中缓存
}

3. 循环分块(Blocking)

当循环遍历的内存范围很大(例如一个大数组)时,由于缓存容积有限,可能会导致每次遍历结束时留下的缓存布局完全无法被接下来的一次遍历利用。这种情形下对循环进行分块就十分有意义。

假设现在使用了一个非常小的全相联缓存,只有四个缓存段,每个16字节。二维整型数组b和c的大小均为1024*1024,并被存储上内存的连续地址上。由于每个整数占4个字节,故在这个缓存最多只能容纳16个整数。假定该缓存使用LRU置换策略。首先考虑未经过优化的代码。这个代码段遍历整个矩阵,每次遍历过程中交替访问由i和j分别指定的向量b[i][0]-b[i][1023]和c[0][j]-c[1023][j]。

b [ 1024 ] =... //初始化
c [ 1024 ] =... //初始化
for (i = 0 ; i < 1024 ; i =i + 1 ) {
  for (j = 0 ; j < 1024 ; j =j + 1 ) {
    for (k = 0 ; k < 1024 ; k =k + 1 ) {
      ... = b [i ] [k ] + c [k ] [j ] ;
    }
  }
}

由于缓存极小,这段代码效率十分低。考虑当i=0、j=0时,最内循环最后一次遍历中,在访问完b[i][k](实际上是b[0][1023])之后,但还没有访问c[k][j](实际上是c[1023][0])的情形,缓存内容如下图所示。

CPU缓存 11 分段前0.png

之后,访问c[1023][0],缓存被刷新为

CPU缓存 11 分段前1.png

这样一个结果无疑对于下一次遍历(i=0、j=1)毫无价值。因为在k自增到4之前,所有数据都无法被重复利用,结果只能被换出。但如果改成

b [ 1024 ] =... //初始化
c [ 1024 ] =... //初始化
int B = 4 ;
for (jj = 0 ;jj < 1024 ;jj =jj +B ) {
  for (kk = 0 ;kk < 1024 ;kk =kk +B ) {
    for (i = 0 ; i < 1024 ; i =i + 1 ) {
      for (j =jj ; j <min (jj , 1024 ) ; j =j + 1 ) {
        for (k =kk ; k <min (kk , 1024 ) ; k =k + 1 ) {
          ... = b [i ] [k ] + c [k ] [j ] ;
        }
      }
    }
  }
}

再次考虑当jj=0、kk=0、i=0、j=0时,最内循环最后一次遍历中,在访问完b[i][k](实际上是b[0][3])之后,但还没有访问c[k][j](实际上是c[3][0])的情形,缓存内容如下图所示。

CPU缓存 11 分段后0.png

之后,访问c[3][0],缓存被刷新为

CPU缓存 11 分段后1.png

这样的结果对于下一次遍历(jj=0、kk=0、i=0、j=1)就十分有价值,因为所需数据的大部分,包括b[0][0]-b[0][3]和c[1][1]-c[3][1],全都已在缓存中。

此外,还有数组合并(Array merge)、循环分解(Loop fission)、分支取直(Branch Straigtening)等多种技术。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值