本篇关注程序性能优化。聚焦这个主题,本是偶然。始于玩笑,终于本心。本想找点高大上的让人直呼牛逼的东西,奈何能力有限,只能给大家一些既便宜、又好用、还简单的普通东西了,不知道你们会不会喜欢。 分为五个主题,分别是『池』『序』『分』『减』『并』:
一、『池』字诀
池化,降低可重用对象的创建和回收代价。 不知道你们发现没有,无论是电影还是游戏中,主角总是孤胆单英雄,最多三五成群。但Boss不一样,Boss手一挥,必须有一群小怪一拥而上,毕竟帮主角刷点经验也是好的。 小怪的特点是:数量多,容易死,循环用。电影不可能请太多的群演,因此我们经常能发现一人分饰多角的超级龙套。而游戏里,也不可能每一个小怪都完全不一样,因为创建它们还挺消耗时间和内存的。哈哈,现在你知道了,你正在打的小怪很可能与刚刚死前掉金币的那只是同一只。 代码中,如果某些对象有重用的价值,并且创建的时候会消耗大量的CPU或IO资源。那么在出现性能瓶颈的时候,一个合理的优化方向就是池化。刚刚例子中,对游戏中小怪进行池化,通常称为对象池,类似的还有线程池、连接池等。 灰太狼:我一定会回来的~
二、『序』字诀
顺序读写,减少随机IO,减少cache miss。 『内存顺序读写的性能要远好于随机读写』,『磁盘顺序读写的性能要远好于随机读写』。类似的话我相信很多程序员都似曾相识,然而,我同样相信很多程序员在写代码的时候从来没有认真考虑过这件事件。 当年做游戏的时候,我见过很多人使用hash表存储场景中的各类对象:花鸟鱼虫、白云苍狗,并每帧遍历hash表以确定位置或攻击信息。我建议他们改成遍历有序表的快照(MVCC了解一下),其中一个原因是可以提高遍历性能,而另一个更重要的原因是可以在遍历的过程中修改表结构(插入删除对象)。 有序表一种基于有序数组的字典结构,C#中有一个名为SortList的标准库实现 多年以来,CPU一直是计算机中跑得最快的部件,也因此被惯出来一个多吃多占的毛病。无论是读内存还是读磁盘,从来不讲究按需分配,而是大块大块的拿数据。当把一大块连续的数据拿到手里的时候,CPU自己也知道一次肯定吞不下,但总觉得多吞几次就好了。 但这个特点其实是需要程序员去配合的,如果代码使用连续内存的数据结构,比如array,那在遍历的时候就相当于投其所好;而如果代码使用hash表,则遍历的时候cache miss的可能性就大大增加。 鉴于java在服务器领域的成功,有些公司使用java开发游戏服务器,我建议他们换一种语言。原因是游戏服务器很可能需要处理大量跟点、向量等三维空间相关的运算,而在java中,默认一切都是对象。于是在一个Vertex(顶点)数组中,看似连续的Vertex对象在物理内存中实际上是离散的,这样的遍历效果就会差很多。而对C++, Golang,甚至C#这类语言来说,它们都支持struct。当把Vertex定义为struct的时候,一个Vertex数组的占用的内存就是连续的,遍历效果会比java好很多。 在后端的技术栈中,kafka绝对是一个非常另类的存在。擅长kafka的人,觉得无它不欢,而不了解它的人,则觉得可有可无。关于kafka为什么这么快的讨论有很多,其中有一个绕不过的原因是:kafka会顺序读写磁盘。我们通常认为磁盘的读写性能远低于内存,但实际上,在关闭fsync的前提下,SSD固态硬盘的顺序读写速度与内存的随机读写速度是相当的,大概都是1 GiB/s,而如果是随机读取,则SSD固态硬盘SSD的Seek速度会直降到70MiB/s,速度下降到1/15。
三、『分』字诀
鸿蒙伊始,民智不开。为教化万民,神器计算机降世。下凡走得急,零件没凑齐。计算机天生残疾,CPU与其它IO部件速度差异过大。为平衡这种速度差异,人间有智者布局各方,分字诀应劫而生,它们是分批、分帧、分页、分时、分片、分区、分库、分表、分离。
1. 分批≈缓存+缓冲
在系统设计之初,每次修改对应一次IO可能是最简单、最直接的设计,不过随着系统流量变大,IO可能会很快成为系统瓶颈。相比于内存操作来说,磁盘IO吞吐小,网络IO延迟大。为了减少IO与内存之间的速度差异,争取每次IO都物尽其用才是上上之选。将多次IO合并为一次,减少磁盘IO(特别是fsync)的次数和网络IO的round-trip次数。 所谓读优化靠缓存,写优化靠缓冲,而分批≈缓存+缓冲。往小了说,缓存就是一块用于读内存,缓冲就是一块用于写的内存。往大了说,缓存就是Redis,缓冲就是Kafka。这些都是在微服务体系中司空见惯的招数,不用我多说。 分批优化化身千万,可谓无孔不入,甚至多数流行的数据中间件都提供了批量IO操作的API:
-
MySQL的insert语句支持在values中一次插入多行数据。
Redis的Pipeline操作可以批量执行多个操作。
ElasticSearch有专门的_bulk api支持在单次调用中索引或删除多条数据。
Kafka更是把分批操作贯彻到了极致:它根本就不提供发送单条消息的功能,使用send()发送的单条消息其实会被Kafka偷偷在内存里攒起来,是的你被骗了。
包括MySQL, Redis在内的各类数据中间件,跟WAL日志落盘时机相关的配置中,一定有一条是只写Page Cache,然后后台线程定期刷盘的策略。(注:严格说来,Redis那个不能叫WAL,因为它是写后日志。)
在游戏引擎中,把顶点数据提交的显卡的Draw Call也都是合并批次的,否则会卡死你。
分批的缺点是在数据一致性差,无论是缓存还是缓冲都存在同样的问题。缓存的话,如果不存在严格的一致保障手段,往往只建议展示用,不作为数据修改依据。
转载请注明:IT运维空间 » 运维技术 » 这五个字,能优化你80%的程序性能问题
发表评论