Coding for Speed 技术分享
上周和公司技术同事们作了次《Coding for Speed》技术分享,本来这只是再普通不过的技术探讨和交流(虽然挂了个颇有噱头的名称),但分享的时候逻辑没理好,语速很快,时间也太紧,因此难言是合格的“分享”、“探讨”,所以我觉得有必要以简短的文章形式对原 PPT 作点补充,即便分享的内容很少也很简单。
本文将按原 PPT 的内容顺序分别作扩展说明或阐述,部分敏感信息将隐去,或只会简单提及。
作本技术分享的初衷,一是在工作中了解及接触了一些我个人认为可优化改进的代码或设计,二也想能和同事们作一些技术方面的交流探讨,三我本人一直对性能这个议题颇有兴趣。
约七、八年前还在做端游的时候,我和几个同事时有“蛋疼的”纯写/内嵌汇编以优化某些函数,或测试比拼 VC、Delphi、LLVM/CLang(C++Builder) 甚至 ICC(Intel C++ Compiler) 等编译生成的程序运行速度,而现在恐怕极少还有技术同仁去折腾这些了吧,(大部分时候)硬件已足够快、编译器优化也足够优秀,多数时候我们只要能正确的设计/使用/选用合适的技术方案和数据结构/算法,基本上不大会有性能方面的问题(尤其是考虑到现今大部分甚至绝大部分游戏的在线人数等远比不了以前,并发压力等小了很多)。
当然,譬如休闲类的移动网络游戏,交互也(远)没有 MMORPG 复杂。
以上所列这些软件、程序、算法甚至指令集等,几乎都有“快”、“高性能”等标签,如 std::map 底层实现即是用的红黑树,LZ4 HC 的解压速度是 zlib 的好几倍而压缩率只小一点点,TCMalloc 和 FastMM 都是非常高效的内存管理器,还有 Id Software 那著名的 InvSqrt 函数,和 Chrome 的底层脚本引擎 V8...等等。
性能实在是很广很深的话题,在这方面我仅仅只是个有丁点经验的菜鸟而已。结合公司项目具体情况和诸同事技术及经验背景等因素,我将对查找表、位图算法、缓存及 IO 优化等方面和大家作探讨交流,并抛砖引玉分享 2 个我曾经想到的勉强也算“性能优化”的“优化”做法。
简而言之,查找表即是预先将某些数据计算好,待需要的时候直接读取即可,譬如我以前写的 2D 绘制引擎(AGE),就预先计算了 0~360° 的 Sin/Cos 值等。很多时候,以少量的空间成本换取性能上的提升不失是个好做法(反之亦然),而这也是很基础很常用的“优化技术”。
还记得以前重构某端游的任务系统时,我使用了位图来存储每个角色的任务标记位,譬如可用 2 个 bit 位来表示单个任务的完成情况(00 未接、01 已接、10 已放弃、11 已完成等),于是只需 1KB 的空间几乎就能存储每个玩家所有的任务完成情况了,而获取每个任务的状态也非常简单快速。
从 Memory Hierarchy 简略图可以看出,越靠近 CPU,速度越快但容量越小,而 Jeff Dean 给出的数据更加直观。相较寄存器少及小的先天限制(多数时候对寄存器的使用是由编译器去作的优化),好的局部性能使程序有着更好的性能(cache 的功劳),譬如精简的循环通常有较好的时间和空间局部性(当然,循环展开是另外的优化话题了,它能有效减少分支预测失败带来的性能损耗)。
较之 register 及 cache 的“不接地气”,明显内存(DRAM)就“平易近人”许多(想想 Redis),譬如游戏服务器完全可以(考虑)以适当的空间成本(内存占用)来提升负载能力,以优化改造之前同事写的某登录服务器为例,其做法是当客户端请求登录时,登录服务器会先从数据库查询用户数据然后再验证,而这步操作(相较而言)明显比较耗时,远比不上直接从内存读取的速度(譬如可以 hashtable 存取),所以可以考虑在登录服务器启动时,载入近几天登陆过游戏的用户数据至内存,当客户端请求登录时,若内存中没有此玩家数据,则再去数据库获取并将之放入内存,于是后续便只需读取内存(当然,每条玩家数据会占用多少内存空间,完全可以估算或测试)。而譬如密码,数据库保存的是其哈希值,但客户端登录时上传的是明文密码(通讯封包当然加密了,不过明文密码这个必然不好),于是登录服务器在验证时还需要将明文密码作哈希等处理然后再比较,我也顺带的将这个步骤作了优化,针对每个账号,当其登录请求第一次验证通过时,直接将其明文密码也缓存,于是以后的登录验证,直接比较明文密码即可,无需再进行哈希校验等。
考虑到磁盘 IO 的低效(及延长硬盘寿命),和减轻数据库等压力,我们应重视及运用 Buffer 和 Batch 机制,譬如若有持续、频繁的写文件之类需求,可以先将它们存入缓冲区(Buffer),每隔一小段时间再 Flush 到磁盘,再譬如若有频繁的更新数据表记录的需求,若时效要求不严,完全可以事务的方式作批量更新等。
有些端游、页游能绘制(鼠标所指)人物的轮廓,我也曾尝试写 shader 实现这种效果,但后来想到,以类似字体描边的做法不就可以实现么,而且写起来更简单,绘制引擎也能作 Batch Rendering 优化。
简单提一下字体描边的做法,譬如在 (x,y) 处绘制文字,那么只需在 (x, y-1)、(x+1,y-1)、(x+1,y)、(x+1,y+1)、(x,y+1)、(x-1,y+1)、(x-1,y)、(x-1,y-1) 处分别再以描边颜色绘制一遍该文字,即能展现所谓“描边”效果了。
网络游戏通常会对 AOI 作优化,而这些优化大多是数据结构和算法类的,譬如 MMORPG,可能有上百张地图,但很多时候,玩家大多集中在几个主城和副本地图里,但那些没有玩家的地图场景仍然会有 AOI 等,所以如果只对有玩家的地图作 AOI 等操作,应是能优化服务器的承载能力的。
对于第二个问题,可能 XOR(异或) 是个好方法。