戏说数据结构和算法

计算机和软件专业的同学在本科期间都有一门必修课:国内大多数叫《数据结构》,国外大多叫《算法导论》,非常重要。分为两部分:数据结构——array、list、stack、queue、tree、set、graph等,和算法——增删改查、排序等等。

这是狭义上的数据结构和算法。实际软件开发中,只要涉及到复杂的功能时,往往会涉及书本之外的数据结构和算法的问题。对于大型软件工程来说,尤其是游戏引擎这种对性能要求近乎苛刻的系统,算法尤为重要。

但是,我们在大学里学习《数据结构》这门课时,并不知道的一点是,这些数据结构和算法都是在计算机的“鸿蒙时代”设计出来的,计算和数据读取的性能接近。但是如果运行在当今的硬件上,可能使用map不见得比无脑遍历连续数组的方式更快,甚至同样的算法在不同的硬件架构下也会得出截然相反的结果。忽视了硬件特性的数据结构和算法,高性能就是镜中花,水中月。遗憾的是大学里老师不会告诉我们这些,以至于工作了很多年的程序员,甚至是很多引擎程序员都不知道,依然生搬硬套教科书上的做法,难免贻笑大方。

寒霜引擎做过一件“毁三观”的事:不用四叉树、八叉树、BSP、BVH、Portal等来管理场景,而是无脑遍历所有场景实体,竟然更快。见《Culling The Battlefield》。

那么这是为什么呢?其实这是近代计算机硬件的发展趋势导致的。我们先来看一下近些年来硬件的发展轨迹,见下图(不要纠结现在马上就2021年了,而图中的数据只到2010年)。图中显示了计算性能大幅提升,而带宽却发展缓慢。

为了提升性能,当前PC上的CPU都是金字塔式的三级cache:越靠近CPU,cache大小越小,速度越高。执行命令访问数据时,先从cache line开始查找,没有的话就依次向远离CPU的方向访问各级cache,一直到主存储器,甚至是去磁盘或者外部存储设备上访问数据。这样,如果数据组织的跟算法的访问顺序很接近,那么就能大幅减少Cache miss,从而大幅提升性能。那可能有人要问,干嘛不给CPU一大块速度非常高的cache呢?很简单,就是成本太高了。

所以,如果你写的代码性能低,可能往往不是你的指令太多,而是数据排布的不合理,造成了大量的cache miss。

针对这种硬件现状,就诞生了一种新的程序设计思想——面向数据编程(Data-oriented design),简称DOD。传统的面向对象设计是以功能为核心的,而DOD是以数据处理为核心的(程序的根本目的不就是处理数据吗)。核心思想就是将需要处理的相关数据都紧密排布在物理存储器上,一次性处理一组对象的某个操作,而不是随意访问分散在物理内存上的各种数据。另外需要注意的是,程序本身也是数据,执行前也需要加载到CPU去执行,一次性处理所有的数据就不存在反复加载程序了。DOD的数据组织形式最典型的一种就是将AOS(Array of Structures)改成SOA(Structure of Arrays),分别见下图(出自https://en.wikipedia.org/wiki/AoS_and_SoA)。

  

DOD也不是什么新技术了,Ogre 2.0多年前就开始研发了,计划从面向对象改成面向数据设计,见《OGRE.2.0.pdf》。2013-2015年在自研《盘古引擎》时也开始使用包括DOD这样的cache友好的设计思想,下面罗列这期间遇到过的一些相关案例。

数据压缩是最常见的一种cache友好的方案。例如,我们纹理压缩、顶点属性压缩,甚至是延迟着色里的GBuffer也能疯狂压缩(CryEngine曾经将GBuffer压缩到了3张RT)。

内存池和对象池是为了解决内存碎片,但是也利于性能提升。

将Ogre里的八叉树改成Cache友好的四叉树,性能提升很多。

使用面向数据设计去重构粒子系统,大幅减少cache miss。

SSAO采样数量一定的情况下,随着采样核的增大,耗时也会增大,为啥?就是因为GPU上的cache大小也是有限的,采样范围大了就增加了cache miss。还有,渲染SSAO时用了一个4x4的随机纹理,在屏幕上反复平铺以增加采样方向的随机性,也实践了数据访问的连续性。下图分别为随机纹理、中间结果和最终过滤的结果。

 

模型里的顶点在编辑器里需要排序,以有效利用VS处理时的cache。

Stl库里的很多容器(containers)的第二个模板参数默认都是内置的模板类template <class _Ty> class allocator。一般来说,内置的分配器就足够了。但是对于3D实时引擎这种对性能要求近乎苛刻的系统,自己设计一个就可以根据实际的应用场景来合理排布容器元素。

曾帮一家公司优化过一个算法,里面需要遍历一个二维数组,修改下访问的主序就能得到截然不同的性能。

之前做分布式纹理压缩时深入研究过pvr这种外部压缩格式,内部压缩格式有PVRTC2和PVRTC4。发现它们的纹素排列方式很诡异,并不是像其它压缩格式那样简单地从从上到下、从左到右,而是之字形排布的。推断为的也是cache友好,须知GPU里的显存也是有cache的,我们shader里进行一次采样,其实硬件就是将一大块都从显存里传输到cache里了。除了CPU上有“cache友好”这一说法,GPU上也是有的。

最近两年Unity也搞出了ECS,其实就是新瓶装旧酒,底层依然是DOD的设计思想。当然不可否认的是Unity一贯的优势——易用性做的真的很好。尽管各种引擎内部都支持DOD的设计,但是能将这个技术暴露出给用户使用的,独此一家。

所以说数据的组织形式比算法更重要:算法是由需求决定的,数据要按照算法去排布。

当然,DOD也有缺点,会破坏封装性,增加了维护成本。那么怎么来决定是用OOD还是DOD呢?我的结论是,将你代码里的1%-10%的代码热点使用DOD,其它地方使用OOD。“将性能优化到极致”是我的口头禅,但是也不会不明白“过犹不及”的道理。世界上绝大部分选择本就是妥协的结果。

那么回到原来的问题,大学学习《数据结构和算法》就没有用处了?绝不是的。首先是通过这门课来深入理解软件运行的原理。另外,还能锻炼逻辑思维能力。当然还有很多都可以锻炼逻辑思维能力,比如那个辍学搞写作的韩寒曾说过,靠学习数学来练习逻辑思维能力就是扯淡,他认为写作更能锻炼这个能力。

那么学习《数据结构和算法》还有什么额外的用处呢?肯定也是有的。保不准哪一天等你已经工作了十几年以后,还有极个别公司面试时会拿这个来难为你,这时你就不用再消耗宝贵的时间去翻书了。

最近很忙,随便写点吧,写到哪算哪。引用的很多案例都是很多年前的,难免记忆出现错位,也懒得去查笔记验证了,出现错误请求读者斧正。另外很多地方跳跃性较强,也难免灯下黑。老引擎程序员可能都碰到过类似问题,看后会心一笑。新入行的看懵的话也不负责解释,以后工作中慢慢去品吧。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值