Lua和C#交互开销探究

Lua和C#交互开销探究


前言

最近又看了一下ToLua相关的东西,终于稍微看明白了一点点,在此作下笔记。


过程

Lua每个用到的C# object都会分配一个ID与之对应,ObjectTranslator类起到关键作用。

ObjectTranslator译为对象翻译者,为什么作者会起这个名,原因就在于ObjectTranslator会把object与ID的匹配关系存起来。每次Lua需要调用C#的时候,最终都会转换成通过ID在ObjectTranslator中找对应object,然后调用object的对应方法。

举个例子:

假设我们是第一次调用:

obj.transform.position = pos

首先是obj.transform这一部分,本质上是调用了GameObjectWrap的get_transform方法。

static int get_transform(IntPtr L)
{
    object o = null;

    try
    {
        o = ToLua.ToObject(L, 1);
        UnityEngine.GameObject obj = (UnityEngine.GameObject)o;
        UnityEngine.Transform ret = obj.transform;
        ToLua.Push(L, ret); // 这里并不是把transform本身push到栈中,而是会生成一个userdata的结构,把这个userdata push到栈中
        return 1;
    }
    catch(Exception e)
    {
        return LuaDLL.toluaL_exception(L, e, o, "attempt to index transform on a nil value");
    }
}

先会把obj转换为已有的ID,之后查找dictionary获得obj本体。之后为transform分配ID并存入dictionary,并在Lua分配一个userdata记录ID以表示返回给Lua的transform(Lua这边实际上不会持有真实的object,而是持有一个table,这个table有着与object对应的方法)。这个userdata还会被设置metatable以使得可以调用obj.transform的各种方法。最后把这个userdata push入内存栈,即表示返回了transform。

之后transform.position = pos,本质上是调用了TransformWrap的set_position的方法。

static int set_position(IntPtr L)
{
    object o = null;

    try
    {
        o = ToLua.ToObject(L, 1);
        UnityEngine.Transform obj = (UnityEngine.Transform)o;
        UnityEngine.Vector3 arg0 = ToLua.ToVector3(L, 2);
        obj.position = arg0;
        return 0;
    }
    catch(Exception e)
    {
        return LuaDLL.toluaL_exception(L, e, o, "attempt to index position on a nil value");
    }
}

同理首先通过ID查dictionary获取transform本体。之后把传过来的参数(Lua自己的Vector3)通过LuaDLL.tolua_getvec3方法获得x、y、z三个值。最后transform.position = new Vector3(x, y, z)。


分析

通过上面可以看出Lua调用C#开销很大,所以基本上各种Lua框架都是利用缓存机制,用一个ID表示C#的对象,在C#中通过dictionary来对应ID和object。同时因为有了这个dictionary的引用,也保证了C#的object在Lua有引用的情况下不会被垃圾回收掉。说是说dictionary,但实际上如今的Lua框架都是自行维护一个存储映射关系的结构(ToLua用的就是List<PoolNode>),并采用了各种优化方式,如对象池等。

所以说,现如今说的Lua和C#交互开销大,大就大在每一步操作的取值、入栈、类型转换、内存分配、GC这些操作。如果不是第一次访问某个对象,那还好,只需要做个查找操作。但如果是第一次访问某个对象,那么上述的这些操作一个都不会少。

如果是只是临时的对象访问,那么在Lua GC后对对象userdata的引用就会释放,这也意味着后续的调用会导致该对象的userdata和ID映射关系又需要重新生成一遍。

比如说,obj.transform就是一个隐性的巨大陷阱,因为transform只是临时使用一下,很快就会被Lua释放掉,这就可能导致之后每次调用obj.transform又需要重新进行一次分配。无意中增加了很大的开销。

再比如,一个常见的场景,在Lua中临时new一个C#对象,然后当参数传给方法。这个临时new的对象由于没有被引用,导致后续GC的时候,userdata和ID映射关系被清除。如果需要频繁的new,那么就会增加非常多的开销。


方案

首先肯定要减少临时对象的访问,需要频繁使用的对象最好引用住。

其次就是减少对象传参。当然能不传对象最好不传,修改接口,这样连查找映射关系的时间都省了。但如果做不到,那传对象也可以,但是不要传临时的对象,最好引用住对象,减少频繁生成userdata和映射关系。

对于传参,不仅仅是上面说的对象传参会存在开销问题,Lua常用的bool、string、table传参也会有开销问题。bool和string这两个结构在C和C#中的内存表示不一样,意味着从C传递到C#时需要进行类型转换,降低性能,而且string还要考虑内存分配。table是Lua专有的数据结构,对应转换为数组的时候需要一个值一个值进行拷贝。由此可见,对于Lua频繁调用的C#函数,参数越少越好。

对于上述例子,一个简单的优化方案就是提供静态方法。

obj.transform.position = pos
-- 把上面代码改为下面代码
Utils.SetPos(obj, pos.x, pox.y, pos.z)
-- 如果自己维护一套id与object的映射,那么还可以优化为以下代码
Utils.SetPos(objId, pos.x, pox.y, pos.z)

这样既避免了transform临时对象,又省掉了Lua Vector3的转换。


内存泄漏

只要Lua对对象的userdata有引用,C#的映射关系也就会一直存在dictionary中。这就导致object一直无法被GC释放,这也就是Lua和C#交互导致内存泄漏的主要原因之一。

最常见的就是在一开始引用了某个component或者gameobject,然后一直没有释放引用。

对于这种问题也好解决,我们遍历一下映射关系就可以直接知道那些没有释放,然后去修改相应的代码即可。


小结

这里直接简单的探究一下Lua和C#交互到底开销在哪,对于Lua框架还有很多可以挖掘深究的地方。之后有时间继续研究。


参考

https://blog.uwa4d.com/archives/USparkle_Lua.html

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++与Lua交互可以通过Lua提供的C API实现。Lua提供了一组C函数,可以在C++中调用这些函数来实现与Lua交互。下面是一个简单的示例: 假设我们有一个Lua脚本文件test.lua,内容如下: ``` -- test.lua function add(a, b) return a + b end ``` 现在我们想在C++中调用这个add函数,可以按照以下步骤实现: 1. 在C++代码中引入Lua的头文件 ```c++ extern "C" { #include "lua.h" #include "lualib.h" #include "lauxlib.h" } ``` 2. 创建一个Lua虚拟机 ```c++ lua_State* L = luaL_newstate(); ``` 3. 加载Lua脚本文件 ```c++ int ret = luaL_dofile(L, "test.lua"); if (ret != LUA_OK) { const char* err = lua_tostring(L, -1); printf("Failed to load script: %s\n", err); lua_close(L); return 1; } ``` 4. 调用Lua函数 ```c++ lua_getglobal(L, "add"); lua_pushnumber(L, 1); lua_pushnumber(L, 2); int ret = lua_pcall(L, 2, 1, 0); if (ret != LUA_OK) { const char* err = lua_tostring(L, -1); printf("Failed to call function: %s\n", err); lua_close(L); return 1; } int result = lua_tonumber(L, -1); printf("Result: %d\n", result); ``` 上面的代码中,我们首先调用lua_getglobal函数获取Lua全局变量add,然后使用lua_pushnumber将两个参数1和2压入栈中,接着调用lua_pcall函数调用函数,并将返回值从栈中弹出,最后使用lua_tonumber获取函数返回值。 需要注意的是,Lua的栈是一个先进后出的数据结构,我们使用lua_push系列函数将数据压入栈中,使用lua_to系列函数获取栈中的数据,使用lua_pop函数将数据从栈中弹出。 总的来说,C++与Lua交互可以通过Lua提供的C API实现,需要使用一些基本的函数来操作Lua的栈和调用Lua函数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值