优化

《游戏性能优化之路》制定目标(上)

  • 2.1 了解你的目标设备
    • 2.1.2以什么指标为依据
    • 2.1.3 帧率
    • 2.1.4卡顿
    • 2.1.5 功耗
    • 2.1.6 内存
    • 2.1.7环境兼容性
    • 2.1.8 其他
  • 2.2 根据实际情况选择你的目标设备
    • 2.2.1 Android & IOS
    • 2.2.2 PC平台
    • 2.2.3 其他平台
  • 2.3 目标地图(TODO)
  • 2.4 为你的游戏添加档位(TODO)

上一章我们快速过了一遍性能优化的整体过程。那么接下来我们来详细介绍一下性能优化每一个阶段当中的细节。我们先从制定计划开始。

2.1 了解你的目标设备

2.1.1 以什么指标为依据

万事万物皆有目标,目标都需要量化,那么我们到底需要什么样的指标才能帮助我们得到更好的性能呢?就以我的经历而言,选择指标这一项就走过不少的弯路。最早的时候我们只看内存和帧率,这两个其实是非常显而易见的指标,很多团队也会选择这两个指标作为源头指标。

回顾性能优化的本质,玩家在游玩游戏的时候可能会遇到什么问题:

  1. 玩游戏的时候发现卡顿,掉帧
  2. 玩游戏的时候发现持续帧率低
  3. 玩手机游戏的时候烫手,掉电快
  4. 玩游戏的时候发现闪退,提示内存不足
  5. 玩游戏的时候发现设备不支持

其中1、2、4可以通过帧率和内存的指标进行观察。

但是1、2虽然都是帧率但亦有不同。卡顿和持续低帧很明显是两个不一样的结果,很有可能出现卡顿多但是帧率还可以的情况,也就是只看FPS只能解决2并不能解决问题1。

然后我们再看第3点,同样FPS较高情况下并不能解决手机发烫的问题,也无法观测到对应的问题。

最后我们看第5点,虽然这个是一个很明显的兼容性相关的问题,但也可能与性能相关,例如我们是否需要支持更低的DX版本,高版本的显卡驱动意味着支持更新的特性。

下面我们逐个指标进行分析。

2.1.2 帧率 & 游戏循环耗时统计

首先我们需要定义什么是帧率。帧率通常意义讲得是一帧当中的帧数,统计FPS的方式有几种。

  • 统计一秒钟跑了多少帧,也就是字面意思上的FPS
  • 直接一秒除以当帧的耗时,但是这种方式往往得到的是每帧离散的数字

当然计算FPS的还有其他做法,每个项目计算FPS的方法并不一样,但最重要的是需要持续按照同一种方式来衡量性能。

但是仅仅帧率是不够的,在实际的游戏项目当中(主要是移动端)我们会进行帧率的限制,并且不会让逻辑吃完所有的时间片,否则可能会面临的就是发热和迅速的降频。通过统计游戏主循环的耗时(不包含等待帧结束的耗时)来对游戏开发过程当中的性能进行环比,以此来观察一段时间中游戏性能发生的变化,以此来推测游戏中是否新增了问题。

但需要注意的是,我们需要保证移动端尽可能频率一致。对于Android手机来说,可以通过一些Root手段进行锁频,但是IOS目前不提供任何的锁频手段。

  • 功耗过高导致发热产生降频,这个时候需要加上散热背夹,如果加了散热背夹也压不住,那可能得先进行更激进的优化了,一般情况下都是资产本身或者是bug导致的。
  • IOS和Android根据厂商不同会有性能模式,尽量保证ios和android在符合同样预期环境的情况下进行测试。在IOS的plist开启性能模式,Android根据需要手动调整,或者让厂商进行包名加白。
  • 另外我们需要让我们的任务优先级足够高,IOS有QOS,Android则有对应的系统接口绑核。

在清理完所有的误差之后,我们确保我们录制的指标万无一失。

2.1.3 卡顿

首先,什么是卡顿,卡顿实际上是人眼察觉到事物改变频率的变化。

那什么情况下人眼会察觉到卡顿呢。众所周知画面至少要大于24帧才能认为画面是连续的(例如早期的电影就是按照24帧来拍摄的),但是区分画面是否流畅的帧率就远不止24帧这么简单了,现在的60帧120帧刷新率人类依旧可以获得感知。由于视觉暂留的效应,人类的眼睛并不是按照帧率来记录世界而是会根据事物变化的频率留下持续的残影,以此带给人一种更连续的感觉。

但是从经验上来看,人类对卡顿的感知是远远强于对帧率本身的。

比如没有进行帧率限制,开了60帧,最终全局帧率只能跑到50帧不到,相比限制了30帧,但全局帧率基本维持在29以上这种情况来说,后者流畅度上会更好。所以卡顿问题是必须需要高优先级解决的问题。

WWDC18有一个非常经典的讲座:https://developer.apple.com/videos/play/wwdc2018/612/

卡顿产生的帧

卡顿的计算方法有很多,但是业界大量使用了perfdog的jank计算方式。

PerfDog的官网有一篇被大量引用的文章:https://perfdog.qq.com/article_detail?id=10162&issue_id=0&plat_id=1

perfdog的文章已经写了很多了,我在这边就不再赘述了.

Perfdog下定义卡顿的方式是:

  • 同时满足两条件,则认为是一次卡顿Jank.
    • FrameTime > 前三帧平均耗时2倍。
    • FrameTime>两帧电影帧耗时 (1000ms/24*2≈83.33ms)。
  • 同时满足两条件,则认为是一次严重卡顿BigJank.
    • Display FrameTime >前三帧平均耗时2倍。
    • Display FrameTime >三帧电影帧耗时(1000ms/24*3=125ms)。

2.1.4 功耗

然后我们来看一下如何定义手机烫手和掉电快的情况。那么显而易见,手机的功耗越高,那么手机掉电也就越快。

我可以看一下功耗的定义:

$P(功耗)=I(电流) * U(电压)$

$E(能耗)=P(功耗)*T(时间)$

在输入电压一定的情况下电流越高,功功耗也就越高。

功耗的问题在于测量,业界常见的功耗测试方式有这些:

  • 稳压电源+电流表
  • 系统接口,例如batteryStats
  • Android BatteryHistorian/功耗性能分析器

关于目标制定

当我遇到目标制定的问题时,第一时间也没想到很好的方法。相信很多人也会遇到,后面和其他项目交流之后会发现其实答案就在于我们的根目标上:我们希望多久手机不降频。

如果我们在恒温恒湿环境下,一定时间内不降频所需要的功耗,就是我们所需要的目标值。而且根据不同设备体质,这个值会不一样,所以一旦决定了用一台机器作为功耗测试机就应该一直用下去,直到发现电池体质明显下降之后再换新机循环往复。

稳压电流+电流表

现在大量公司都采用了电流表的手段来采集功耗数据,这需要直接拆机,并且有非常多的坑,需要有足够经验和比较好的动手能力。IOS由于没有提供任何的对外接口直接获取当前功耗,所以也只有这种方式能够真的拿到功耗,但是IOS26之后这个情况或许将有所改变。

XCode Power Profiler

在XCode26 苹果推出了PowerProfile用于解决这个问题。不过也存在几个问题:

  • 拿不到实际的功耗数据,只是一个相对值
  • 依旧无法锁频,当降频之后采集到的数据依旧是偏低的
  • 需要IOS26或以上的系统

但总之多一个手段总比少一个手段要好

Android BatteryHistorian/功耗性能分析器

对于相对旧的系统来说,BatteryHistorian是观察电池使用量的方式

https://developer.android.com/topic/performance/power/setup-battery-historian?hl=zh-cn

而在Android 10,也就是API等级29之后,就可以通过AndroidStudio中的功耗管理器(ODPM)来查看功耗数据了。

https://developer.android.com/studio/profile/power-profiler?hl=zh-cn

系统接口

通过Android的接口我们可以拿到当前的android电池信息。

Macrobenchmark也能得到一些电量变化信息。

第三方SDK

目前很多第三方手机或者芯片厂商也会提供功耗分析工具

每个工具的准确性都需要验证,实现也参次不齐,这里就不再赘述了

2.1.5 内存

在前面我们提到过,不同操作系统下的内存指标实际上是大相径庭的。所以知道内存需要以哪个指标为准是非常重要的。我见过非常多项目,即使上线性能开发人员仍然不明确AppMemory具体含义的情况。那么我们就逐个平台的来分析每个平台的内存特性以及什么才是该平台最合适的测量指标:

Android

Google官方对Android内部的管理有一些文档上的介绍,可以对内存的概览有一些帮助

https://developer.android.com/topic/performance/memory-overview?hl=zh-cn

总得来说Android的内存有以下特点

  • CPU、GPU共享同一个RAM
  • 内存分为了日常使用的RAM和用于压缩换出的zRAM。
  • 内存页面分为缓存页、匿名页,而每一个页又区分为dirty和clean。当页面刚分配的时候为clean,发生写入之后则标记为dirty。
    • 缓存页
      • 私有页:clean页往往可以通过swap out来增加可用内存,而dirty页则需要通过zRAM压缩换出的方式来减少。
      • 共享页:clean页可以swap out以增加内存,dirty页则swap或者明确msync或者munmap写回到storage中。
    • 匿名页
      • 没有Storage支持的内存则直接swap out到zRam进行压缩

如何统计Android内存呢:

Android会追踪每一个应用的页面使用情况。

内存使用会分为共享和独占的,如下图

常驻内存大小RSS(Resident Set Size):应用用到的所有共享页面和非共享页面的数量

均摊内存大小PSS(Proportional Set Size):应用使用到的非共享页面加上均摊计算之后的内存页数量

独占内存大小(Unique Set Size):独占的非共享页面的内存页数量

PSS在评估一个应用的占用当中非常有用,但是它的overhead非常高,不适合持续追踪使用,我的使用经验是采集以此需要1~2ms,而RSS比较快速,可以用来追踪实时数据。

Android会有LowMemoryKiller在内存吃紧的时候杀应用,相比ios,android杀应用的时机似乎比较模糊,而且和各个厂商的调教也有关系,所以我们很难明确定义Android应用应该使用多少内存比较合适,相反IOS会有一个相对明确(但也并不精确)的斩杀线,从经验上来看同档位Android机器的budget会定在比ios高1个G左右,实际的标准指定还是以当时的技术环境为准,我的经验只能说明过去的一些做法。

Android内存虽然一般分为Java堆内存和Native内存,但对游戏开发者而言我们一般会更关注Native部分内存,我们的绝大部分分配都不会进到Java堆中。

一般可以通过ActivityManager的接口获取完整的Memory信息。

IOS

https://developer.apple.com/videos/play/wwdc2018/416

WWDC18有一个经典的讲座,分享了ios的内存管理机制。

IOS的分页机制,其实和之前讲到的Android比较相似,一般16k为一页,Page同样分为Clean和Dirty。

Clean和Dirty的概念同安卓。

当ios内存不足时,会尝试将dirty内存进行压缩,塞到Compressed内存中,所以常见的memory分布如下

常见的CleanMemory有:文件、Framework之类的

DirtyMemory往往是需要在运行时操作的文件。

这个讲座里面也讲到了很多有用的Profile工具,但这个我们放到后面的章节再进行介绍。

除了上面讲到的虚拟内存机制外,ios在新版本中还提供了一个虚拟内存扩展的功能:

https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.kernel.extended-virtual-addressing

在部分大内存机型上可以进一步扩大可以使用的内存。

ios的上杀死进程的内存量相比android更明确一些,虽然没那么精确,但是通过测试我们可以拿到一个相近的范围。

例如3G手机,一般杀死应用的线在1.6G左右,4G手机则差不多在2.2G左右。这些数据网上有很多人会进行整理,我在这里就不在进行赘述了。

在应用程序中我们可以通过获取ios中的task_info中的physical_footprint 。

Windows

https://learn.microsoft.com/en-us/windows/win32/memory/about-memory-management

在微软的官网文档里面我们可以看到详细的关于内存管理的介绍。这篇文档介绍得非常详细,涉及到了 Windows 内存管理的方方面面,我在这里不再进行赘述。

简单来说内存换出机制比较接近传统意义上换出到磁盘上的思路,这一点是对应用程序透明的,并且用户可以设置工作集的大小来控制内存。

对于性能优化者来说,需要关注的是 Windows 的 WorkSetSize,通过 Windows 的系统函数GetProcessMemoryInfo就可以直接拿到对应的内存信息。详细信息在:

https://learn.microsoft.com/en-us/windows/win32/memory/memory-performance-information

其中性能优化需要着重关注的是 commited 总量,意味着我们实际用了多少内存。

2.1.6 环境兼容性

上面的指标虽然复杂,但是最终都是一个可以量化的数值,但是在这一节我们提到的硬件兼容性则其实是一个性能指标的前提,我的性能目标是在什么样的环境下达到的,我需要支持Win7吗?我需要支持多低的Android、IOS版本,这影响到了后面引擎feature的开发,如果当初定的系统版本过低,或者甚至需要支持OpenGL ES2,那后面的工作会各种束手束脚。

我们就来逐个平台分析,什么样的版本、配置要求才是适合我们的。

Android

  • Android 的 SDK 版本号
  • Android 的 NDK 版本号

这两个决定了支持的标准库,系统 API

  • 是否需要支持 Vulkan
  • 是否需要支持更旧版本的 OpenGL

IOS

  • 最低支持的 IOS 版本号
  • IOS SDK版本
  • 标准库版本

Windows

  • 最低支持的 Windows 版本
  • 最低支持的 DX 版本号

基建层面

C++需要支持什么版本,编译器类型(MSVC?Clang?)

C#的Runtime?目标框架?

Lua的版本?Luajit?Lua5.4?

对于图形API来说,首先需要确定是否需要支持现代 GPU API:DX12、Valkan、Metal

如果我们的目标用户往往有更好的设备,那么我们需要支持更先进的渲染技术,那么所需的图形API版本也就越新。

对于标准库、SDK版本、编译器版本这些

  • 一个是取决于你需要使用的第三方库的最低版本要求
  • 另外你是否需要更新的API来构建一个更高效的代码框架,新的库往往能够支持更强大的功能,而减少大量基建上的开发。比如C++的模版,在新版本中可以省去大量的复杂写法
  • 不同的编译器版本支持的优化选项也有所区别,以后可以拿出来写一写

总之,完全确定兼容性是一个繁琐的过程,并且在项目的发展过程当中很大概率会变,比较好的方式可能是先按照市面上常见软件的支持标准制定,后续根据需要再进行调整。例如微信支持的最低IOS版本。

2.1.7 其他

为什么这里有个其他呢,实际上,除了以上我们提到的这些问题以外,玩家还可能遇到另外的一些问题:

  • 包体过大,储存空间不够
  • 网络流量带宽过高,网络延迟过高
  • 下载量过大,每次需要下载巨量数据

这些问题根据游戏类型的不同,并不是所有游戏都会遇到,所以不着重在这里介绍了,在后面的章节当中,我或许会提到这些问题的解决方法,但是优先级相较于前面的这些点并不高。

包体优化目标

目录规范必须尽早确定。进包规则尽量清晰明确。

我们可以根据常见的设备容量大小以及应用商店的应用安装包要求来确定我们的目标。

例如一些超大型的开放世界游戏在项目的后期可能会遇到无法在128G设备上覆盖安装之类尴尬的问题。

网络优化目标

这个根据每台服务器流量费用大概估算,项目的对流量的预算反推出我们的流量优化目标。

网络优化本身也是一个非常大的话题不再这里赘述。

下载量问题

CDN也和上述同理。客户端的分包和增量更新管理也是一个值得探讨的学问,不过现在已经有非常多成熟的技术帮助我们达到这一点。

2.2根据具体情况选择你的目标设备

恭喜你,到目前为止,你已经了解了所有性能优化所需了解的源头性能指标,接下来可以定你的机型了!

既然要划分机型,我们首先需要知道,市面上的硬件有哪些。在这里我会列举一下目前市面上的主流机型(目前为止)。我们需要清楚的一点是,每一年硬件都在不断的更新,每一年的机型情况也不一样,所以即使在公司当中已经有了一套明确的目标机型标准,我们也不能陷入路径依赖,如果面向的是高端玩家,竞争的也是最新画质,那么不可避免地需要战未来,在硬件快速迭代的今天,最新最强的芯片在五六年后也会沦为低端机器,所以与时俱进是非常重要的。

以前在做项目的时候有人提出一个有趣的观点,每年都有大量的新玩家购置更新的手机,以及有一批老手机淘汰,所以即使不做任何的性能优化,大盘的数据从长时间维度来看永远是更好的!

当然这是在开玩笑,平常我们在拉取性能数据的时候总是需要拿到相同型号的设备、相同画质、相同环境的数据进行环比,所有上报的大盘数据可参考性几乎为0(拿来写ppt都随时会被challange。

2.2.1 Android & IOS

移动平台包含了Android和IOS,虽然都是移动平台,但是这两个平台却有着明显的差异。对于Android平台而言,内存往往比同档位对标的IOS机型内存更大,而IOS虽然内存小,但是在io效率、cpu能效比更有优势。

在PC平台,我们往往会说我们用的是什么CPU、用的是什么GPU、内存多大等,有明确的划分。但是对于移动平台来说,这些部分往往是组成在Soc中的,Soc相当于一组芯片组,其中包含了CPU、GPU、NPU以及信号基带。我们在评估Soc的时候大部分情况下基于实际的cpu、gpu而非soc本身进行判断,而不同的Soc以及芯片布局设计也会影响到实际的能耗比,比如骁龙888是业界知名的火龙,所以虽然它预期上有着很强的性能但是因为本身发热问题严重,实际表现会不及预期。

网上有大量的芯片天梯图,在这里我贴一个看上去比较清晰的,仅供参考:

https://www.mydrivers.com/zhuanti/tianti/01/index.html

目前Android阵营的芯片系列有:

  • 高通骁龙系列
  • 联发科 天玑系列
  • 华为麒麟系列
  • 三星猎户座系列

于此对应的AndroidSoc上的常见GPU有以下这些:

  • Mali
  • Adreno
  • Samsung Xclipse
  • PowerVR
  • Maleoon
  • NVIDIA Tegra

苹果目前的芯片主要就是A系列芯片

如果是一家成熟的公司,那么往往已经有几款上线产品,那么产品在线上的付费率、市占率就是确定最终目标芯片的最好参考。而通过实际测试确定最终能承载高中低端机的机型。

如果是一家新成立的公司,那么做好市场调研,则是一个避免不了的工作。

如果实在不知道应该怎么决定,先定3年前的最新设备作为一般用户设备可能会是一个比较好的临时方案,但最终我们还是以实事求是的态度去看目标机型这件事情,追求更高端的用户群体就往上提,追求更多受众就往下压。

2.2.2 PC平台

PC平台的硬件相对而言比较比较明确,cpu和gpu的厂商也就几家。Interl、AMD、英伟达。

同样,这里贴两个典型的天梯排名,仅供参考

https://www.mydrivers.com/zhuanti/tianti/gpu/index.html

https://www.mydrivers.com/zhuanti/tianti/cpu/index.html

我们需要先明确游戏本身是CPU Bound的还是GPU Bound的,比如说像重渲染、线性叙事的游戏往往GPU Bound更严重一些。而大世界游戏,或者是经营类的游戏例如纪元系列或者文明在CPU上的bound会更严重。

在选择PC平台的时候我们有几个衡量指标,CPU算力和显存的大小,有时候我们通过CPU的核心数来估计CPU实际的算力。

除此之后我们还需要确定我们默认用户使用的分辨率会是多少,以便后续有固定的分辨率进行环比以得到令人信服的性能数据。

最终定设备目标的方式还是和移动端一样,于此同时也可以参考一些来自第三方的数据,例如stream玩家显卡使用分布等数据。

还是那句话,实事求是,我们的项目追求的是什么。既要又要必然带来灾难。

2.2.3 其他平台

其他的平台,类似于Sony的PlayStation、微软的XBox、任天堂的Switch,因为设备种类少,所以不会存在复杂的设备选择问题,但是还是需要确定是否需要上某些旧的平台:根据不同的平台估计游戏未来开发成本来判断需不需要进行支持。例如PS4使用的是机械硬盘,游戏的io上会有巨大的瓶颈,在游戏的IO性能上需要做大量的工作,而PS5平台则没有这个问题。或者是PS平台接入的成本本身,底层的渲染框架也需要做大量适配,团队的技术水平和人员素质也是一个很重要的衡量指标。

后记:实际上我没有写到的东西还有非常非常多,包括如何保证功耗测试正确、CPU数据受到哪些外部因素影响。但是我想想,这些虽然在数据测试阶段都是需要解决的问题,但是目标制定阶段,我们先按下不表。认识到每一个指标本身的意义更重要。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注