一份2010年的77页老PPT,这几年在游戏引擎圈子里被反复翻出来当教材。它讲的道理不复杂:同一段瞄准计算,逻辑一个字没改,只换了数据在内存里摆放的方式,估算周期能从7680降到1980;另一个剔除系统改造后,速度快3倍,代码量降到1/5。这份材料没发明新算法,它改的只是"数据摆哪儿"这一件事——但这件事,恰恰是很多工程师最容易绕过去的地方。

内存有多慢,一张周期表说清楚

演示稿给出的硬件背景是3.2GHz的CPU:

  • 从主内存读一次数据,约600个周期
  • 从L2缓存读,约40个周期
  • 从L1缓存或寄存器读,1到2个周期
内存有多慢(3.2GHz估算) 主内存 ~600 周期 L2 缓存 ~40 周期 L1/寄存器 1–2 周期 2010年前后主机平台典型延迟示意,非现代硬件通用值

这是当年主机平台的典型比例,不是今天所有硬件的通用结论。但差距的量级至今没消失:CPU越算越快,内存追不上,读数据往往比算数据贵得多。这份材料还提了两个不只关乎速度的理由——搞不清数据怎么被读写,多线程只能给数据加锁,锁不住代码逻辑本身;想把计算丢给协处理器(当年是SPU,今天是GPU)去跑,数据格式不清楚也无从丢起。数据导向设计,先把这个边界画清楚。

Bot瞄准案例:7680周期对1980周期

演示稿用一个最简单的例子摊开讲。游戏里一个Bot更新瞄准方向,逻辑就一行:坐标点乘目标位置,再乘一个系数,写进一个浮点数。

同一段瞄准代码,周期对比 面向对象写法 逐个Bot单独调用 各自读坐标、系数 频繁跳转不同地址 7680 周期 4个Bot合计估算 数据导向写法 批量读入坐标数组 循环处理,逻辑不变 连续写入结果数组 1980 周期 同样4个Bot估算 数字来自2010年演示稿的示例估算,非通用性能基准

按面向对象的写法,这段计算挂在Bot类的方法上,每次调用都要为一个Bot单独取指令、单独读坐标、单独读系数,再单独写回结果。四个Bot调用四次,合计估算约7680个周期,大头都花在等内存,不是花在那一次乘法上。

换成数据导向的写法,函数不再挂在类上,改成接收一个坐标数组和一个系数数组,一次性读进来,循环处理,结果连续写进一个输出数组。计算逻辑一个字没变,还是那句点乘乘系数。变的只是:只取需要的输入、写进连续内存、批量跑完整个循环。同样四个Bot,估算周期降到1980——差距不是算法更聪明,是CPU不用来回跳着去不同地址"够"数据。

材料里第二个例子是剔除系统(culling system),把基于对象遍历的写法换成线性数组加暴力遍历。

剔除系统改造效果(演示稿示例) 3x 运行速度提升 1/5 代码量降至 更简单 逻辑可维护性 针对特定剔除逻辑的改造效果,不代表所有系统都有同等收益

这两组数字都来自2010年这份材料的示例估算,不是现代硬件的通用基准,更不是所有系统都能复制的收益。它们指向同一个方向:访问模式提前想清楚,"暴力"的线性扫描往往比"聪明"的对象封装更快。


抽象不是免费的

面向对象编程的好处这份材料从没否定——封装、多态、可复用,这些是给"人"看的,方便工程师组织代码、划分职责。问题是CPU不关心你的类设计得多优雅,它只认内存地址和缓存行。一个对象把坐标、系数、状态、指针混着放,循环里只想读坐标,CPU却要把整个对象所在的缓存行都搬进来,用得上的字节可能只占零头。

内存不认对象,只认地址。

荀子讲"君子生非异也,善假于物也"。数据导向设计的聪明,不是比面向对象更高明,而是更老实地顺着内存这个"物"的脾性来——先问输出需要什么最小输入,再倒推数据该怎么摆、怎么流动,而不是先建好模型再往里塞逻辑。

这个思路的代价也不该被藏起来。数据结构提前定死,后期改需求的灵活性会打折;逻辑和数据拆开摆,阅读和调试的直觉性会变差;不是所有代码都值得为了几十个周期,牺牲面向对象带来的可维护性。材料自己也说得明白:游戏行业敢这么干,前提是"我们清楚自己的数据长什么样"——反过来,数据形态一旦模糊,这套优势就无从谈起。受这套思路影响最直接的,是游戏引擎、实时渲染、物理模拟这类每帧要处理成千上万个对象的热路径,不是普通业务系统。

  • 结论.内存瓶颈面前,先问数据怎么流动,比先问对象长什么样更值钱。
  • 风险.这套账只在访问模式可预测、性能敏感的热路径上成立,业务代码硬套只会徒增维护成本。

十五年前一张77页的PPT,今天读起来还不过时,大概不是因为"数据导向"这四个字有多新,而是它戳中了工程师容易图省事绕开的事——代码是写给人看的,但程序终归跑在硬件上,硬件从不惯着抽象。