关于缓存和优化

缓存


现代处理器在读/写内存时是十分缓慢的,通常需要几千个处理器周期才能完成。和CPU里的寄存器相比,存取寄存器只需要十几个周期,甚至有时只需要一个周期。为了降低读/写主内存的平均时间,现代处理器会采用高速的内存缓存(cache)。

缓存是一种特殊的内存,当CPU首次读取某内存区域的内容时,该内存小块会载入高速缓存。这个内存块单位称为缓存线(cache line),缓存线通常在8至512字节,具体视CPU架构而定。以后CPU再次读取数据,该数据已在缓存中,那么数据就可以直接从缓存加载到CPU,这个过程比直接从内存中读取快很多。如果要求的数据不在缓存中,才必须读取主内存。这种情况叫做缓存命中失败(cache miss)。我们基本很难避免不出现miss,但是通过最优的方式安排内存中的数据和操作数据的算法代码,就可以减少miss的次数。

现代CPU上面的一级(level 1, L1)缓存和二级(level 2, L2)缓存:

cpu_cache

主内存(Main RAM)比L2缓存慢,L2缓存比L1缓存慢,部件的价格则是反过来的。本人使用的是Mac,在BSD系统上可以通过命令来查询CPU参数:

Last login: Thu Mar  6 20:12:33 on console
➜  ~  sysctl machdep.cpu 
.......
machdep.cpu.cache.linesize: 64
machdep.cpu.cache.L2_associativity: 8
machdep.cpu.cache.size: 256

.......

指令缓存和数据缓存


在为游戏引擎或者任何性能关键系统编写代码的时候,必须要明白不仅仅是只有游戏素材被塞入了内存中,游戏代码指令也被放入了内存。指令缓存(instruction cache, I-cache)会预载即将执行的机器码,而数据缓存(data cache, D-cache)则用来加速主内存读/写数据。大部分处理器会在物理上独立分开两种缓存。所以,程序变慢了,卡了,要考虑不仅仅是数据缓存命中失败,还可能是指令缓存命中失败。

避免数据缓存命中失败

  1. 把数据编排进连续的内存块中,避免CPU读取时跑来跑去。
  2. 尺寸越小越好,这样才能把数据完整的塞入cache中。
  3. 访问数据时要顺序。

避免指令缓存命中失败

这一部分是本篇文章的精华所在,数据缓存的道理在之前的代码编写中或多或少都有知道,明白在渲染过程中如果有大量随机读取内存时存在画面丢帧的现象。代码指令在内存中的布局基本上是由编译器和链接器决定的,几乎无法控制,然而,多数c/c++链接器都有一些简单规则,知悉并运用它们就能控制代码的内存布局。

  1. 单个函数的机器码几乎总是置于连续的内存,绝大多数情况下,链接器不会把一个函数切开,并在中间放置另一个函数。
  2. 编译器和链接器按函数在翻译单元源代码(.cpp文件)中的出现次序排列内存布局。
  3. 位于一个翻译单元内的函数总是置于连续内存中。即链接器永不会把已编译的翻译单元切开,中间加插其他翻译单元的代码。在Visual C++编译器中可以使用函数级(function-level linking) /Gy选项,那么编译的输出单位为函数,链接时按各个函数并不一定以翻译单元内的次序进行布局。还可以在链接时使用/ORDER选项自定义函数的布局次序。

因此,可以采用以下的经验法则。

  1. 高效能代码的体积越小越好,体积以机器码指令数目为单位。(编译器和链接器会负责把函数置于连续内存。)
  2. 在性能关键的代码段落中,避免调用函数
  3. 若要调用某函数,就把函数置于最接近调用函数的地方,最好是紧接调用函数的前后,而不要把该函数置于另一翻译单元(因为这样就完全无法控制两个函数的距离)。
  4. 谨慎地使用内联函数。内联小型函数能增进效能。然而过多的内联会增大代码体积,使性能关键代码再不能完全装进缓存。假设有一个处理大量数据的紧凑循环,若循环内的代码不能完全装进缓存,每个循环迭代便会产生两次指令缓存命中失败。遇到这种情况,最好重新思考算法及其代码实现,看看能否减少关键循环中的代码量。

题外话


《游戏引擎架构》的作者是顽皮狗资深程序员Jason Gregory,之前听过一期机核网在GDC2012大会上联合采访顽皮狗的产品总监Benson Russell & 程序员Marshall Robin的节目。我记得里面说到了策划们经常喜欢跑到程序那边看看他们在干嘛,往往那些程序员们都在盯着屏幕上面的一些统计图表傻笑,看着自己优化的代码又减少了多少ms的消耗。顽皮狗的游戏深受玩家们的喜爱,和所有策划,程序,美术的工作是离不开的,实现游戏本身的程序员们值得大家尊敬。

engine_time

《神秘海域2》引擎中的剖析分类显示,展示了多个顶层引擎系统的粗略计时。

function_cost

《神秘海域2》引擎中的阶梯式显示,让用户可以深入探究至某个函数调用的开销。

cpu_cost

《神秘海域2》引擎中的时间线模式,可完全展示单帧里多个操作在PS3 SPU,GPU,CPU上的运行情况。

本文参考:《游戏引擎架构》

Share