Cpu优化大全
发表于2016-10-27
前言
观察AppStore中游戏应用的评论,玩家对手机发热这一项评论的非常多。在玩家眼里这是游戏优化的不够好,太耗电。主要原因是CPU运算量大。这篇文章从两个方面介绍如何提高运算效率。其中“使用技巧”主要介绍在Unity研发中一些使用不当的方式,以及如何修正。“算法优化”项算是抛砖引玉,算法的优化总是无止境的,不同项目类型都有自己需求。
使用技巧
神奇的启动时间
在刚开发Unity项目时,遇到了一个非常棘手的问题。游戏启动时内存占用非常高(90M)。我使用二分法,排查是哪里分配的内存。但是结果令我非常的不解,因为当我用二分法,一直排除到程序启动至加载一个场景,一行代码都不执行,但App启动后内存占用缺还是很高(85M)。为了排除场景有未排除的代码, 新建了空白scene。 在google上搜索了好久关于启动内存高的问题,都没有得到答案。此刻只好怀疑到资源这块,通过删除Resources下的资源,神奇的事情发生了,启动内存降低了,自然启动速度就非常快。
直到看Unite 2006的开发者大会性能优化演讲,才看到Unity会根据Resources目录下的资源生产对应的Entity信息,在App启动时会加载所有的资源信息,资源越多,对应需要的内存越多,时间越久。这么重要的信息Unity官方尽然没有任何说明,这令我非常惊奇。
我这边的项目解决办法就是使用AssetBundle,Resources目录下放少量的资源,解决了问题。官方在Unite 2016大会上也是给出的相同的方案,希望能帮到大家。
延迟解析
在项目设计过程中,我们经常将加载解析配置当做完整的模块,将所有配置文件加载并解析。由于许多模块都依赖配置文件,一般将配置文件启动时加载,当项目大的时候,会发现这里非常拖慢启动速度。这里将配置文件分为以下几类:
- 提前加载配置文件,启动时加载
- 尤其运行必须的配置文件,比如版本号,远端服务器地址
- 影响游戏启动后显示的配置,因为加载文件有卡顿的情况,所以放在loading时加载是值得的
- 所需及所求的文件,当使用时加载,解析,缓存
数组遍历
数组的遍历非常常见,尤其在使用Unity Api是更加需要注意,Unity Api 返回数组每次都会返回新的数组,如果每次都访问数据,不仅分配了不必要的内存,还增加cpu调用次数。
1 2 3 4 5 6 7 8 9 10 11 12 13 | // bad // 这里调用了 n的2次方 Input.touches ,n = Input.touches.Length; for ( int i = 0; i < Input.touches.Length; ++i) { Touch touch = Input.touches[i]; } // good
Touch[] touchArr = Input.touches; int len = touchArr.Length; for ( int i = 0; i < len; ++i) { Touch touch = touchArr[i]; } |
内联函数
C#并没有C++的内联函数,在类内部使用时使用成员变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class Player { private int m_playerId; public int playerId { get { return m_playerId; } set { m_playerId = value; } } private void Update( int id) { // bad playerId = id; // 这里会调用 get 函数 // good m_playerId = id; // 直接访问成员变量 } } |
1 2 3 4 5 6 7 8 9 10 | private Transform m_transform; void Start() { m_transform = transform; } void Update() { m_transform.Translate( new Vector3(1, 1, 0), Space.World); // 大量调用时,减少调用链 } |
SendMessage()
此函数是用来向特定对象发送消息函数,由于会遍历对象所有节点,效率不高。通过监听者模式,效率更高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | static class MessageMgr { public static void RegisterMsg( string msgType, System.Action callBack) { } public static void DispatchMsg( string msgType) { } } void SendMsgToPlayer() { // bad SendMessage( "InitPlayer" , this ); // 会遍历整个Object的子节点,判断是否有InitPlayer方法 MessageMgr.DispatchMsg( "MsgInitPlayer" ); // 自定义消息管理, 定向发送消息,效率更高 } |
反射
反射在编译到IOS平台时,并没有明确说明转换过程,使用时需要测试方可放心使用,下面说说遇到一个问题。
1 2 3 |
System.Type t = System.Type.GetType( "Player" ); Player p = (Player)System.Activator.CreateInstance(t); p.playerId = 10001; |
携程
携程有效的分摊cpu 最大峰值。下面的例子介绍,如何分段加载游戏场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | void LoadLevel() { int level = 2; StartCoroutine(LoadLevelCoroutine(level)); } IEnumerator LoadLevelCoroutine( int level) { // load secene yield return new WaitForEndOfFrame(); // load ui yield return new WaitForEndOfFrame(); // load audio yield return new WaitForEndOfFrame(); // load actor yield return new WaitForSeconds(1.0f); } |
第三方库
使用效率高的库,在使用第三方库的时候,多做比较分析,得出最优的效率库。
1 2 3 4 | PlayerInfo litPlayerInfo = JsonMapper.ToObject(litJsonStr); // LitJson PlayerInfo unityJsonInfo = JsonUtility.FromJson(litJsonStr); // Unity 自带的JsonUtility 效率差很多倍 |
缓存
多使用缓存, 不要重复计算,下面的例子,一个是每次在使用数据时解析json,另一个是解析好后,每次调用直接使用缓存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | [System.Serializable] class PlayerConfig { public int id; public string name; } private PlayerConfig m_config; private string m_jsonStr; private PlayerConfig ParseConfig( string jsonStr) { return JsonUtility.FromJson(jsonStr); } private void InitConfig() { m_jsonStr = "{"id":1001, "name":"lfwu"}" ; m_config = ParseConfig(m_jsonStr); } private void Update() { // bad int id = ParseConfig(m_jsonStr).id; // good id = m_config.id; } |
字符串
字符串比较时,.net 涉及到语言相关项,会拖累速度,建议使用string.Equals,Compare的速度很慢, 因此在使用 string.IndexOf(), string.LastIndexOf() 需要注意语言相关问题。
1 2 3 4 5 6 7 8 9 10 11 12 | string str1 = "hello, world" ; string str2 = "hell0, wor1d" ; // bad int ret = 0; ret = string .Compare(str1, str2); // 语言相关,速度慢 ret = str1.CompareTo(str2); // 语言相关,速度慢 // good bool isSame = false ; isSame = str1.Equals(str2); isSame = str1.Equals(str2, System.StringComparison.Ordinal); // 速度快,按字符2进制比较 |
Dictionary
使用ContainsKey判断,在调用取值,调用了两次取值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Dictionary< int , string = "" > playerDic = new Dictionary< int , string = "" > { { 10001, "lfwu" }, { 10002, "xiaoy" } }; int id = 10001; if (playerDic.ContainsKey(id)) { return playerDic[id]; } else { return string .Empty; } |
1 2 3 4 5 6 7 8 9 10 11 | Dictionary< int , string = "" > playerDic = new Dictionary< int , string = "" > { { 10001, "lfwu" }, { 10002, "xiaoy" } }; int id = 10001; string ret = string .Empty; playerDic.TryGetValue(id, out ret); return ret; |
内置函数
在创建脚本继承MonoBehaviour时,删掉默认不用的函数,这些函数都会被调用。
1 2 3 | void Awake() {} void Start() {} void Update() {} |
Update
在戏开发中Update调用非常常见,Unity中类继承MonoBehaviour,会自动注册Update函数。
1 2 3 4 5 6 7 8 9 | private List m_updateList = new List(); private void Update() { int len = m_updateList.Count; for ( int i = 0; i < len; ++i) { m_updateList[i](); } } |
设置帧率
Unity默认帧率是60,针对不同的游戏可以设置不同的帧率,设置合理的帧率,提高性能,减少cpu调用次数。
1 | Application.targetFrameRate = Config.kFrameRate; |
算法优化
遇到被除数是2的时候,可以改为乘法运算。
1 2 3 4 5 | // bad float ret = dis / 2.0f; // good float ret = dis * 0.5f; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | Vector3 posA = new Vector3(1, 1, 1); Vector3 posB = new Vector3(2, 2, 2); // bad float minDis = 10.0f; float dis = Vector3.Magnitude(posA - posB); // 这里调用了开平方,效率比较低 if (dis < minDis) { } // good float sqrMinDis = 10.0f * 10.0f; float sqrDis = Vector3.SqrMagnitude(posA - posB); // 平方和,速度很快 if (sqrDis < sqrMinDis) { } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | float Q_rsqrt( float number ) { long i; float x2, y; const float threehalfs = 1.5F; x2 = number * 0.5F; y = number; i = * ( long * ) &y; // evil floating point bit level hacking i = 0x5f3759df - ( i >> 1 ); // what the fuck? y = * ( float * ) &i; y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration return y; } |
总结
只有对自己对每一行代码的性能消耗胸有成竹,才能做到如庖丁解牛那般轻松。