转自:http://gad.qq.com/article/detail/25645

前言

Unity下的C#GC Alloc(下面简称gc)是个大问题,而嵌入一个动态类型的Lua后,它们之间的交互很容易就产生gc,各种Lua方案也把这作为性能优化的重点。这些优化说穿了其实不复杂。

元凶在这里

先看看这两个函数

1
2
3
4
5
6
7
8
9
int inc1(int i)
{
    return i + 1;
}
 
object inc2(object o)
{
    return (int)o + 1;
}

window下实测inc1的性能是inc2的20倍!

差距为什么那么大?主要原因在其参数及返回的类型,inc2参数是object类型,意味着一个值类型传入(比如整数)需要boxing,具体一点就是在堆上申请一块内存,把类型信息,值拷贝进去,要使用的时候需要unboxing,也就是从刚刚那堆内存拷贝到栈上,等函数执行完毕后,这个堆内存被gc检测到已经没引用,释放该堆内存。

20倍差距是一个参数一个返回的情况,随着这样的参数加多,差距更大。而且更糟糕的是:GC比较难控制,Unity的手游项目,GC往往是卡顿的元凶。

目前所有lua方案针对lua和c#间交互的gc优化,或者值类型优化,其实都是在做一件事:避免inc2的情况

C#调用Lua避免inc2

Lua是一门动态类型语言,它的函数可以接受任意类型,任意个数的参数,返回值也是任意类型,任意个数。如果希望以一个通用接口去访问lua函数,情况会比inc2更糟糕:为了支持任意类型任意个数参数,我们可能得用可变参数;为了支持任意类型多返回值,这个接口可能需要返回一个object数组,而不是一个object。因而我们还多了两个数组要分配及释放。函数原型大致如下:

object[]Call(params object[] args)

因为以上原因,大多方案虽然都提供了这种方式(因为方便),但又不推荐使用。有的方案会提供无GC的用法,例如ulua如果要避免gc,得这么来:

1
2
3
4
5
6
var func = lua.GetFunction("inc");  
func.BeginPCall();
func.Push(123456);
func.PCall();
int num = (int)func.CheckNumber();
func.EndPCall();

思路是把lua的栈操作api暴露出来,一个个参数的压栈,调用完一个个返回值的取。这些压栈和取返回值的接口都是确定类型的,换句话也就是inc1的接口。

上面只是单参数,单返回值的情况,大多数情况代码会更繁琐。

而slua没有找到相关的方案。

xLua的解决办法的核心思想是:只要你告诉我要用什么参数调用,我帮你优化。

1
2
3
4
[CSharpCallLua]
public delegate int Inc(int i);
Inc func= luaenv.Global.Get("inc");
int num =  func(123456);

1、按你所需声明一个delegate,打上CSharpCallLua标签;

2、执行生成代码;

3、用Table的Get接口把inc函数映射到func委托;

4、接下来就可以愉快的使用这个delegate了。

多复杂的参数都是和上面一样:声明,获取,使用。仅比带gc的Call接口多了一步声明,使用上和Call接口一样简单,甚至处理返回值方面更简单些,而且还额外带来强类型检查的好处。

如果lua函数有多个返回值怎么办?

多返回值将会映射到C#的返回值以及各输出参数,从左往右一一映射。

除此之外,xLua还支持一个lua table映射到一个C# interface,对这个interface的属性访问会访问到lua table的对应字段,成员方法调用会调用到lua table里头的对应函数。同样的,无gc。

这是如何做到的呢?说起来也不复杂,以lua函数映射到c# delegate为例,xLua会对声明了CSharpCallLua的delegate生成一段代码,比如Inc的生成代码会类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
public int SystemInt32(int x)
{
    //...init
    LuaAPI.lua_getref(L, _Reference);
              
    LuaAPI.xlua_pushinteger(L, x);
    int __gen_error = LuaAPI.lua_pcall(L, 1, 1, err_func);
 
    //...error handle
    int __gen_ret = LuaAPI.xlua_tointeger(L, err_func + 1);
    LuaAPI.lua_settop(L, err_func - 1);
    return  __gen_ret;
}

Get方法返回的委托将会指向这个方法。从这段代码来看,和ulua无gc代码类似,不同的是,别人家得手写,而且由于xLua少了一层封装,直接调用Lua的api,应该也更高效些。

复杂值类型优化

从C#到lua的复杂对象传递说起

lua虚拟机,对于.net就是非托管代码,要传递对象过去,得解决几个问题:

1、lua使用该对象期间,该对象不能被gc;

2、如果非托管代码(lua)回调托管代码(c#),当回传该对象的引用时,应该正确找到对应的对象;

3、重复传递一个对象,在unmanaged code测的引用表示最好是一致的;

问题1和问题2 官方给的方案是pined对象,实测pined一个对象以及释放的性能大致和Dictionary的Set/Get相当,而问题1和问题2可以优化为数组操作,性能可以比Pined方案高4~5倍:接受一个对象,在一个数组上找到一个空位置放进去,返回数组的下标作为对象引用。通过链表组织空位置,空位置查找可以优化成O(1)操作,而通过引用找对象当然也是O(1)。

问题3没啥好的解决办法,用Dictionary建立对象到引用的索引。

复杂值类型的困境

C#一切都是对象,自然也包括值类型,也能沿用上面的方案,这功能上没问题,性能却遭遇了滑铁卢:

每一次值类型放入对象池(指的是前面一节中提到的为了解决3个问题而做的一套机制)中就会碰到inc2的情况,会boxing成一个新对象,还有入池的一系列操作。有人会问用pined方案会不会没这问题,其实是一样的,值类型是在栈上,而pined了之后要从栈转到堆上,栈转堆还是会有类似的过程:分配堆内存,拷贝,用完释放。

这问题比前面那问题影响面更广,只要C#往lua传递一个复杂值类型就会出现,比如普普通通的Vector3四则运算会产生大量的gc。

ulua和slua思路是一样的,对特定的几个U3D值类型(Vector2, Vector3,Vector4,Quaternion)做硬编码优化,以Vector3为例:

1、用lua重新实现了Vector3的所有方法;

2、C#的Vector3传入lua:是先在lua侧建一个luatable,把待传入Vector3的x,y,z设置为对应字段;设置该table的metatable为1的方法实现;

3、lua回传Vector3到C#:C#构建一个Vector3后,取出对应table的x,y,z字段赋值到Vector3;

xLua的复杂值类型优化

上面的优化存在一些问题:需要增加一种新的值类型十分困难,所以目前为止采用这种方案能支持的值类型手指头就能数得过来,用户自定义的struct就更不可能支持了,核心代码深度耦合这几个类型也是不合理的。还有个比较严重的问题:xLua作者比较抗拒硬编码这种行为。

让我们思考一下,ulua和slua那种优化能避免gc的本质是什么?还有简单值类型从C#传递到lua也没产生gc,原因是什么?

答案就是:值拷贝

ulua和slua的复杂值类型优化,从C#传递到lua本质上是把Vector3值拷贝到lua table,避免了入池进而避免了inc2;简单值类型也是,一个c#的int传入lua,也是直接把int值拷贝到lua的栈上。

明白了这个思路就开阔很多,xLua设计了一套新的值类型方案,只要一个struct里头只包含值类型,可嵌套struct,当然,要求被嵌套的struct也只包含值类型,该方法都适用。

原理也不复杂:

1、生成struct的值拷贝代码,用于把struct里头各字段拷贝到一块非托管内存(Pack函数),以及从非托管内存拷贝出各字段的值(UnPack函数);

2、c#传struct到lua:调用lua的api,申请一块userdata(对于c#来说是非托管代码),调用Pack把struct打包进去;

3、lua回传到给c#:调用UnPack解出struct;

4、struct的方法还是沿用c#原本的实现;

说穿了,就和pb类似,把c#的数据结构序列化到一块内存以及从内存反序列化回来。

先说这方案的缺点:

缺点源于这个方案调用struct的方法还是调用原来C#的实现。从lua经C语言,再经pinvoke调用到C#,这个适配的成本已经远远大于一些简单方法执行的开销。当然,xLua只是默认调用C#的实现,也不是必须的,xLua提供了不经过C#,在C就直接读取更改struct字段的API,比较勤快的童鞋利用这API,可以尝试把需要高性能的地方用Lua实现,这就避免了lua和C#间的适配成本。

PS一下:网上很流行的lua方案性能用例,用Vector3.Normalize来测试lua调用c#静态函数的性能,甚至Unity官方发的测评都用这个用例。由前面的分析可以知道,这是不对的,这些被测方案的Vector3.Normalize都仅在lua里头跑,压根没测试到“lua调用c#静态函数”。

这方案优点:

1、支持的struct类型宽泛的多,用户要做的事情也很简单,声明一下要生成代码即可(GCOptimize),之所以要声明,主要是避免生成代码过多;

2、相比table方案更省内存,只是struct的大小加上一个头部,而64位下空table就80字节+,实际测试Vector3的userdata方案的内存占用是table方案三分之一;

其它值类型GC优化

下面大多数优化都只在xLua有效,可以在其05_NoGc示例看到用法,生成代码后运行在profiler看你效果。

1、枚举类型传递无GC;

2、decimal不丢失精度而且无GC;

3、所有无GC的类型,它的数组访问没有GC,这个貌似大多数方案都做到;

4、能被GCOptimize优化的struct,在Lua可以直接传一个对应结构的table,无GC;

5、LuaTable提供一系列泛化Get/Set接口,可以传递值类型而无GC;

6、一个interface加入到CSharpCallLua后,可以用table来实现这个interface,通过这interface访问table无GC;

这些优化和前面介绍的两大思路一脉相承,可以通过源代码看其实现,这就不分析了。

Unity下XLua方案的各值类型GC优化深度剖析的更多相关文章

  1. NET中的引用类型和值类型 zt

    .NET中的类型分为值类型和引用类型,他们在内存布局,分配,相等性,赋值,存储以及一些其他的特性上有很多不同,这些不同将会直接影响到我们应用程序 的效率.本文视图对.NET 基础类型中的值类型和引用类 ...

  2. C#的两种类据类型:值类型和引用类型

    注:引用类型相等赋值是地址赋值,不是值赋值. 什么是值类型,什么是引用类型 概念:值类型直接存储其值,而引用类型存储对其值的引用.部署:托管堆上部署了所有引用类型. 引用类型:基类为Objcet 值类 ...

  3. 【转】C#详解值类型和引用类型区别

    通用类型系统 值类型 引用类型 值类型和引用类型在内存中的部署 1 数组 2 类型嵌套 辨明值类型和引用类型的使用场合 5 值类型和引用类型的区别小结   首先,什么是值类型,什么是引用类型? 在C# ...

  4. C#值类型以及默认值记录下

    C#的值类型有bool,byte,sbyte,decimal,double,float,int,uint,long,string等 如果我们擅长使用默认值,可以帮助我们减少带来赋值及代码编写. 比如我 ...

  5. 【Unity|C#】基础篇(2)——栈与堆、值类型与引用类型

    传送门:https://www.cnblogs.com/moonache/p/6008048.html [笔记] 图1:值类型与引用类型 存储方式 > 值类型:数据直接存在栈中 > 引用类 ...

  6. Unity下的开发框架--适应web和微端游戏异步资源请求的框架

    一.   内容简介: 1.   框架对Web与微端游戏特性的支持: Web和微端游戏最重要的特性是,资源是持续从服务器上即时下载下来的.而保证体验流畅的关键就是保证资源下载分散到持续的体验过程中,并保 ...

  7. 匹夫细说C#:可以为null的值类型,详解可空值类型

    首先祝大家中秋佳节快乐~ 0x00 前言 众所周知的一点是C#语言是一种强调类型的语言,而C#作为Unity3D中的游戏脚本主流语言,在我们的开发工作中能够驾驭好它的这个特点便十分重要.事实上,怎么强 ...

  8. 自定义值类型一定不要忘了重写Equals,否则性能和空间双双堪忧

    一:背景 1. 讲故事 曾今在项目中发现有同事自定义结构体的时候,居然没有重写Equals方法,比如下面这段代码: static void Main(string[] args) { var list ...

  9. C# - 值类型、引用类型&走出误区,容易错误的说法

    1. 值类型与引用类型小总结 1)对于引用类型的表达式(如一个变量),它的值是一个引用,而非对象. 2)引用就像URL,是允许你访问真实信息的一小片数据. 3)对于值类型的表达式,它的值是实际的数据. ...

随机推荐

  1. Kettle 数据抽取

    1.创建数据库连接 2.建立转换 3.指定源数据库和目标数据库的字段映射 一定要在「输出」中勾选「指定字段」,然后点按钮「Get All fields」,再「Enter mapping」,在弹出窗口映 ...

  2. 数据可视化——Matplotlib(1)

    导入相关模块 import matplotlib.pyplot as plt import pandas as pd import numpy as np 基本图表 散点图:scatter N = 1 ...

  3. 【Python】 \uxxxx转中文

    背景 写Python接口自动化过程中,使用到邮件发送测试结果详情,邮件呈现出来的内容为 \uxxxx ,不是中文 接收到的邮件内容: 成功: 110 失败: 1 失败的用例如下 : [(u'\u752 ...

  4. no crontab for root 解决方案

    root用户下 输入 crontab -l 显示 no crontab for root  例如: [root@localhost ~]# crontab -l no crontab for root ...

  5. puma(5300✨) Rails的一个多线程,高并发处理的web server

    https://github.com/puma/puma 在开发和产品环境下,Puma是一个简单,快速,多线程,并高并发highly concurrent HTTP1.1server for Ruby ...

  6. 第五章 如何使用Burp Target

    Burp Target 组件主要包含站点地图.目标域.Target 工具三部分组成,他们帮助渗透测试人员更好地了解目标应用的整体状况.当前的工作涉及哪些目标域.分析可能存在的攻击面等信息,下面我们就分 ...

  7. sql 多个字段分组,删除重复记录,保留ID最小的一条

    IF OBJECT_ID('cardDetail') IS NOT NULL DROP TABLE cardDetail CREATE TABLE cardDetail ( id ,) PRIMARY ...

  8. nyoj——297(期望)

    GoroSort 时间限制:3000 ms  |  内存限制:65535 KB 难度:4   描述 Goro has 4 arms. Goro is very strong. You don't me ...

  9. IOS UI-控制器的创建和控制器的View的创建

    一.控制器的创建和控制器的View的创建 说明:控制器有三种创建方式,下面一一进行说明. 一.第一种创建方式(使用代码直接创建) 1.创建一个空的IOS项目. 2.为项目添加一个控制器类. 3.直接在 ...

  10. halcon中你不知道的标定板细节

    本人文着重阐述以下问题: halcon是否只能使用halcon专用的标定板? halcon标定板如何生成? halcon标定板如何摆放,拍照数量有无限制? halcon是否只能使用halcon专用的标 ...