一份2010年的77页老PPT,这几年在游戏引擎圈子里被反复翻出来当教材。它讲的道理不复杂:同一段瞄准计算,逻辑一个字没改,只换了数据在内存里摆放的方式,估算周期能从7680降到1980;另一个剔除系统改造后,速度快3倍,代码量降到1/5。这份材料没发明新算法,它改的只是"数据摆哪儿"这一件事——但这件事,恰恰是很多工程师最容易绕过去的地方。
内存有多慢,一张周期表说清楚
演示稿给出的硬件背景是3.2GHz的CPU:
- 从主内存读一次数据,约600个周期
- 从L2缓存读,约40个周期
- 从L1缓存或寄存器读,1到2个周期
这是当年主机平台的典型比例,不是今天所有硬件的通用结论。但差距的量级至今没消失:CPU越算越快,内存追不上,读数据往往比算数据贵得多。这份材料还提了两个不只关乎速度的理由——搞不清数据怎么被读写,多线程只能给数据加锁,锁不住代码逻辑本身;想把计算丢给协处理器(当年是SPU,今天是GPU)去跑,数据格式不清楚也无从丢起。数据导向设计,先把这个边界画清楚。
Bot瞄准案例:7680周期对1980周期
演示稿用一个最简单的例子摊开讲。游戏里一个Bot更新瞄准方向,逻辑就一行:坐标点乘目标位置,再乘一个系数,写进一个浮点数。
按面向对象的写法,这段计算挂在Bot类的方法上,每次调用都要为一个Bot单独取指令、单独读坐标、单独读系数,再单独写回结果。四个Bot调用四次,合计估算约7680个周期,大头都花在等内存,不是花在那一次乘法上。
换成数据导向的写法,函数不再挂在类上,改成接收一个坐标数组和一个系数数组,一次性读进来,循环处理,结果连续写进一个输出数组。计算逻辑一个字没变,还是那句点乘乘系数。变的只是:只取需要的输入、写进连续内存、批量跑完整个循环。同样四个Bot,估算周期降到1980——差距不是算法更聪明,是CPU不用来回跳着去不同地址"够"数据。
材料里第二个例子是剔除系统(culling system),把基于对象遍历的写法换成线性数组加暴力遍历。
这两组数字都来自2010年这份材料的示例估算,不是现代硬件的通用基准,更不是所有系统都能复制的收益。它们指向同一个方向:访问模式提前想清楚,"暴力"的线性扫描往往比"聪明"的对象封装更快。
抽象不是免费的
面向对象编程的好处这份材料从没否定——封装、多态、可复用,这些是给"人"看的,方便工程师组织代码、划分职责。问题是CPU不关心你的类设计得多优雅,它只认内存地址和缓存行。一个对象把坐标、系数、状态、指针混着放,循环里只想读坐标,CPU却要把整个对象所在的缓存行都搬进来,用得上的字节可能只占零头。
内存不认对象,只认地址。
荀子讲"君子生非异也,善假于物也"。数据导向设计的聪明,不是比面向对象更高明,而是更老实地顺着内存这个"物"的脾性来——先问输出需要什么最小输入,再倒推数据该怎么摆、怎么流动,而不是先建好模型再往里塞逻辑。
这个思路的代价也不该被藏起来。数据结构提前定死,后期改需求的灵活性会打折;逻辑和数据拆开摆,阅读和调试的直觉性会变差;不是所有代码都值得为了几十个周期,牺牲面向对象带来的可维护性。材料自己也说得明白:游戏行业敢这么干,前提是"我们清楚自己的数据长什么样"——反过来,数据形态一旦模糊,这套优势就无从谈起。受这套思路影响最直接的,是游戏引擎、实时渲染、物理模拟这类每帧要处理成千上万个对象的热路径,不是普通业务系统。
- 结论.内存瓶颈面前,先问数据怎么流动,比先问对象长什么样更值钱。
- 风险.这套账只在访问模式可预测、性能敏感的热路径上成立,业务代码硬套只会徒增维护成本。
十五年前一张77页的PPT,今天读起来还不过时,大概不是因为"数据导向"这四个字有多新,而是它戳中了工程师容易图省事绕开的事——代码是写给人看的,但程序终归跑在硬件上,硬件从不惯着抽象。
