今天我们来聊聊如何跟Unity学代码优化,准确地说,是通过学习Unity的IL2CPP技术的优化策略,应用到我们的日常逻辑开发中。

做过Unity开发的同学想必对IL2CPP都很清楚,简单地说,IL2CPP就是Unity用来替代mono的一种script backend。至于说Unity为什么用IL2CPP替代mono,就是另外的话题了,本文就不细港了。

IL2CPP由两部分组成:

  • 一个AOT(ahead of time)compiler。完全用C#写的。

  • 一个VM runtime library。主体C++,外加部分平台特定的汇编代码。

IL2CPP AOT compiler的工作原理就如字面意思,读取并Parse (虽然并不知道用Mono.Cecil算不算Parse)IL Assembly ,分析并优化,然后生成cpp代码。IL2CPP的实现也很简单,生成的C++代码基本跟IL一一对应,有兴趣的同学可以自己试一下写点C#,然后看看生成的C++代码。

IL2CPP正式release已经有一年多了,一开始人人质疑,现在大家已经基本接受。这种转变肯定不是一日促成的,主要还是靠Unity对IL2CPP的重视和持续跟进的优化。

这两个月,Unity官博发了一个IL2CPP优化三部曲,接下来我们就看看如何从其中学习代码优化思路。


首先是第一个优化例子:

 public abstract class Animal {
public abstract string Speak();
} public class Cow : Animal {
public override string Speak() {
return "Moo";
}
} public class Pig : Animal {
public override string Speak() {
return "Oink";
}
} public class Farm: MonoBehaviour {
void Start () {
Animal[] animals = new Animal[] {new Cow(), new Pig()};
foreach (var animal in animals)
Debug.LogFormat("Some animal says '{0}'", animal.Speak()); var cow = new Cow();
Debug.LogFormat("The cow says '{0}'", cow.Speak());
}
}

这个是最教条主义的面向对象编程入门示例,很显然,从常识来思考的话,示例中的animal.Speak()是多态的,而cow.Speak()不是,前者会做一次virtual function call,而后者会做一次direct function call,两者的性能差距是一次虚函数表查询。

但是,IL2CPP实际上并不会这么做。IL2CPP的优化策略非常保守,而且为了实现简单,IL2CPP并不会在读IL指令的时候维护上下文状态。因此IL2CPP看到cow.Speak()没有办法判断cow的具体类型,保险起见,只能做一次虚函数表查询,也就是表现为virtual function call。

当然优化起来也很简单,程序员人肉加hint即可。而且这种hint方式我们在各种语言里都能见到,那就是给Cow的类型定义加一个sealed修饰符,问题终结。


优化一方面要跳过不需要的逻辑,另一方面还要简化无法跳过的逻辑。毕竟对于大多数情况,virtual function call的开销是逃不掉的。接下来,IL2CPP开发组又介绍了他们优化virtual function call的思路。

先看示例代码:

 class BaseClass {
public virtual string SayHello() {
return "Hello from base!";
}
} class GenericDerivedClass<T> : BaseClass {
public override string SayHello() {
return "Hello from derived!";
}
} public class VirtualInvokeExample : MonoBehaviour {
void Start () {
Debug.Log(MakeRuntimeBaseClass().SayHello());
} private BaseClass MakeRuntimeBaseClass() {
var derivedType = typeof(GenericDerivedClass<>).MakeGenericType(typeof(int));
return (BaseClass)FormatterServices.GetUninitializedObject(derivedType);
}
}

MakeRuntimeBaseClass().SayHello()这个坑相信大家刚接触Unity的时候都踩过,由于iOS平台不支持JIT compile method,这里如果不做hint,就会导致真机运行时crash。

IL2CPP的runtime library实现也类似,会在SayHello这个virtual function call的过程中查一次虚表,如果找不到调用方法,就会抛出一个托管的异常。

代码在这里:

 static inline void GetVirtualInvokeData(Il2CppMethodSlot slot, void* obj, VirtualInvokeData* invokeData) {
*invokeData = ((Il2CppObject*)obj)->klass->vtable[slot];
if (!invokeData->methodPtr)
RaiseExecutionEngineException(invokeData->method);
}

这里对于我们写逻辑的来说,其实真没什么可优化了。而且对于有指令级优化经验的程序员,会把这个机会交给CPU的branch prediction。

但是IL2CPP团队还是选择把这个if优化掉了。简单地说就是自己写了个stub method,然后vtable[slot]本来应该为null的情况都给指到stub method。

这样,虽然在极少数需要抛出异常的情况下,多了一次函数调用的开销,但是对于绝大多数情况,都省了一次if检查开销。

按IL2CPP官博的说法是,这个优化提高了3%到4%的表现,我们就姑且信之,淆习一个。


接下来是原博的第三个示例:

 interface HasSize {
int CalculateSize();
} struct Tree : HasSize {
private int years;
public Tree(int age) {
years = age;
} public int CalculateSize() {
return years*;
}
} public static int TotalSize<T>(params T[] things) where T : HasSize
{
var total = ;
for (var i = ; i < things.Length; ++i)
if (things[i] != null)
total += things[i].CalculateSize();
return total;
}

注意第21行中的things[i] != null,这里如果T具现为Tree类型,就会做一次装箱操作。

如果对代码生成有了解的同学,可能还会联想到generic sharing,也就是泛型函数具现为不同的引用类型时可以共享同一个方法实例,而具现为值类型时就会决议到不同的方法实例。

同时由于IL2CPP的AOT性质,编译期就已经知道了这些事情,所以IL2CPP完全可以把具现的每个值类型泛型函数实例特殊处理,去掉里面的装箱操作。

事实上,IL2CPP就是这么干的,也确实让程序员少操了不少心。


小结一下,以上优化技巧,我们应该如何在写逻辑的时候应用上?下面就逐条淆习一下:

  • 第一个例子中,IL2CPP借助编译期hint获得了额外的优化元信息。

针对这一点不太好列举写逻辑时候的应用情景,如果经常用可以给类型加注记或Attribute的语言(比如C#)可能会有类似的优化经验。

假设我们要开发一个非侵入式的序列化库,核心需求是把传进来的object序列化成字节流。

对于库来说,传进来的是一个未知的object,需要借助反射拿到类型元信息,然后动态生成序列化代码,以供之后的该类型object序列化使用。

这就跟JIT一样,相当于在每种类型的object第一次序列化的时候,库需要动态生成方法,这个成本相当高,不过好在可以之后摊还。但是对于有些服务端来说,这种随机的性能压力是不可忍受的。

因此我们可以hint住可能会序列化的类型定义,形成一种约束,规定程序员在运行时只能给库这些hint过的类型的object。

这样,序列化库初始化的时候一次性生成好这些类型的序列化函数,就能把不确定的消耗转化为确定的消耗,把运行时的消耗提前,提高整体的性能表现。

  • 第二个例子中,IL2CPP把nullcheck的极少数分支转为stub method,消除了nullcheck。

其实我们在写逻辑的时候,也不知不觉就会写出各种带if-elseif的恶心逻辑,这时候我们也可以用类似于stub method/stub class的方法,既能让代码变优雅,又能提高效率。

举个例子,我们有一个IServiceProvider,它会根据配置的不同实例化为不同的ServiceProvider。那么,一种设计是每个用到ServiceProvider的地方都checknull,另一种设计是让ServiceProvider一开始初始化为一个TrivialServiceProvider,后面该怎么用就怎么用。

其实两种设计并没有绝对的好坏之分,完全看IServiceProvider在逻辑中扮演什么角色。

如果IServiceProvider的接口并不具有默认值语义,那有可能第一种设计更适合你。但是相反的话,第二种比第一种更优雅,而且对于trivial占极少数情况的逻辑,还能获得额外的性能表现。

  • 第三个例子中,IL2CPP对可以优化的情况做了特殊处理。

这类例子就比较多了,比如redis的zset在元素少的时候会用ziplist,元素多的时候才改为skiplist等等。

最近开始在订阅号写文章了,觉得合适的会转过来博客。但是几番对比,发现订阅号的写文章体验完爆各种博客。

有兴趣的同学可以关注下订阅号「说给开发游戏的你」,下面是二维码。

跟Unity3D学代码优化的更多相关文章

  1. Unity3D编程回忆录,Unity3d视频教程,教父团队倾情之作

    之前一直在看Unity3d的视频教程,包括很多老外的视频教程,老外的教程确实不错,技术含量很高,而且讲得很激情,让我有种恨不得一秒钟就想吧unity3d学个精通的冲动,只是,毕竟是英语教程,没办法,哎 ...

  2. 01我为什么学Unity3d

    首发于游戏蛮牛论坛&&我的CSDN博客:http://blog.csdn.net/wowkk/article/details/18571055 转载请说明出处.谢谢. 本人现大学生,带 ...

  3. 新手学Unity3d的一些网站及相应学习路线

    一.unity3d有什么优势 如果您对开发游戏感兴趣,而又没有决定选择哪一个游戏引擎,别犹豫了 unity3d是一个很好的选择! 就我来看unity3d优势主要有以下几方面:首先部署简单,自带了一个I ...

  4. 学unity3d需要什么基础

    学unity3d需要什么基础?在游戏业发展如火如荼的情境下,很多人开始转行投身于游戏程序开发,而unity3D游戏开发则是他们必须了解和会用的游戏开发工具.在学习之前又应该了解哪些内容呢? unity ...

  5. unity3d代码优化标准

    转载自:https://blog.csdn.net/m0_37283423/article/details/84378384 代码优化 ● 尽可能使用for来代替foreach:每次foreach会产 ...

  6. 【跟我一起学Unity3D】做一个2D的90坦克大战之AI系统

    对于AI,我的初始想法非常easy,首先他要能动,而且是在地图里面动. 懂得撞墙后转弯,然后懂得射击,其它的没有了,基于这个想法,我首先创建了一个MyTank类,用于管理玩家的坦克的活动,然后创建AI ...

  7. 我跟着siki学Unity3D游戏开发——PongGame

    一.屏幕坐标转换为世界坐标. 1.游戏逻辑,根据界面布局,将墙体控制到对应的位置: vector3 position=Camer.main.ScreenToWorldPoint(new vetor2( ...

  8. 刚学unity3d,跟着仿作了flappy bird,记下一些琐碎的心得!

    1.关于场景,即scene. 一个正常的游戏至少要有三个场景,即菜单(或者文件夹)场景.游戏关卡场景.游戏结束场景.它们一般统一放在project文件夹下scene文件夹(自己创建)中,方便管理. 1 ...

  9. 【跟我一起学Unity3D】代码中分割图片而且载入帧序列动画

    在Cocos2dx中.对大图的处理已经封装好了一套自己的API,可是在Unity3D中貌似没有类似的API(好吧,实际上是有的,并且功能更强大),或者说我没找到. 只是这也在情理之中,毕竟Unity3 ...

随机推荐

  1. C#给PDF文档添加文本和图片页眉

    页眉常用于显示文档的附加信息,我们可以在页眉中插入文本或者图形,例如,页码.日期.公司徽标.文档标题.文件名或作者名等等.那么我们如何以编程的方式添加页眉呢?今天,这篇文章向大家分享如何使用了免费组件 ...

  2. C# DateTime日期格式化

    在C#中DateTime是一个包含日期.时间的类型,此类型通过ToString()转换为字符串时,可根据传入给Tostring()的参数转换为多种字符串格式. 目录 1. 分类 2. 制式类型 3. ...

  3. 如何进行python性能分析?

    在分析python代码性能瓶颈,但又不想修改源代码的时候,ipython shell以及第三方库提供了很多扩展工具,可以不用在代码里面加上统计性能的装饰器,也能很方便直观的分析代码性能.下面以我自己实 ...

  4. ASP.NET WebApi OWIN 实现 OAuth 2.0

    OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用. OAuth 允许用户提供一个令牌, ...

  5. iOS架构一个中型普通App的一些经验总结

    这一版比较完善的的App终于提交审核了.有时间写写自己的一些经验的总结了.自己主导的从0到比较成型的app到目前来说也只有两个,但是其中的很多东西都是大同小异.基本上是想到了什么就写什么,感觉写的不到 ...

  6. 微信小程序初探

    做为码农相信大家最近肯定都会听到微信小程序,虽然现阶段还没有正式开放注册,但大家可以还是可以开发测试. 到微信的WIKI(http://mp.weixin.qq.com/wiki?t=resource ...

  7. C#泛型详解(转)

    初步理解泛型: http://www.cnblogs.com/wilber2013/p/4291435.html 泛型中的类型约束和类型推断 http://www.cnblogs.com/wilber ...

  8. css样式之border-image

    border-image-source 属性设置边框的图片的路径[none | <image>] div { border: 20px solid #000; border-image-s ...

  9. 说说BPM数据表和日志表中几个状态字段的详细解释

    有个客户说需要根据这些字段的值作为判断条件做一些定制化需求,所以需要知道这些字段的名词解释,以及里面存储的值具体代表什么意思 我只好为你们整理奉上这些了! Open Work Sheet  0 Sav ...

  10. web.xml中load-on-startup的作用

    如下一段配置,熟悉DWR的再熟悉不过了:<servlet>   <servlet-name>dwr-invoker</servlet-name>   <ser ...