编写高效代码 之 优化内存访问
2012-10-09 12:54
267 查看
从理论上看,每条运算指令的执行时间都很短,大多数指令一个Cycle就能完成,很多时候还能一个Cycle执行多条指令,可是实际上,执行指令只是处理器要做的很少一部分工作,处理器还要从存储器中取指令,从存储器中将数据导入到寄存器中,等算完后,再将结果存入到存储器中。
处理器运算的速度像兔子赛跑一样快,但是存储器的访问速度像乌龟走路一样慢,而且越是远离内核的存储器,访问速度越慢。
下面这个表是在几个x86处理器中,内核访问各级数据Cache和内存所需要的 Cycle数:
从这张表可以看出,从L1中访问数据速度还较快,但是仍然要慢于运算的速度,从L2中访问数据速度还能将就,从内存中访问数据就无法忍受了。我们应该尽量减少内存的访问,要访问,也要尽量避免Cache miss。
下面这段程序,需要4次内存访问:
如果改成如下的形式,就只有两次内存访问了:
最好改为:
以C64 DSP为例,通常一条指令,一次对两个32bit的数据做处理,而它却1次可以访问两个64bit数据。
在DSP中,SIMD指令和普通指令共用寄存器,有些数据虽然不能用SIMD指令处理,我们也可以一次将内存中的多个数据搬入到寄存器中,用简单指令分别处理,当要存储时,将分散的寄存器组合在一个连续的位置,再将数据输出到内存中。由于操作寄存器远比操作存储器快,虽然多了些数据的拆分和组合的操作,代价还是值得的。
Cache正是利用了程序、数据访问时的时间局部性和空间局部性,为了使Cache的访问效率最高,程序和数据的组织,也应该要符合这两个特性。最典型的例子就是二维数组的访问,下面就是一个二维数组:
二维数组
如果a[i][j]在Cache中,那么a[i][j+1]就很可能也在Cache中,但是a[i+1][j]则不一定。于是代码这样写就不太好:
应该采用如下的写法,Cache的效率才高:
再来看另一个例子,在下面的这段代码中:
如果a和b数组存放在不同的Cache line中,一开始访问a会产生一次Cache miss,一开始访问b也会产生一次Cache miss,如果a和b数组存放在一个Cache line之中,则只会产生一次Cache miss。
在一起使用的数据放在一起能减少数据的Cache miss,在一起使用的函数放在一起能减少程序的Cache miss。
程序的组织也要符合Cache局部性原则。例如,一个程序大小为40K Bytes,经常使用的代码占据30K,很少使用的代码(如初始化、异常处理等)占据10K,指令Cache为32K Bytes,这段程序是无法完全放在Cache中的,我们可以将经常执行的代码放在一起,将很少使用的代码放在一起。这样经常使用的代码就能完全进入Cache中,减少了Cache miss。
有些较好的编译器能分析函数的调用关系,并合理的安排函数的存储位置,以提高指令Cache的命中效率。
处理器运算的速度像兔子赛跑一样快,但是存储器的访问速度像乌龟走路一样慢,而且越是远离内核的存储器,访问速度越慢。
下面这个表是在几个x86处理器中,内核访问各级数据Cache和内存所需要的 Cycle数:
处理器 | Level 1 data Cache | Level 2 data Cache | 内存 |
P3 | 3 | 8 | 140 |
P4 | 2 | 19 | 350 |
PM(奔腾M) | 3 | 10 | 80 |
Core2(酷睿) | 3 | 14 | 185 |
Opteron(皓龙) | 3 | 13 | 100 |
少使用数组,少使用指针
由于大块数据会被放在存储器中,简单局部变量才会被放在寄存器中,因此应该尽量少用数组、指针,多用简单局部变量。下面这段程序,需要4次内存访问:
c = a[i] * b[i]; d = a[i] + b[i];
如果改成如下的形式,就只有两次内存访问了:
x = a[i]; y = b[i]; c = x * y; d = x + y;
少用全局变量
全局变量因为要被多个模块使用,不会被放到寄存器中,局部变量才能被放在寄存器中,应尽量避免使用全局变量。下面这段程序:int x; int fun_a () { int y, z; y = x; z = x + 1; … }
最好改为:
int x; int fun_a () { int y, z, temp; temp = x; y = temp; z = temp + 1; … }
一次多访问一些数据
我们通常会有这样的生活常识,要去很远的地方,就会多带一些东西,要去近一点的地方,就会少带一些东西。既然数据访问速度较慢,我们就一次多访问些数据。处理器将这些为我们考虑到了,通常都提供了较大的数据带宽。以C64 DSP为例,通常一条指令,一次对两个32bit的数据做处理,而它却1次可以访问两个64bit数据。
在DSP中,SIMD指令和普通指令共用寄存器,有些数据虽然不能用SIMD指令处理,我们也可以一次将内存中的多个数据搬入到寄存器中,用简单指令分别处理,当要存储时,将分散的寄存器组合在一个连续的位置,再将数据输出到内存中。由于操作寄存器远比操作存储器快,虽然多了些数据的拆分和组合的操作,代价还是值得的。
Cache正是利用了程序、数据访问时的时间局部性和空间局部性,为了使Cache的访问效率最高,程序和数据的组织,也应该要符合这两个特性。最典型的例子就是二维数组的访问,下面就是一个二维数组:
二维数组
如果a[i][j]在Cache中,那么a[i][j+1]就很可能也在Cache中,但是a[i+1][j]则不一定。于是代码这样写就不太好:
for(j=0; j<500; j++) { for(i=0; i<500; i++) { sum += a[i][j]; } }
应该采用如下的写法,Cache的效率才高:
for(i=0; i<500; i++) { for(j=0; j<500; j++) { sum += a[i][j]; } }
再来看另一个例子,在下面的这段代码中:
int a[4], b[4], i; for (i = 0; i < 4; i++) { b[i] = Func(a[i]); }
如果a和b数组存放在不同的Cache line中,一开始访问a会产生一次Cache miss,一开始访问b也会产生一次Cache miss,如果a和b数组存放在一个Cache line之中,则只会产生一次Cache miss。
在一起使用的数据放在一起能减少数据的Cache miss,在一起使用的函数放在一起能减少程序的Cache miss。
程序的组织也要符合Cache局部性原则。例如,一个程序大小为40K Bytes,经常使用的代码占据30K,很少使用的代码(如初始化、异常处理等)占据10K,指令Cache为32K Bytes,这段程序是无法完全放在Cache中的,我们可以将经常执行的代码放在一起,将很少使用的代码放在一起。这样经常使用的代码就能完全进入Cache中,减少了Cache miss。
有些较好的编译器能分析函数的调用关系,并合理的安排函数的存储位置,以提高指令Cache的命中效率。
相关文章推荐
- 连载:编写高效代码(12) 优化内存访问——别让包袱拖垮了你
- 编写高效代码(12) 优化内存访问——别让包袱拖垮了你
- 编写高效代码(7) 优化内存访问——别让包袱拖垮了你
- 编写高效代码(8) 程序、数据访问符合Cache的时间、空间局部性
- 编写高效的C程序与C代码优化
- 程序设计基石与实践系列之编写高效的C程序与C代码优化
- 【ZZ】C 语言中的指针和内存泄漏 & 编写高效的C程序与C代码优化
- 编写高效的C程序与C代码优化
- 编写高效的C程序与C代码优化
- 编写高效的C程序与C代码优化
- 编写高效代码(13) 数据对齐访问
- 编写高效的C程序与C代码优化 via jobbole
- 编写高效的C程序与C代码优化
- 编写高效代码(14) 程序、数据访问符合Cache的时间、空间局部性
- 连载:编写高效代码(13) 数据对齐访问
- 编写高效的C程序与C代码优化
- 连载:编写高效代码(14) 程序、数据访问符合Cache的时间、空间局部性
- 编写高效的C程序与C代码优化
- 编写高效的C程序与C代码优化
- 程序设计基石与实践系列之编写高效的C程序与C代码优化