timing

问题描述

Unity 项目在实践中往往选择使用 Lua 作为更上层的逻辑脚本。这一方面是由于 Unity 本身对热更不是很友好,用 Lua 热更灵活得多,另一方面也是简化与服务器共享代码和数据。目前多种不同的 Unity + Lua 集成方案中,实践中采用比较多的是庞巍伟同学的 slua 方案。

使用 Lua 的团队,往往倾向于“较重的”集成,也就是暴露相当大规模的引擎接口给 Lua,这样逻辑上才能有足够的自由度对游戏和引擎做全面的控制。当 C#/Lua 之间的互操作接口迅速增长到成千上万的数量时,一个重要的问题就会浮现出来:C# 和 Lua 的交互层对引擎是封闭的,很多引擎内建的工具,没有办法跨越宿主 (C#) 和脚本 (Lua) 的边界。这些受到影响的机制里,最重要的就是 Unity Profiler。

Unity Profiler 是 Unity 提供的一个有力的性能分析工具 ,能够在优化阶段有效地帮助定位瓶颈,有时也容易借机发现一些潜藏的 bug。而当我们定位到某个 Lua 函数有较大的开销(CPU 或内存 GC Alloc)时,由于跨语言边界的影响被阻拦,就难以进一步观察更多的细节。

“正常的”做法

由于 Profiler 的 API 接口也一并暴露给了脚本,正常的做法是:根据 C# 这边的有问题的调用,翻到对应的 Lua 代码,把相关的脚本逻辑读一遍,为那些潜在的开销大的逻辑添加对应的性能剖析采样 Profiler.BeginSample()/EndSample(),来定位问题代码段落,然后再翻回对应 C# 函数,再在里面加上测试代码印证我们的想法。

实际上我们知道,大多数情况下(如果不算 bug),与引擎部分相比,逻辑脚本的 CPU 开销是相对比较低的(逻辑代码里以 if 判断居多,遇到需要循环的情况都非常少,一般用不到啥非常复杂的运算——或者说在设计得当的情况下,复杂的运算都会交给底层去整块整块地做),而容易造成困扰的托管内存分配导致 GC 卡顿的内存问题,也是完全由脚本调回来的 C# 代码造成的。

这样分析下来,往往绕一圈又回到 C# 里,中间付出了大量无谓的在脚本里兜圈子的时间不说,被逼着读和分析重复性高的脚本逻辑代码,也大大增加了精力和脑力的负担。

AOP

既然知道了反正总是要从 Lua 回到 C# 的,那么有没有什么简单的办法,一劳永逸地为所有暴露给 Lua 的 C# 接口加上性能剖析采样呢?

如果能做到这一点,我们就可以无视中间脚本层 (Lua) 的干扰,在 C# 环境内解决所有问题。

俺的目光很自然地投向了 AOP (Aspect Oriented Programming),这种技术能帮我们在不用修改目标函数代码的情况下,加入我们想执行的代码(就像 Python 的 decorator 那样)。

python-deco

经过一番研究,我成功地得出以下这条结论:

__现有的一些针对 C# 的 AOP 方法,在 Unity 的 mono 下,基本都跪了~~__

还能不能让俺过一个快乐的儿童节了~~


在这些尝试里,最接近成功的是:使用 lambda expression 包一层,添加相关代码后,再注册给 slua,然而,slua 需要为注册进来的函数添加下面的 attribute:

    [MonoPInvokeCallbackAttribute (typeof (xxx))]

而 C# 不支持为 lambda expression 添加 attribute,所以 &_& ……

利用 slua 代码生成的简明做法

发现难以通过 C# 本身的语言机制解决问题之后,我把目光投向了 slua:既然所有的绑定代码是 slua 生成的,那么不如直接修改生成代码,把采样代码生成到接口的绑定函数里~

找到普通函数接口的生成位置

void WriteFunctionImpl( StreamWriter file, MethodInfo m, Type t, BindingFlags bf)
{
    ...
}

在一头一尾添加了对应的生成代码(BeginSample() 的参数可以直接用 MethodInfo.Name 得到正确的函数名 ),运行 slua 的 Make 生成一下,得到下面的结果(单个函数):

[MonoPInvokeCallbackAttribute(typeof(xxx))]
static public int xxx(IntPtr l) {
    try {
        Profiler. BeginSample( "xxx");

        ...

        return 1;
    }
    catch( Exception e) {
        return error( l, e);
    }
    finally {
        Profiler. EndSample();
    }
}

EndSample() 在 finally 内,保证每个出口都能正确配对。


粒度控制

接下来更进一步,我们希望有某种粒度的控制能力,只为某个关心的类生成,甚至只为该类内关心的函数生成。回到前面的函数生成所在的方法,可以看到签名:

void WriteFunctionImpl( StreamWriter file, MethodInfo m, Type t, BindingFlags bf);

其中第二个参数可以用来筛选我们关心的函数(可以跟 m.Name 比较来过滤字符串),而第三个参数 Type t 可以用来筛选对应的类(通过 if ( t == typeof(TargetClass))),这样就可以只在我们需要的时候,为特定的类和函数生成了。


对应改动在这里 (<15行)。

Gu Lu
2016-06-02

Comments
Write a Comment

Tags

游戏开发   随笔   Programming   C/C++   优化   Unity   C++   知乎   游戏设计   比特币   Unity3D   区块链   软件开发   引擎设计   系统架构   Production   idtech   中国文化   加密货币   项目管理   Bitcoin   游戏评论   资源管理   资源流水线   效率   道德经   网络   方法论   模板编程   Lua   Oculus   GDC   渲染   VR   PerfAssist   Unity MemoryProfiler   BCH   经济学   信息过载   行业报告   字体   Productivity   图形   网络编程   Dice   协程   EMC   Premake   测试   中间件   Game Engine   新手引导   Methodology   CI   命令行解析   goroutine   ndk   Ethereum   nanomsg   自动化   Scripting   摘录   Debugging   同步技术   cppcon   C++模板   DOOM3   技术评估   Unity GC   C++11   学习方法   Surface Pro 3   Engine Evaluation   CRT   文化   Blockchain   笔记   golang   图形编程   多线程   ETH   Blockchain Computing   Bitcoin Cash   cppcon14   Bitcoin SV   Visual Studio   Unity Coroutine   跨语言可变参数列表   团队协作   货币   Deployment   Visual Assist   工程改进   Michael Abrash   exp   开放世界   域名   虚拟现实   系统重构   slua   遮挡剔除   完美转发   协作式调度   Modern C++   类型推导   Memory Debugging   个人成长   小故事   BTC   暴雪   产品   历史   错误处理   Unity Profiler   MOD  

知识共享许可协议
本作品由Gu Lu创作,采用知识共享Attribution-NonCommercial-NoDerivatives 4.0 国际许可协议进行许可。