lua性能优化之luajit集成

汪阳辉
2023-12-01

luajit工作模式:luajit中存在两种工作模式,分别如下:
1.jit模式:也就是即时编译(just in time)模式。该模式下会将代码直接翻译成机器码,并向操作系统申请可执行内存空间来存储转换后的机器码。执行时直接执行机器码就行,所以效率是最高的。但是iOS,xbox,ps4等平台鉴于自身安全原因都是不授权分配可执行内存空间的,所以这些平台下就不能使用jit模式。

2.interpreter模式:也就是翻译器模式。该模式下会将代码先翻译成字节码,然后将字节码翻译成机器码,所以无需像操作系统申请可执行内存空间,所以几乎所有平台都支持这种模式。但是性能相比jit模式而言还是有一定差距的。

luajit的工作方式:luajit采用trace compiler方案,也就是追踪编译方案。luajit都会先用interpreter模式将代码转换成字节码。然后在支持jit的平台上将经常执行的代码开启记录模型并记录这些代码实际运行每一步的细节,最后被luajit优化以及jit化。

jit失败情况:并不是所有可以被jit的代码都可以正常被jit处理的,以下情况就会造成jit代码失败。
一.可供执行的内存空间被耗尽:针对arm平台有个限制就是跳转指令只能前后跳转32m,所以必须至少保证64m的空间被用来存储jit处理后的机器代码。如果这64m中有任何一点内存被用作他用就会出现内存空间不足而造成jit代码失败。这种情况的优化建议如下:
1.在android工程的Activity入口中就加载luajit,做好内存分配,然后将这个luasate传递给unity使用。
2.减少lua代码,进而减少lua内存大小,使其编译的机器码可以完整的存放在分配的可执行内存中。
3.禁用jit功能。

二.可供使用的寄存器不足:针对arm平台下,可供使用的寄存器数量会远远低于x86平台,而lua中的lua变量就是存储在寄存器中的,当local变量越多,或者local变量调用层级越深造成local变量生命周期变长而不能及时回收而间接增加local变量个数,都会使寄存器的使用个数变多,这样就容易出现寄存器不足而造成jit代码失败的情况。这种情况的优化建议如下:
1.减少local变量的使用个数。
2.不要过深层次的调用local变量。可以通过do … end来限制local变量的生命周期。

三.调用c相关的函数:c#底层也是调用c,跟直接调用c一样,这些c相关的代码是不能被jit化的。这种情况的优化建议如下:
1.使用luajit中提供的ffi工具来调用c相关代码,这样就可以被jit化。
2.使用luajit 2.1.0beta2版本中的trace switch功能,将c代码独立出来,从而将可以jit的代码进行jit处理。但是效果有限且不是很明显。

四.不支持的字节码:有非常多bytecode或者内部库调用是无法jit化的,最典型就是for in pairs,以及字符串连接符。常见的不能jit处理的对比列表如以下链接,凡是标记2.1以及yes的都尽量少用,最好不用。
http://wiki.luajit.org/NYI

五.jit失败判定:我们可以使用luajit目录下的v.lua文件进行查看是否jit代码失败,代码如下:
local verbo = require(“jit.v”)
verbo.start()
当你看到以下错误的时候,说明你遇到了jit失败
failed to allocate mcode memory,对应错误一
NYI: register coalescing too complex,对应错误二
NYI: C function,对应错误三
NYI: bytecode,对应错误四
这在luajit.exe下使用会很正常,但要在unity下用上需要修改v.lua的代码,把所有out:write输出导向到Debug.Log里头。

luajit性能提升方案:
1.按照luajit规范编写lua代码:投其所好,按照标准编写的lua代码性能会更高些。可以参照http://blog.csdn.net/zjz520yy/article/details/79456937

2.尽量使用local function而不是class方式:因为class调用function方式都是从metatable中进行表查询来调用的,相对local func = Class.Func的调用而言,性能肯定是低了点,但是可读性却强了很多。所以使用local function还是class function形式还是视情况而定。对于频繁调用的函数,如Vector3的操作等,建议用local function,这样性能会提高很多,而针对主要代码可以按照class function形式,这样更加面向对象,可读性也会强很多。

3.借助ffi可以提升lua与c/c#之间的交互:ffi是luajit提供的一个神器,主要用来在jit模式下进行高效的luajit与c之间的交互。其原理就是向luajit中指定c函数原型,这样luajit就可以直接生成机器码级别的优化代码来与c交互,而不需要传统的lua api来做交互。这样在与c#交互时,主要留下c到c#的交互消耗了。相关使用方式如下:

首先,我们在c中定义一个方法,用于将c#的函数注册到c中,以便在c中可以直接调用c#的函数,这样只要luajit可以ffi调用c,也就自然可以调用c#的函数了

void gse_ffi_register_csharp(int id, void* func)
{
  s_reg_funcs[id] = func;
}

这里,id是一个你自由分配给c#函数的id,lua通过这个id来决定调用哪个函数。



然后在c#中将c#函数注册到c中

[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern void gse_ffi_register_csharp(int funcid, IntPtr func);

public static void gse_ffi_register_v_i1f3(int funcid, f_v_i1f3 func)
{
  gse_ffi_register_csharp(funcid, Marshal.GetFunctionPointerForDelegate(func));
}

gse_ffi_register_v_i1f3(1, GObjSetPositionAddTerrainHeight);//将GObjSetPositionAddTerrainHeight注册为id1的函数

然后lua中使用的时候,这么调用
local ffi = require("ffi")
ffi.cdef[[
int gse_ffi_i_f3(int funcid, float f1, float f2, float f3);
]]

local funcid = 1
ffi.C.gse_ffi_i_f3(funcid, objID, posx, posy, posz)

就可以从lua中利用ffi调用c#的函数了
可以类似tolua,将这个注册流程的代码自动生成。

选择luajit的原因:虽然luajit更新频率不高,但是相比原生lua而言,luajit在interpreter模式下就可以支持所有平台,而且提高3到8倍的性能提升,在jit模式下可以提供几十倍的性能提升,虽然jit模式不是很好控制。所以鉴于出色的性能提升以及丰富的第三方方案来弥补原生lua新特性,所以建议还是继续使用。

开启interpreter模式方式:只需要在lua代码的第一行加上以下代码就行。

if jit then

  jit.off();jit.flush()
end
 类似资料: