Cpu优化大全 之 代码优化

前言
观察AppStore中游戏应用的评论,玩家对手机发热这一项评论的非常多。在玩家眼里这是游戏优化的不够好,太耗电。主要原因是CPU运算量大。这篇文章从两个方面介绍如何提高运算效率。其中“使用技巧”主要介绍在Unity研发中一些使用不当的方式,以及如何修正。“算法优化”项算是抛砖引玉,算法的优化总是无止境的,不同项目类型都有自己需求。

使用技巧
神奇的启动时间
在刚开发Unity项目时,遇到了一个非常棘手的问题。游戏启动时内存占用非常高(90M)。我使用二分法,排查是哪里分配的内存。但是结果令我非常的不解,因为当我用二分法,一直排除到程序启动至加载一个场景,一行代码都不执行,但App启动后内存占用缺还是很高(85M)。为了排除场景有未排除的代码, 新建了空白scene。 在google上搜索了好久关于启动内存高的问题,都没有得到答案。此刻只好怀疑到资源这块,通过删除Resources下的资源,神奇的事情发生了,启动内存降低了,自然启动速度就非常快。

直到看Unite 2006的开发者大会性能优化演讲,才看到Unity会根据Resources目录下的资源生产对应的Entity信息,在App启动时会加载所有的资源信息,资源越多,对应需要的内存越多,时间越久。这么重要的信息Unity官方尽然没有任何说明,这令我非常惊奇。

我这边的项目解决办法就是使用AssetBundle,Resources目录下放少量的资源,解决了问题。官方在Unite 2016大会上也是给出的相同的方案,希望能帮到大家。 

延迟解析
在项目设计过程中,我们经常将加载解析配置当做完整的模块,将所有配置文件加载并解析。由于许多模块都依赖配置文件,一般将配置文件启动时加载,当项目大的时候,会发现这里非常拖慢启动速度。这里将配置文件分为以下几类:
  1. 提前加载配置文件,启动时加载
    1. 尤其运行必须的配置文件,比如版本号,远端服务器地址
    2. 影响游戏启动后显示的配置,因为加载文件有卡顿的情况,所以放在loading时加载是值得的
  2. 所需及所求的文件,当使用时加载,解析,缓存
数组遍历
数组的遍历非常常见,尤其在使用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
// 这里只调用了 1次 Input.touches
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;          // 直接访问成员变量
     }
}
对于继承MonoBehaviour,常用的变量如果大量调用, 可定义成员变量指向。
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
// 这里会在ios上分配内存,如果非要使用,并且调用次数很多,做缓存
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;
}
建议使用TryGetValue。
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]();
     }
}
在一个对象里通过遍历所有对象的函数效率会比分别在每个对象里自己调用Update效率高很多

设置帧率
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)
{
 
}
开平方,那让我们看下传奇Quake代码里的开平方。
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;
}
总结
只有对自己对每一行代码的性能消耗胸有成竹,才能做到如庖丁解牛那般轻松。

你可能感兴趣的:(Unity性能分析及优化)