直接在 cs 里调用 luaL_error 真的没问题么? #14

Closed
cloudwu opened this Issue Jan 4, 2017 · 88 comments

Projects

None yet

10 participants

@cloudwu
cloudwu commented Jan 4, 2017

https://github.com/Tencent/xLua/blob/master/Assets/XLua/Src/StaticLuaCallbacks.cs#L98 类似这种。

这相当于绕过了 mono ,直接 unwind 了 stack frame 。

而 mono 自己 raise exception 需要 unwind stack frame 的时候,可是做了许多额外工作的。

@chexiongsheng
Collaborator

没有直接调,这个luaL_error是C#实现:
public static int luaL_error(IntPtr L, string message) //[-0, +1, m]
{
xlua_csharp_str_error(L, message);
return 0;
}

而xlua_csharp_str_error是C实现,只是设置标志位lua_upvalueindex(2)
LUALIB_API int xlua_csharp_str_error(lua_State* L, const char* msg)
{
lua_pushboolean(L, 1);
lua_replace(L, lua_upvalueindex(2));
lua_pushstring(L, msg);
return 1;
}

而C#的函数指针是包装过的,检查到有异常,在C代码那调用真正的lua_error抛异常
static int csharp_function_wrap(lua_State *L) {
lua_CFunction fn = (lua_CFunction)lua_tocfunction(L, lua_upvalueindex(1));
int ret = fn(L);

if (lua_toboolean(L, lua_upvalueindex(2)))
{
    lua_pushboolean(L, 0);
    lua_replace(L, lua_upvalueindex(2));
    return lua_error(L);
}

if (lua_gethook(L)) {
	call_ret_hook(L);
}

return ret;

}

@cloudwu
cloudwu commented Jan 4, 2017

https://github.com/Tencent/xLua/blob/master/Assets/XLua/Src/StaticLuaCallbacks.cs#L201

这里 LuaAPI.lua_pushstring 里间接调用了 lua_error 呢?

@chexiongsheng
Collaborator

这只在内存不足的时候抛异常,如果是lua53的话,代表系统内存没了。发生概率小,好像也没特别好的处理方式。你有啥建议呢?

@chexiongsheng
Collaborator

或者封装下吧,在C层面catch

@chexiongsheng
Collaborator

谢谢指正

@cloudwu
cloudwu commented Jan 4, 2017

m 类型如果不考虑的话也说得过去,不过 lua_getfield 是 e 类型的异常必须捕获 https://github.com/Tencent/xLua/blob/3b99c8459ada782d12dd952502e04fa0075a289b/build/xlua.c#L62

btw, m 类型异常不光是 oom ,还有可能因为 __gc 里的 error 触发。

@chexiongsheng
Collaborator

lua_getfield, lua_setfield都处理了,e类型都统一处理过,如果发现遗漏,不吝指教。
m类型我再统一处理下。

@cloudwu
cloudwu commented Jan 4, 2017

若从 c# 调用 lua 函数,一个完备的方案是在 c# 里把函数及所有参数 marshal 成一个 c 结构,这个过程不应调用任何 lua api;在 c 函数里将这个 c 结构 unmarshal 到 lua state 中;这个 unmarshal 过程用 lua_pcall 调用。

@chexiongsheng
Collaborator

lua_getglobal和lua_setglobal是有遗漏,不过用的地方不多。
你说的那种方式性能不太理想。

@cloudwu
cloudwu commented Jan 4, 2017

正确性高于性能,且未必有性能问题。性能应该从实现方法上推敲去改善,而不应利用不完备的设计来提升。

@cloudwu
cloudwu commented Jan 4, 2017

另外,上面提到的方案更简洁,跨越两种语言的 api 更少。

通常 正确性第一,其次是简洁性,它可以为正确性提供更多保障;最后才应该考虑性能。甚至性能好坏只应该是一种副产品,而不应喧宾夺主。

@chexiongsheng
Collaborator

推敲下,应该性能也没有大问题,可以后续验证下。
但api应该没怎么减少,pushxxx和toxxx在lua调C#还是会用到的。
至于正确性,现有方案也不见得不能保证:把所有会抛lua异常的api都封装好,在C#层面处理好异常返回。

@chexiongsheng
Collaborator

况且lua调用C#,这些api的封装也势在必行
除非lua调C#也改为marshal,unmarshal,那这样直接改为lua和C#之间用RPC来通讯好了。

@cloudwu
cloudwu commented Jan 4, 2017

反向也是一样的,完备性上讲, lua 调用的 c# 函数同样要处理 c# raise 的 exception .

@cloudwu
cloudwu commented Jan 4, 2017

ffi 交互 api 最终只需要有两个,从一种语言中传递一个双方都能解析的 c struct 让对方解析它做对应的操作。

@chexiongsheng
Collaborator
chexiongsheng commented Jan 4, 2017 edited

c#异常已经处理,就开篇那种方式。
c#调用lua还是先保留现有实现,会统一对所有产生异常的lua api封装为用返回值来表示异常,之前把e类型大多都处理了,现在会处理掉m类型。
marshal,unmarshal方案目前能想到的问题是对方的对象引用持有:可能lua测执行的过程中,c#的对象已经释放,反之亦然。目前lua对c#的引用管理是通过userdata的gc加上c#的对象池,c#对lua的引用通过C#构造,析构的lua_ref和lua_unref。
性能还是会有区别的,il2cpp的pinvoke其实只是普通c函数调用,而相比之下,marshal,unmarshal至少多了打解包开销。

@michaelpengcn

看到云风大神在这里讨论,我顿时对这个xlua感兴趣了

@caiguangwen1

云大在给这个项目涨粉。。。

@cloudwu
cloudwu commented Jan 4, 2017 edited

marshal 不一定要编码成字节流,它只是不同语言间数据交换的一种方法。本质上,调用 lua_pushstring 等也是在做 marshaling ,只不过是对单个对象做而已。以上提出方案是建议把单次交互的所有需要交换数据一起做 marshaling ,这其实是提供给具体优化更大的余地。

跨语言对象持有的生命期问题是一个独立问题,应该和跨语言调用分解后独立处理。

userdata 的 __gc 方法是一个很重量的东西,而且不可控因素很多。比如在 xLua 目前的实现中,lua 中的 __gc 回饶回 c# 中的 LuaGC 方法,这又涉及跨越不同语言信息交换,这是在做 FFI 时应尽量减少的行为。另外 __gc 发生的时机不可控,复杂的 __gc 方法会影响正常业务流程的时间开销估计。

把这个问题正交分离单独解决的方法是:

在做 marshaling 的期间,所有 C# 对象进入 marshaling 流程后都放到一个用于保持生命期的集合(处于 mono 虚拟机)中,并传递 id (可从集合中索引)到 lua 。

lua 在做 unmarshal 的时候,把 id 关联到一个 table ,table 中可记录更多 C# 对象的类型信息。这个关联关系放在 lua vm 的全局弱表中即可。lua 业务层引用这个 table 相当于引用 C# 对象。传递回 C# 只需要编码 id。

lua 在走完一轮 gc 后,遍历出弱表中哪些 id 对应的 table 消失,可以统计出哪些 C# 对象不再被引用。把不再引用的 id 交还给 mono ,然后 mono 里给出一个 api 根据需要清理它们。这个过程只有交换 id 的过程是涉及语言间数据交换的。

C# 引用 lua 对象的处理方法是一致的,只是换一种语言实现。

@chexiongsheng
Collaborator
chexiongsheng commented Jan 4, 2017 edited

“这其实是提供给具体优化更大的余地。”

如果mono的pinvoke开销不好评估,如果il2cpp下,直接push的方式,仅仅相当于你在pcall里头做的事情的一部分。
以静态int add(int a, int b)为例,目前方式:
lua_pushnumber(L, a);
lua_pushnumber(L, b)

marshal/unmarshal方案是这样的
1、先marshal
marshal(buff, a) %
marshal(buff, b) %

2、在pcall里头unmarshal
unmarshal(buff, &a) %
unmarshal(buff, &b) %
lua_pushnumber(L, a); *
lua_pushnumber(L, b) *

你说的优化只是优化%标识的语句,总不能优化没吧?
而现在方案开销只对应*号的语句。
另外,还没考虑指示调用哪个lua函数的marshal/unmarshal呢。

ps一下,il2cpp是大势所趋,android下也是。

@dualface
dualface commented Jan 4, 2017

在 mono 对象被 lua 引用时就标记为不可 gc,我认为会有问题:

  • lua 没有做 gc 前,被 lua 引用的 mono 对象也不会被 gc
  • lua 自动 gc 的时机只和 lua vm 的状态有关,和 mono 的内存消耗无关

我个人认为比较合理的做法是:

  • mono 对象和 lua 的 gc 不要关联
  • mono 对象 gc 时,将 lua vm 中的对应值赋值为 null 即可
@cloudwu
cloudwu commented Jan 4, 2017

mono 和 lua 是两个非常独立的模块,首先应该考虑设计层面的模块间减少耦合度,提高内聚性,再考虑实现细节的优化。设计做简单了,实现优化甚至是可有可无的东西。

提高内聚性后,交互变成批量处理的东西,在理论上存在更大的优化空间。比如聚合要处理的批量数据后(把要传递的参数聚合在一起),对 cache 更友好。

lua_pushstring 这种要保证异常处理的完备,每次调用都是要检查的,而聚合在一起后,是通过 lua 自己的 error 机制统一处理例外情况,而不需要逐个做判断。

总的来说,在设计层面考虑问题,不要陷入不必要的优化细节考虑上。

@cloudwu
cloudwu commented Jan 4, 2017

lua gc 有它自身的完备性,不应该用各种使用约定来约束对象生命期。lua gc 本身也是完全可控的,lua 也提供了足够丰富的控制手段。

@cloudwu
cloudwu commented Jan 4, 2017 edited

FFI 本身就是非常重量的事情,Lua 和 C 之间的交互也属于 FFI 。lua mailling list 很多年前作者就强调过,如果你需要从 lua 中调用 C ,那么要做的就必须是件重量的事情。运行开销的很大比例若花在跨语言交互上本身就是有问题的。

所以 FFI 的设计关键是要简单,如果只是想提高交互效率,lua 和 C 交互的 api 本身就有很大的改进空间。扩展个 opcode 会比用标准 api 去交互高效的多。

@liyonghelpme

控制一帧里面跨语言交互的次数,然后重点优化频繁调用的改成批处理,这样既简单也和谐友爱

@chexiongsheng
Collaborator
chexiongsheng commented Jan 5, 2017 edited

一个个问题来分析
一、调用方式
正确性:marshal方案固然有保障,但全面封装异常也可以,如果全面有保证的话。
性能:
marshal方案多了marshal/unmarshal(包括调用及返回,有两次marshal/unmarshal),但好处是只有一个pcall
全面封装异常会对每个m类型的压栈/出栈api会有一次pcall开销,像lua_pushnumber这类不需要。
光推敲,很难得出哪个更好,但相信也不会有什么质的差别。
复杂度:
我十分赞同你说的设计要简单的观点。但我没能看出引入一套考虑了函数(名或者引用)以及各种类型参数的marshal/unmarshal方案会比对几个m类型api封装要简单。
二、对象生命周期管理
感觉用弱表来跟踪C#对象引用是个好想法,可以验证下。但也不一定要把对象映射到lua table,一个空lua table 就有80字节左右,用userdata更合适,不挂__gc就可以了。弱表跟踪userdata同样适用。
两个互无感知的GC间的对象引用是个麻烦事,比如环引用就无解。更彻底的做法是C#实现lua,对的,就是你们之前的UniLua,只是性能不知道能做到什么程度,感觉il2cpp下有可能性能问题能解决。
xLua v1的时候有集成UniLua作为WebPlayer的兼容,不过由于一直没有WebPlayer的项目,在V2重构时去掉了。

ffi确实重,项目主要的性能瓶颈往往在这。

推热补丁其实一个目的是希望尽量降低这影响
1、lua只出现在打补丁的时候,正常逻辑下不涉及;
2、约束了使用场景:如果是Stateless方式,热更的是一般是纯逻辑,不涉及状态(当然要涉及也可以),那对象生命周期管理的负担有望减轻;而Stateful方式的状态管理也是自动化的,一个对象对应一个lua table,如果你设置了的话,对象析构系统自动化解除关联,避免了代码不严谨导致的环引用。

@cloudwu
cloudwu commented Jan 5, 2017 edited

简单指的是尽量引入最少的东西。

比如,只用 table 能解决的问题,就不要引入 userdata ;如果能只向 mono 导入 2,3 个 C API 解决的问题,就不要入导入十几或几十个 C API 。

这些都是耦合度层面的设计考虑,把类型处理、生命期管理放在模块内部是从提高内聚性方面考虑的。和 pinvoke/pcall 的代价到底有多大无关。如果初期开发原型,直接把数据都打包成 json 就好了,C# 和 lua 都有成熟的 json 库,然后慢慢再来改进。可以这样分步来完成实现,就是低耦合高内聚的体现。

从使用角度上讲,userdata 只能携带 C 层面的数据,往往在具体实现时要考虑绑定一些 lua 层面的对象。比如、封装的 C# 对象需要有类型名字方便调试。这些名字是以 string 对象的形式呈现的,如果把 string 放在 userdata 的 C 内存里,会增加跨语言的耦合度,通常是在构造跨语言引用对象时,放在 lua vm 中的。如果采用 userdata ,就需要额外设置它的 uservalue ;这样就不如直接使用一个 table 。

而且 table 对 lua 代码本身有更大的可操控性,方便直接用 lua 写更多逻辑。

总的来说,就是莫要执着在性能上。性能只是正确设计的副产品。

ps. mono 对象和 lua 对象相互 1:1 对应关系,是不会成环的。用不着用 C# 去实现 lua 或是用 lua 实现 C# 。UniLua 的开发动机是需要在 web 浏览器里使用 u3d 的标准控件来跑 lua 代码;失去这个需求后,我们再也没用过它。

@cloudwu
cloudwu commented Jan 5, 2017

另外,lua 的 function 是 first class 的;作为一个完整的 ffi 方案,怎么把一个 lua 里的 object , 包括function / table / thread 的引用传递给 c# 是首要考虑的事情。

@cloudwu
cloudwu commented Jan 5, 2017

我下午随便写了一段代码演示怎么把 C# 里一组变量一次 marshal 到 C 库里,只需要 C 里面实现一个函数作一次 p/invoke 就够了。这里的 marshal 过程是没有太多开销的。

https://github.com/cloudwu/csbridge

C# 不是很熟,应该还有优化余地。

在这个基础上改一下,就可以进一步去调到 lua vm 里去了。有空我也写个 xxlua 用用。

@dualface
dualface commented Jan 5, 2017

总的来说还是同意云大的看法,设计上先保持清晰,才能有更大的优化空间。也看了一下 csbridge 的实现,这种方式是很清晰简洁,后期优化空间也大,相比直接 push 参数到 stack,应该没多大区别。

不过目前的实现只适合简单的值类型,对于 function/table/thread 等类型,处理起来就麻烦多了。

举个例子:

lua 传入一个 function 给 mono,mono 会在后续调用这个 lua funciton。

lua function 虽然在 lua 里是 first class,但在 c 里并不能简单的转换为一个值,更不能直接转为一个 mono value。

我的做法是:

定义一个全局 lua table,每当有 lua function 要传入时。就生成一个 lua function id,然后把 id 和 lua function 关联起来。也就是 lua table[id] = lua function。

id 是一个 integer,自然就可以很方便的传递给 c/mono 了。

当 mono 需要 call 这个 lua function 时,给出 id 和参数就行了。 bridge(假设 bridge 负责 mono/lua 间的交互)根据 id 查找 lua function,push 到 stack 里,然后再 push 参数并 pcall。

不过这种做法要注意的就是当 mono 不再需要之前传入的 lua function 时,得记得从 lua table 里删除 id,不然 lua function 会无法 gc。

不知道有没有更好的解决方案?

@cloudwu
cloudwu commented Jan 5, 2017

lua 对象(包括 funtion)传给 C# 引用是必须解决的问题,而且设计后面函数调用的设计,这个今天来不及做了,明天我把它补上。

csbridge 第一版实现(见最早的 commit),同事看过提意见说 string 放在那个结构里会导致 marshal 的时候复制,增加 p/invoke 的成本;所以我后来的版本增加了一个 api 把有 string 和没有 string 的情况分开处理了。

这里还存在一个优化点,可以把一部分常用的 string 同步到 lua vm 中(通常是短字符串)。然后在第一次调用后,以后全部只传 string id ,不用在 p/invoke 的时候再装箱拆箱。有空我会把这个优化加上。

@chexiongsheng
Collaborator

@cloudwu 这是一段循环引用的测试

using UnityEngine;
using System.Collections;
using XLua;

public class CRTest
{
public LuaTable Tbl;
public CRTest(LuaTable tbl)
{
tbl.Set("csobj", this);
Tbl = tbl;
}
}

public class CircularReference : MonoBehaviour {
LuaEnv luaenv = new LuaEnv();

// Use this for initialization
void Start () {

}

// Update is called once per frame
void Update () {
    for (int i = 0; i < 1000; i++)
    {
        CRTest crt = new CRTest(luaenv.NewTable());
        //crt.Tbl = null; //打开注释之后内存就不泄漏,或者这句换成crt.Tbl.Dispose();也可以
    }

    System.GC.Collect();
    System.GC.WaitForPendingFinalizers();
    luaenv.FullGc();
    luaenv.Tick();
}

}

@chexiongsheng
Collaborator

lua gc是Mark & Sweep,mono的sgen也是。
如果对象都在一个gc里头,循环引用是没问题的。
但分别在两个不同的gc管理器,问题就来了:
CRTest 对象被lua里头的一个table引用了,它没法释放,而lua的table被CRTest 对象引用了,也没法释放。Mark & Sweep是在一个图里头标识出从root出发不可达的部分,如果这个图能够囊括所有对象,循环引用是不会造成问题的,但问题是对于lua或者mono,这个图都不全。

@cloudwu
cloudwu commented Jan 5, 2017

这是一个设计错误,正确设计生命期管理是没有这个问题的。今天我没空写代码,明天会补充到 csbridge 里。

核心问题是:一个 A 虚拟机里的对象,把引用传递到 B 虚拟机里,该怎么作对?ps. @dualface 提到到 function 引用的问题是这个的子问题, mono 需要引用 lua 函数,lua 其实也需要引用 mono 函数。所谓 lua 调用 mono 里的 C# 方法本质上就是发送了一个 mono 函数类和参数,申请 mono 运行它;和 mono 调用 lua 的过程是一致的。

由于 mono 和 lua 在这个问题上是对称的,只是拥有的语言特性有点不同,所以下面我只讨论一边,即,怎么把一个 mono 对象的引用传递给 lua 。

mono 中应该有一个集合 T 。同时 Lua 中有两个集合,一个弱表集合 C ,一个 id 集合 L 。

当一个 mono 对象被创建出来,第一次需要传递给 lua 时,应该为它申请一个新的 id ,id:object 放入 T ,id:weak 放入 C 。之后将 id 传递给 lua ,随后 lua 应该把 id 放在 C 和 L ,生成一个 lua object (内含 id )给 lua 业务引用。

当 lua 的业务不再引用某个 id 后,这个 id 会在 C 中消失,定期比较 C 和 L 可以找到曾经传递给 lua 却不再引用的 id 集,把这个集交给 mono 。

mono 收到 lua 传递过来的不再引用的 id 集后,把这些 id 从 T 中删除,等待 mono gc 去回收。


反过来不写了,mono 可以用 weak reference 来模拟弱表。

@chexiongsheng
Collaborator
chexiongsheng commented Jan 6, 2017 edited

@cloudwu 其实目前xLua的对象引用实现和你说的是类似的。
C#有个ObjectPool的类,就是对应你说的T集合,保存的是id:object映射,在lua测有个弱表,对应你的C,lua object在xLua是用userdata。回收思路不一样,xLua是gc回调,而你这里是定期扫描弱表,但在对象引用这块达成的效果是一致的:T的释放依赖于lua测不再引用那id。反向也是类似,思路都是引用对方对象时,在对方那增加个引用,当且仅当本方没有使用时,释放增加的那个引用。
但还是会出现环的问题,你可以在你的模型推导一下,当两个对象分别属于不同gc管理,又互相引用的时候,会释放么?哪个会先释放呢?
我觉得根因不是出在实现上的问题,而是没法满足mark&swap算法的一个必要条件:能遍历整个网络。它虽然也能遍历T集合,但T集合实际上是另一个gc系统建立的虚引用,它无法进一步检查这些虚引用的来源。
我想到的解决思路,如果还是mark&swap算法,那这算法囊括的对象得是所有的.net和lua对象。

@cloudwu
cloudwu commented Jan 6, 2017 edited

对于 A B 个对象分处两个虚拟机,又相互引用的情况,可以这样处理:

A 发现所在自己所在虚拟机中已经没有引用,仅被 B 的虚拟机引用时,通知 B 告知自己这边已经无引用。但并不把自己实体删除。由于 A 自身已经没有引用,所以只有 B 重新把引用传回来时才会重建(利用实体)。

我今天正在依此实现,看晚上能不能做完。

@chexiongsheng
Collaborator

这也很难啊,拿mono gc来说
T集合对于mono gc来说就普通对象而已,它怎么知道这是来自别的虚拟机的引用保持机制呢?除非你修改mono gc,对T集合特殊处理,发现某某类名,就认为这是来自unmanager的引用。

@cloudwu
cloudwu commented Jan 6, 2017

lua 侧是完全没有问题的,做起来非常简单。因为 lua 有 ephemeron table 。在 ephemeron table 的 value __gc 中复活即可。

C# 不熟,我需要找找有没有类似机制。

@cloudwu
cloudwu commented Jan 6, 2017

C# 侧不用解决,因为只要一边能处理对,循环就被打破了。

@cloudwu
cloudwu commented Jan 6, 2017

我来讲讲循环引用是怎么解开的:

如果语言具有一种能力,当一个对象没有引用了后,可以感知到这点,但是又允许不删除这个对象,暂时保留起来。那么,解决这个问题就非常简单。

比如,在 mono 一边有个对象 A 它引用了 lua 中的对象 B 而 B 也反向引用回 A,那么本质上, mono 中有两个对象 A 和 B' ,lua 中有 B 和 A' 。这里 A' 是 A 的代理, B' 是 B 的代理。

如果 mono 发现它自身已经不再引用 A 了,无论是否 lua 中是否还存在 A' ,它都可以把 A 转移到一个第三方位置(或者 A 内部有一个计数器 +1 )。这时 A 是没有被删除的,如果 lua 把 A' 发送回来,就将 A 状态复原即可。

随后,lua 如果发现自己已经不再引用 A' 了,也通知这个第三方(或再将 A 的内部计数器 +1 )。一旦第三方收到两次 A ,那么 A 就可以真的删除了。


lua 是非常容易做到这点的,如果不想侵入对象本身的实现,可以加一个壳利用 ephemeron table 来实现。

当 lua 中 B 创建过 B' 传递给 mono 时,为 B 创建一个壳对象 { B } ,放置 B -> { B } 到 ephemeron table 中。一旦 B 在自身中没有引用,就在 { B } 的 __gc 方法中把 B 复活,放在一个独立表(第三方)内。

只要 B' 在 mono 中也确认被删除,才真的移除;否则,一旦发现 mono 重新通过 B' 访问 B ,就在 ephemeron table 中重新构造引用,并在独立表中删除 B ,等待重复上面的过程。


C# 我不太熟悉,不知道有没有方法做这样的复活处理,比如为传递去 lua 的 C# 对象都创建一个壳,在壳对象的 finalizer 中是否能做同样的事情我不太确定。

不过没有也没太大关系,等吃完饭我写写,如何通过 lua 的单边特性把循环引用解开。

@chexiongsheng
Collaborator
chexiongsheng commented Jan 6, 2017 edited

“只要 B' 在 mono 中也确认被删除”
这没法确认呢,因为B还没真正删除,所以引用了A‘,进而引用了A,进而引用了B’
或者你说可以在B的壳的__gc那清除A'的引用,但貌似不行
1、我的例子只是最简单的情况,实际上可能是很错综复杂的一个链引用到A‘;
2、你也没法确认该释放哪个字段,你统一清空成空table也不行,因为A那随时可能调用回来,你清了这就没法用了。
3、我那例子也是个简单的环,实际可能是一个错综复杂的局域网。

@cloudwu
cloudwu commented Jan 6, 2017 edited

所谓没办法确认是指 mono 缺乏 lua 对应的手段,同样的事情在 lua 里很容易做(确认自己内部没有引用,只有外部引用)。不过没关系,有一边能做就足够了。

在写怎么做前,先说说我们公司目前是怎么做这个事情的。

吃饭前咨询了我公司做 某lua 的同事,对,每个用 u3d 开发的公司都会顺手搞个 某lua ,反正 C# 不受待见,都想用 lua 做开发。

我公司这个不是我做的,他们用的也没啥问题,不存在循环引用的问题。这是因为,他们认为,如果你用 lua 开发业务逻辑,其实只有 lua 引用 C# 的需求,不应该存在 C# 长期引用 lua 对象的需求。

只要是自己主动调用清理垃圾的过程,而 C# 短期引用 lua 对象只要不超过两次清理的边界就不会有问题。而 C# 引用 lua 中的对象也仅仅只有函数而已,函数是获取后就调用的,不必长期持有。封装层是通过弱引用持有 lua 的函数,如果企图长期引用,那么后面会抛出异常。单向引用自然没有这么些啰嗦的事,我个人认为这个约定是非常合理的,违反约定也能立刻发现。


接着上单边解循环的方法,其实也就多一个步骤就可以了。

当 lua 发现自身已经对若干对象没有任何引用,这些对象包括自身构造的,以及 mono 推送过来的远程对象代理,在这里对象的 __gc 方法中(自身构造的对象用上面提到的非侵入方案,远程代理对象可统一加 __gc ),把这批对象复活放到一个叫做 坟场 的 table 中。这个 table 是强引用。同时通知 mono ,我已经不再持有这批对象了。由于坟场是强引用,所以对象并不会真的删除。但是 lua vm 其它部分对其没有任何关联。

mono 方此时会解除这批对象的外部引用。以之前的简单例子而言,如果 lua 方解除了 A' 和 B ,这个时候 mono 可以正确处理。更复杂的间接引用也是正确的。

之后有三种情况:

  1. mono 方依旧有在使用 A ,如果它不把 A 或 B' 发送给 lua ,那么会一直相安无事。因为 A' 和 B 都在 lua 的坟场里,并没有删除。

  2. mono 把 A 或 B' 中的任何一个发送给 lua ,lua 发现它们在坟场中,那么应该先把坟场中的所有对象都还原到 cache 里,并清光坟场同时通知 mono 加回它们的引用。然后再从 cache 索引出 A 或 B' 。坟场中的所有对象都应该认为之间有可能有潜在关联的,需要共同进出。这里,可以考虑按 gc 批次生成多干独立坟场,相互不干扰;用一个坟场也可以,只不过一些无关的对象会反复进出而已。

  3. mono 发现它可以回收 A 或 B' 了。因为 A' 和 B 之前就被告之解除了引用,由 mono 单方面持有,不存在循环引用的问题。这个时候,mono 已经释放了对象,它接下来的责任就是通知 lua ,我真的删除了某对象。lua 收到删除命令,检查坟场,如果在坟场,就从坟场把对应对象清掉;如果坟场没有,则可能在去坟场的路上,把 id 记录下来,任何对象进坟场的时刻都应该检查这个 id 删除集合,阻止进入即可。


方案总结:

任何有可能存在在 mono lua 双方的对象,无论是由哪方构造,都必须先由 lua 方发起删除请求,mono 方确认后才可真正删除。

lua 方发起删除请求后,对象进入 lua 坟场,mono 方有权利从坟场将对象复活,复活会取消删除请求。

mono 方收到删除请求后,才可以真正删除对象,它先从自身删除,再给 lua 方确认。一旦被 mono 确认的删除指令,lua 最终一定会执行删除。

@cloudwu
cloudwu commented Jan 6, 2017

具体实现时,需要注意 lua 弱表里的对象在 __gc 复活这个过程有中间态,处于中间态的对象没有常规 api 可以访问的到。即对象已经被从 weak table 清扫出去,__gc 方法尚未调用,所以还没有进入坟场。如果在这个时候需要拿回对象,应该主动调用 gc 等待。

关于中间态的讨论,我 blog 上有一篇: http://blog.codingnow.com/2016/11/cache_data.html

@chexiongsheng
Collaborator

还是会有问题,我想到的就有两个严重问题
1、像这种语句gameobject.Tramsform.Position=xxx,会有很多临时对象,这种对于lua都是用过抛弃型对象,都会产生一个代理,在cache和坟场间跑来跑去。最重要的是,如果c#那是长期对象,那代理将永不释放,一直跑来跑去。
2、更为可怕的是,由于cache和坟场间所有对象是同进同出,这类语句又是大概率事件,只要碰到一下就导致上一个gc周期进坟场的对象集体回炉,C#侧引用重新加回,这情况导致的结果我觉得会是长期有大批对象没法释放,而且由于有1问题,每次回炉都会成本巨大。

@cloudwu
cloudwu commented Jan 6, 2017

想的太多啦。

  1. 进坟场的周期是以 lua gc 循环为准的,临时用一下,即使立刻不再引用,也是要等整个gc 结束后才处理的,没什么跑来跑去的。所谓跑来跑去的频率最高不超过 gc 循环。

  2. 多个坟场的优化我前面提过了,可以隔离不相关的对象。但由于1 , 我认为意义不大。

  3. 需要交换的对象数量没那么多,整理过程在一个c函数里实现,是o(n) 的。我估计n最多在千这个级别,且是主动调用的。

  4. 方案只是把事情做对,然后在根据使用需要,指定最佳使用方法,比如避免临时引用。但由于前面所述我依然觉得没有问题。

@cloudwu
cloudwu commented Jan 6, 2017

另外注意,任何一批新进坟场的对象,如果仅仅包含代理对象,不含任何本地对象时,可以认为这批对象可以直接删除。两边按这个约定去优化可以减少很多操作。

@cloudwu
cloudwu commented Jan 6, 2017

由于实际使用时,绝大多数都是 lua 引用mono 对象,极少传递临时对象(自己把引用扔掉)去mono ,所以上面的优化流程会覆盖实际使用的绝大多数情况。

@chexiongsheng
Collaborator

我说的临时对象指的是c#在lua的代理,比如C#调用Lua,参数是一个c#对象,比如gameobject,执行完不会保存,这就是临时对象,在函数执行过程中访问了gameobject任何一个非基本类型,又有临时对象。这样的临时对象怎么防止?这类代理必然是进入cache-坟场。如果这类代理对应的c#对象是永久的,那代理就不会释放,每个gc周期都会来回跑一趟。
而且由于这类操作比较频繁,导致的结果是还没来得及解环,就触发集体回迁cache

@chexiongsheng
Collaborator

不一定是谁调谁,只要lua访问过的C#对象,没有显式的保存它的引用下来,就是临时对象。这怎么防,要求都要显式保存到lua?

@cloudwu
cloudwu commented Jan 6, 2017

你应该把这件事情看成在一个完整的 gc 循环基础上为了解开循环额外做的事情,到底做了多少,应该和一个完整gc 循环做的事情做对比,然后再把整个 gc 的额外代价和所有在 lua vm 里做的操作做对比,推算出实际的开销比。

gc 整轮的复杂度是 O(n1) , 坟场处理的复杂度是 O(n2) 。显然这里的 n1 是超过 n2 几个数量级的。

上面我补充了, lua 里有大量 c# 代理对象无所谓,只要 lua 尽量不传递自己不再引用的临时对象到 c# 就可以避免。一旦单次 gc 结束后,没有发现有传递到c# 的lua 对象不见了。再多代理都可以直接扔掉,不用进坟场推迟删除。

@chexiongsheng
Collaborator

问题是这些代理的二次访问会重新激活坟场所有对象,导致上一周期白做了。
举个很实在的例子,一个cube的不断旋转,其中一种方式是每帧调用这个gameobject下的transform对象的rotate方法,假如我在lua里头直接gameobject.transform:rotate(xxx),那至少每帧二次访问transform这个对象,由于没保存引用,就是临时的

@cloudwu
cloudwu commented Jan 6, 2017

没什么所谓白做的,如果一个周期内这些对象并没有真的删除,那就是刷了一遍 cache 而已,比你扔掉重建成本小。

如果其中一些真临时对象两边都删了,那么这些临时集合就变小了。

真正每轮 gc 多做的事情就是把你曾访问过的临时对象的临时代理搬个家而已。本来你访问过一次就可能访问多次,并不浪费什么。

一旦某轮gc 没发现有传出去的本地对象消失,就集体清除了。如果做多坟场分离,发生的几率更大。

但这都不是问题,一轮 gc 下来,实际要对所有活着的对象做一点操作;而复活坟场只是对你的临时代理对象每个做一点操作。显然临时代理对象的总数是远小于所有对象总数的,这才是关键。

写这么多,不是应该写出来看看?怎么看也就是几个小时的代码啊。

@chexiongsheng
Collaborator
chexiongsheng commented Jan 7, 2017 edited

C#中只要没释放的对象,在lua那访问过,就会有代理。按这实现的设定,这代理的释放是依赖C#对象的释放的,所以访问过的永久C#对象,就一直不释放。感觉这代理的数量不少,进而导致其中某个被二次访问的可能性大大增加。

以每帧gameobject.transform:rotate(xxx)为例,刚完成一个gc周期,包括transform在内的代理,以及环相关的对象因为没被其它lua对象引用被移动到坟场。
C#可能还没来得及检查及解环(我说的白做指的是这,毕竟,这方案就是为了解环),一个帧又到了,二次访问了transform,全部集体移回cache了。

在脑海推敲都过不了的代码还是先别写吧。推敲过得了的代码还可能碰到现实中意料不到的问题呢。

这方案想法是很妙,但目前为止感觉带来的问题比它解决的问题要多。毕竟你们用的某lua肯定也有这坑,但也没给你们带来什么麻烦,是吧?

@cloudwu
cloudwu commented Jan 7, 2017 edited

你做一个 gc 循环要 mark 几万个对象,顺带拷贝几百个对象又算的上什么呢。把它看成针对外部数据做二次 mark 就好了。一个 gc 循环期间新增加的对象中,如果没有传递到 C# 中去的 lua 临时对象,而 lua 自己释放掉了的,不会走坟场流程。

你有多少把一个 lua 临时对象传到 mono 里,lua 自己不引用的情况?这种东西多了,项目也别做了吧?

你当真考察过你的项目多长时间做一整轮 gc ? 一帧做一次?你的理解中,如果 lua 把一个对象解引用,lua 就马上会回收它么?这是个错误的理解。

对于频繁临时使用 C# 对象,能长期保留其代理对象,而不是清理掉然后创建新的,其实是好事而不是坏事。

解不开是因为 mono 的确没有释放那个对象 (指你说的 transform ),如果 mono 释放了,自然就清理掉了。

@cloudwu
cloudwu commented Jan 7, 2017 edited

上面说的只是方案,具体实现有无数种优化方法。只要实现我说的流程就可以。关键是找到外部引用,内部无引用集。不用 __gc 复活去找也没问题,自己写百来行代码遍历 lua vm 去找也挺容易。

若假设释放循环引用并不是特别需要立刻处理的事情,可以直接把 lua 中监测外部引用的弱表做成强表。也就是放出去的外部引用无论自己内部是否还有引用都不管它。然后等合适的时机(比如 loading 等),主动调用检测循环。也就是把强表改弱,强制跑完一轮 gc ,找到唯一外部引用,然后再跑后续流程。

只要你能正确理解方法,那么实现上可以去优化的地方多的是。

脑子里想不明白,更应该实现出来看看哪里出了问题。

@chexiongsheng
Collaborator

可能我表达的得不好吧,我不是说一个gc流程加那些对象的移动会有多耗,而是我们本来的目的就是为了解决循环引用。而由于同进同出的规则存在,循环引用相关对象会被临时访问代理的二次访问牵连而重新移回cache,那你的这算法最精妙的部分就失效了。我之所以强调那些临时访问代理多,不是为了说移动的代价大,而是想说二次访问的几率变大了。

@cloudwu
cloudwu commented Jan 7, 2017

同进同出是指一个 gc 循环期间,所有不被内部引用,只被外部引用的对象,不是指历史上所有的这种对象。这些对象是潜在的有内部联系的。且如果这批对象里只有 mono 代理,是可以直接清除的。

只有夹入了 lua 对象,才会导致一批进坟场。

如果夹入的 lua 临时对象真的是临时的,mono 用完也不要了,那么 mono 之后会通知这个 lua 对象删除,之后,这批对象间的联系就会被打破。就是完成了循环解除。

如果夹入的 lua 临时对象是持久的,那么本就不应该删除。如果 mono 随后访问这个对象,将永久从坟场复活,不会反复。如果想加强这种情况的优化,可以不立刻解除 mono 访问过的 lua 对象,不立刻放到弱表里,或只根据需要把 cache 属性变弱。这样可以在 gc 循环结束时,最少的筛选出这种 lua 临时对象无关的东西。

唯一出反复进出可能是:lua 这边属于临时对象,lua 自己不再引用,mono 一直持有,就是不删,但是也从来不访问这个对象。

@cloudwu
cloudwu commented Jan 7, 2017

今天花了一天时间,搭了个架子,实现了 C# 对 Lua 的调用。主要把本贴最前面的想法用代码说明。

https://github.com/cloudwu/csbridge

完成的部分包括:可以获取 Lua 虚拟机里的全局函数;可以传递 C# 的 class 到 Lua 中被 Lua 持有,可以调用 Lua 对象。调用结果可以返回到 C# ,返回结果可以是任意类型。

尚没有完成 Lua 对 C# 的调用,和 C# 长期持有 Lua 对象的处理。

@chexiongsheng
Collaborator

"如果 mono 随后访问这个对象,将永久从坟场复活,不会反复。"
不太明白,如果lua所有对象都不持有这对象,gc过后不是还会进坟场么?
另外,感觉你那个在loading时大搞一次的想法靠谱,我验证下

@cloudwu
cloudwu commented Jan 7, 2017

mono 每次调用,访问过的对象,都会在坟场外制造强引用,这个强引用保持多久要按需要去设计。通常会优化成访问过的对象保持一段时间再去掉强引用。

ps. csbridge 中 Lua 对 C# 的调用也完成了。基础框架算基本完成,需要配合一些代码生成工具去生成高效的供 Lua 调用的 C# 代码,或者不考虑效率用反射封装也行。

@chexiongsheng
Collaborator

这不是又要加一坨代码了。。我感觉目前为止,这方案已经复杂到不太适合放在平时的运行期了,循环引用这类不是很紧急的东西,由业务在合适的地方(比如切场景的loading之类)主动去调用解环比较合适。

@cloudwu
cloudwu commented Jan 8, 2017

不用加一坨,我的实现中大约加了 3 行 lua 代码完成延迟解引用。就是另外给一个强表,访问时在弱表和强表各加一次引用。之后定期把强表清掉。

ps. 最要紧的是,这些管理引用关系(以及其它一些)逻辑,一定一定要用 lua 自己去写,而不要在 C 代码利用 lua c api 写上一大坨。

@chexiongsheng
Collaborator

ps一下,你前面说的“每个用 u3d 开发的公司都会顺手搞个 某lua ,反正 C# 不受待见,都想用 lua 做开发”
我先举个反面例子:我前同事公司的三个项目,第一个项目是C#+Lua,做完这项目他们表示完全受不了Lua。第二个项目只用C#,结果后悔了,后悔的原因是没法热更,一些小bug(小主要是代码改动很小,但对用户影响还是挺大的)只能干等大版本,现在第三个项目开发中,准备用xLua的hotfix特性。
你云大的公司,程序员都是你面试(或者你面试过的人面试的),或者冲着你云大过去的,那当然“C# 不受待见,都想用 lua 做开发”。
我个人用lua也有一段时间了,从11年底开始到现在,12年初就搞过类似xLua的东西,当时只是在那家小公司里头内部用,没放出去。那时前后台框架都是我主导开发,lua全栈开发。说这些只是想说明我个人对lua是有深厚的感情的,至少不会黑它。我个人的看法是C#也是很好的语言,如果综合语言设计,语言实现,库,社区,工具链等来看,比lua要好。

@cloudwu
cloudwu commented Jan 9, 2017 edited

设计框架的人喜欢 C# 很正常,对于固定需求,静态语言更有优势。但多变的业务逻辑还是动态性强的语言更好。纯 C# 当然比 C# + Lua 做项目好,准确说是纯用一种语言肯定比混合语言开发好。

如果单论语言设计, C# 和 Java 在一个层次上,比 C++ 好一点,和 Lua 的 “语言设计" 这点还是有很大差距的。

@cloudwu
cloudwu commented Jan 9, 2017 edited

如果只是想更新代码,我觉得更好的方案是再嵌一个 C# , 统一用一种语言,而不是再引入一门新语言。

比方说,可以试试 http://www.paxcompiler.com/paxscriptnet/about.htm

@chexiongsheng
Collaborator

@cloudwu 首先只谈语言设计,不谈语言实现,库,社区,工具链,有点耍流氓。
况且,就是“语言设计”,我也没觉得C#差了,lua的设计可能更少错误,个人觉得也是因为做得少,少做少错。你前面也强调了,你对C#不熟。。。

@jinqi166
jinqi166 commented Jan 9, 2017

到此为止吧,在讨论下去就跟本帖无关了,只是大家看待问题的触发点和角度不同而已,我觉得这个issue可以close掉了

@cloudwu
cloudwu commented Jan 9, 2017

实现好不好就看代码是否清晰,好不好理解。按这个标准, lua 的官方实现和 mono 的实现哪个好呢?

做的少,没有多余的东西,已有的东西又是完备的,足够解决问题。这就是做的最好的地方了。设计的足够少已经体现了语言设计的功力。

而 C# 做了那么多,结果基础的 coroutine (yield 和异常不兼容)和 closure (不能引用外部 ref out 变量) 都不够完备 …… 不是高下立分么?

@chexiongsheng
Collaborator

你说的那个coroutine不是C#的,这是Unity用generator模拟出来的货。。c#在异步并发的解决方案是await。

ref,out在lua没有对应物,不好比较。

@chexiongsheng
Collaborator

还有,mono也代表不了.net

@chexiongsheng
Collaborator

"代码是否清晰,好不好理解",如果要拿mono代表.net话,是不是也可以拿luajit来代表一下lua呢?

@dwing4g
dwing4g commented Jan 9, 2017

C#的流行真是拜Unity所赐啊, 游戏引擎基本都是C/C++写的, 接入Lua语言本来是很自然方便的事, 基本没有自研引擎的公司能想到开放C#语言来二次开发.
结果Unity一出, 既不想暴露C++接口, 又想要运行效率, 结果接个Mono来支持C#开发, 搞得与Lua之间的互操作极其复杂, 真希望Unity开发者以后直接暴露C接口就天下太平了(或许从il2cpp中能找到接口?)...
话说, 新版Unity的mono已经支持sgen了?

@cloudwu
cloudwu commented Jan 9, 2017

luajit 和 lua 特性都不同,是不同的语言,或者说,是一种 lua 的方言。luajit 的 coroutine 也是不完备的,靠给给系统打补丁完成。其它许多语言特性也遗留了 lua 一个古老的版本中的设计冗余,比如把全局变量特殊看待,引入函数环境这些多余的东西,而这些 Lua 本身已经自己修正了。

找 mono 代表 .net 的实现质量,真的是因为微软家的不开源啊。

ref out 在 lua 中没有对应物不正说明 C# 加入了一些可以不要,实现了却和其它语言特性放在一起不完备的东西?

coroutine 和并发是两个层面的东西,coroutine 指的语言是否能把执行序列作为 first class 来看待。在这点上 C# 做的并不完备。

@chexiongsheng
Collaborator

感觉ref,out一个是实现多返回,另外一个是性能考虑吧。我猜的,作者才知道。
不能因为某语言没有某个特性也是完备的,就否掉别的语言那个特性吧?Brainfuck也是图灵完备的,语言元素更少,那是不是可以完全把其它所有语言的大多数特性都否掉了?
lua的设计也没有完全正交,比如它的各种语法糖。
c# await返回的task也是first class的,就一个状态机,本质上和coroutine的stack没区别。

@dwing4g
dwing4g commented Jan 9, 2017

ref out用在struct类型的参数, 可以省很多开销. 当然主要还是为了解决多返回值的问题, 毕竟C系语言都不习惯返回多个值, 所以参考了指针参数的方式.
C#最大的优点是开发效率,运行效率,除错效率都非常好; Lua最大的优点是简单,轻量级; 各有无法替代的优势, 也是对手的劣势, 适用环境区别很大, 就不要争了, 所谓一些语言细节都是次要的, 没有十全十美的语言, 对少量缺陷要容忍. 要不是Unity的掺合, 平时没见过比较C#和Lua的.

@cloudwu
cloudwu commented Jan 9, 2017 edited

Brainfuck 没有 coroutine 也没有 closure ,完全不能放在一起讨论。图灵完备只是一个必要条件,不是充分条件。

async/await 解决的只是并行多个执行序分离的问题,并不能直接控制执行序,也就是缺乏 yield 的能力。比如要实现一个迭代器,还是得用 IEnumerable 和 yield ,继而上面提到的和异常系统的兼容性依旧存在。

如果增加一个特性而不考虑和已有的一个特性的兼容性,而另一种语言有相同的两个特性却能解决好。这里谈的不就是设计好坏吗?

@liiir1985
liiir1985 commented Jan 9, 2017 edited

用IEnumerable来做coroutine本来就是unity里才有的特有用法,在所有其他.net应用中都是不存在的
我也想不出来在其他环境中的.net为什么会需要用coroutine,而不是直接用async/await
C#在设计的时候就本来不是这么使用的,只是Unity为了实现特定的功能这么用了。早期Unity的开发团队也是可以说对c#非常不熟,定义的接口也是很多不符合.Net规范的
ref关键字最主要是为了值类型的传参效率,ref int 在IL层面= int&,closure取之前栈上的int&并没有任何意义,甚至可能取到野指针
对于在非值类型上用ref关键字…… 绝大多数情况纯粹是使用者理解错误的错误用法

c++/c上配lua无可厚非,也没必要跟c#做比较
c#在ide,调试,语法糖等方面使得他的开发效率是非常高的,如果unity能支持.net 4.6 的profile,加上dynamic关键字,用起来跟lua也没有太大差别
至于开发效率,还有ide这些,个人感觉不同,也没有特别大的对比意义,就像vim/emacs还是VisualStudio好用,不同环境和团队有自己的见解,横向对比没有意义

@chexiongsheng
Collaborator

Unity不支持async/await是因为它用的mono版本过低

@dwing4g
dwing4g commented Jan 9, 2017

Unity 5.5大幅更新了mono, 貌似支持了async/await, 但gc貌似尚未支持sgen.

@jinqi166
jinqi166 commented Jan 9, 2017

你的逻辑代码在unity下只能在主线程跑,现在是将来也是,至于渲染上使用多线程你不用关心也不管你的事!开发者大会上有人问多线程渲染是否安全以及用户逻辑是否可以使用多线程,官方答案很坚定:“逻辑部分是线程安全的,因为你只有一个主线程”

@cloudwu
cloudwu commented Jan 9, 2017

coroutine 一个最大的用途就是用来干净的实现迭代器,迭代器的意义就在于两个不同的执行序间有通讯的能力。语言不支持没关系,C++ 就不支持,用 C++ 开发的项目当然可以选别的方法来解决问题。这当然不是必不可少的语言特性。

但如果来讨论语言设计好坏,C# 等于支持了一半,这就很尴尬了。

对于 closure 来说,一切外部值都是引用。比如在 lua 里,你写 local a = 1 ,那么后续有 closure 的话,自然也是引用的 &a ,能不能保证不是野指针是语言在实现这个特性时需要保证的东西。比如 lua 里将一个栈变量经过 closure 封闭后内部变成一个引用对象,就是运行时处理的过程。C# 想做当然也做的出来,选择直接禁止某种用法也没有说有错。只能说语言设计时考虑的不完备。

@dwing4g
dwing4g commented Jan 9, 2017

话说, C#的yield/async/await都不是真正的coroutine, 编译器搞的语法糖而已; Lua的coroutine是真的.

@liiir1985

mono 2.6最坑的不是支不支持async/await,而是他用的Boehm GC,这东西拿给c++用还凑合,给c#用是巨坑无比,毕竟mono2.6已经是快10年前的东西了,希望早日把这个破gc换了,换成generational的

@liiir1985

关于逻辑代码是否只能在主线程跑,要是你看了Keynote 2016的话就知道,从Unity5.6开始,unity将步入全面多线程的时代,会引入一个C# Jobsystem的东西让开发者多线程跑逻辑,他还提供了一个逻辑层多线程的Demo, 控制20w条鱼的不同轨迹运动

@dwing4g
dwing4g commented Jan 9, 2017

想要支持真正的coroutine, 对底层改动非常大, 除非一开始设计.NET的时候就考虑到有这个东西, 现在再支持恐怕弊大于利了, 所以也就只能用编译器技巧来实现表面差不多的玩意, 这也是没办法的事. Lua这种轻量级的运行环境反而容易实现. 不过话说回来, 这一点对C#来说无可厚非, 有点小缺陷没什么影响的, 完全无法撼动C#的其它优势, 除了影响完美主义者的心理作用.
关于多线程, 必要性真不大, 现在就很好了, 没听说有CPU瓶颈的, 即使遇到大多都有设计问题, 个别特殊情况自己创建线程即可.

@liiir1985

多线程的确不是一个非有不可的东西,对于unity而言,加入逻辑层多线程是因为现在他把渲染多线程了之后,瓶颈的确出现在了CPU层。他的渲染多线程是通过一套自己的JobSystem实现的,CPU层往JobSystem添加Job,然后渲染层多线程并行处理Job列表,然后现在的瓶颈就在排job这层了,所以他退出C#JobSystem只是个顺带的,底层实际上是他实现了Transform等关键组建的线程安全,从而达到可以多线程排Job

@cloudwu
cloudwu commented Jan 9, 2017

手持设备上就不要去想利用多线程去提高性能了,一个线程能做的事情,安排到几个线程做只会更浪费电。排查热点,想想哪里做的不对需要改进才是正途。

@cloudwu cloudwu closed this Jan 9, 2017
@dragengt

从知乎过来留名……支持云风大大。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment