首页 » 技术文章 » 【源码解读】luajit源码入门

【源码解读】luajit源码入门

 

简介

luajit也是lua代码的一款虚拟机,相比较原生lua虚拟机而言,其优势为性能优越,同时支持ffi,能非常方便的集成C语言实现的模块,而这在原生lua虚拟机中集成需要编写较多的接口代码。

背景

最近尝试使用luaprofiler工具来测试lua层代码的函数级性能统计,但发现luaprofiler工具并不能适用luajit,于是对luajit做了一个大体的了解。

luajit运行机理(非jit模式)

luajit(关闭jit模式)与原生的lua虚拟机运行机制是一样的,都会产生中间代码,并不直接将lua解释生成机器码,而是生成一段中间指令,然后解释器解释指令并执行(直接生成机器指令的虚拟机比如mono,它是直接将IL指令解释成机器指令,将机器指令保存到内存中,然后修改这片内存的属性,让其具备可执行的属性,再跳转到该段内存直接运行)。 luajit与原生的lua虚拟机不同之处在于,中间指令的设计方式以及解释执行方式都不同,而且luajit中间指令的执行都是通过汇编直接编码,力求达到效率最大化,而且指令设计也非常紧凑,指令与指令之间的衔接也很紧凑,因此每个操作所需要的指令数要大大小于原生lua虚拟机,从而获得更高的性能,接下来简单介绍下其实现原理(因为虚拟机部分很多都是使用二进制机器指令编写,无法阅读,很多都是使用gdb调试过程反汇编跟踪的,所以没能做到较细,只是个大体的介绍)。luajit也分为编译和解释执行两步,编译部分与原生lua虚拟机类似,逐段逐行读入token,然后生成中间指令。luajit中间指令是一个4个字节的整数,将4个字节的整数分为了几个部分,其中最低的一个字节用来作为指令的操作,中间的一些指令用来指示操作应该操作的寄存器(这个寄存器是指lua中的freeregs,其实就是lua_state中的base与top这一段内存区的某个slot),整个编译过程最后生成了一个指令数组,解释执行时,其步骤为:取出第一条指令=>获取最低字节得到操作码op=>跳转到GG_State->dispatch[op]位置=>执行操作=>读取下一条指令操作码op=>跳转到GG_State->dispatch[op]位置,这样实现了多条指令的衔接。原生lua是每次读取一条指令,然后在lvm.c中的luaV_execute中的循环体中执行,因此每个操作对应的指令会更多一些(取指,swich条件判断,跳转,最后再执行),而luajit逐条指令衔接,且GG_State->dispatch[op]保存的位置都是使用机器指令直接编码(lj_vm.S文件中),实现效率最优化,luajit和原生lua在编译时生成的指令数目基本相等。

luajit jit模式

上述运行机理针对的是解释模式,luajit还支持jit模式,该模式会生成机器代码,而且还会根据其执行流热度还决定是否生成机器码。其生成机器码并修改代码段属性的堆栈调用为:

#0  0x00007ffff73eeb60 in mprotect () from /usr/lib64/libc.so.6
#1  0x0000000000441efb in mcode_setprot (p=0x0, sz=0, prot=5) at lj_mcode.c:80
#2  0x0000000000441f45 in mcode_protect (J=0x40000548, prot=5) at lj_mcode.c:150
#3  0x0000000000442365 in lj_mcode_abort (J=0x40000548) at lj_mcode.c:265
#4  0x000000000041fc62 in trace_abort (J=0x40000548) at lj_trace.c:496
#5  0x000000000042085c in trace_state (L=0x40000378, dummy=0x0, ud=0x40000548) at lj_trace.c:613
#6  0x000000000042e6b0 in lj_vm_cpcall ()
#7  0x0000000000420973 in lj_trace_ins (J=0x40000548, pc=0x4000aa7c) at lj_trace.c:633
#8  0x000000000040c207 in lj_dispatch_ins (L=0x40000378, pc=0x4000aa80) at lj_dispatch.c:373
#9  0x000000000042fd30 in lj_vm_inshook ()
#10 0x00000000004108bc in lua_call (L=0x40000378, nargs=2, nresults=0) at lj_api.c:1010
#11 0x000000000047ec31 in hookf (L=0x40000378, ar=0x7fffffffe140) at lib_debug.c:218
#12 0x000000000040c007 in callhook (L=0x40000378, event=2, line=20) at lj_dispatch.c:333
#13 0x000000000040c32c in lj_dispatch_ins (L=0x40000378, pc=0x40008208) at lj_dispatch.c:387
#14 0x000000000042fd30 in lj_vm_inshook ()
#15 0x0000000000410975 in lua_pcall (L=0x40000378, nargs=0, nresults=-1, errfunc=2) at lj_api.c:1028
#16 0x0000000000403aa3 in docall (L=0x40000378, narg=0, clear=0) at luajit.c:121
#17 0x000000000040446f in handle_script (L=0x40000378, argv=0x7fffffffe578, n=1) at luajit.c:285
#18 0x00000000004050a1 in pmain (L=0x40000378) at luajit.c:520
#19 0x000000000042e379 in lj_BC_FUNCC ()
#20 0x0000000000410b3a in lua_cpcall (L=0x40000378, func=0x404f18 <pmain>, ud=0x7fffffffe470) at lj_api.c:1050
#21 0x0000000000405209 in main (argc=2, argv=0x7fffffffe578) at luajit.c:565

luajit与一般的虚拟机jit实现较为不同,一般的虚拟机jit通过将热点函数或方法直接动态生成机器码,从而在后续的执行流中能加速,而luajit则通过优化热点路径来实现。 那么luajit如何发现热点路径的呢?在luajit的源码中存在一个宏定义如下:

#define hotcount_get(gg, pc) (gg)->hotcount[(u32ptr(pc)>>2) & (HOTCOUNT_SIZE-1)]

luajit的解释器在解释字节码执行时,会更新热点统计信息。在buildvm_x86.dasc源码中存在下述代码:

case BC_FORL:
#if LJ_HASJIT
    |  hotloop RB
#endif
    | // Fall through. Assumes BC_IFORL follows and ins_AJ is a no-op.
    break;

即对于for循环语句,会进入热点路径检测,hotloop的定义如下:

|.macro hotloop, reg
|  mov reg, PC
|  shr reg, 1
|  and reg, HOTCOUNT_PCMASK
|  sub word [DISPATCH+reg+GG_DISP2HOT], 1
|  jz ->vm_hotloop
|.endmacro

每次执行到hotloop中时,其会根据pc的值,更新该pc对应的热点计数,这里的热点计数是通过递减来实现,每个热点最开始会被初始化为56,当for循环中的语句调用56次时,此时会执行jz ->vm_hotloop,vm_hotloop的定义如下:

#if LJ_HASJIT
  |  mov LFUNC:RB, [BASE-8]		// Same as curr_topL(L).
  |  mov RB, LFUNC:RB->pc
  |  movzx RD, byte [RB+PC2PROTO(framesize)]
  |  lea RD, [BASE+RD*8]
  |  mov L:RB, SAVE_L
  |  mov L:RB->base, BASE
  |  mov L:RB->top, RD
  |  mov FCARG2, PC
  |  lea FCARG1, [DISPATCH+GG_DISP2J]
  |  mov aword [DISPATCH+DISPATCH_J(L)], L:RBa
  |  mov SAVE_PC, PC
  |  call extern lj_trace_hot@8		// (jit_State *J, const BCIns *pc)
  |  jmp <3
#endif

中间的汇编代码不作过多的分析,其最终会调用lj_trace_hot。

void LJ_FASTCALL lj_trace_hot(jit_State *J, const BCIns *pc)
{
  ERRNO_SAVE
  /* Note: pc is the interpreter bytecode PC here. It's offset by 1. */
  hotcount_set(J2GG(J), pc, J->param[JIT_P_hotloop]+1);  /* Reset hotcount. */
  /* Only start a new trace if not recording or inside __gc call or vmevent. */
  if (J->state == LJ_TRACE_IDLE &&
      !(J2G(J)->hookmask & (HOOK_GC|HOOK_VMEVENT))) {
    J->parent = 0;  /* Root trace. */
    J->exitno = 0;
    J->state = LJ_TRACE_START;
    lj_trace_ins(J, pc-1);
  }
  ERRNO_RESTORE
}

最终调用到lj_trace_ins,其代码实现为:

void lj_trace_ins(jit_State *J, const BCIns *pc)
{
  /* Note: J->L must already be set. pc is the true bytecode PC here. */
  J->pc = pc;
  J->fn = curr_func(J->L);
  J->pt = isluafunc(J->fn) ? funcproto(J->fn) : NULL;
  while (lj_vm_cpcall(J->L, NULL, (void *)J, trace_state) != 0)
    J->state = LJ_TRACE_ERR;
}

static TValue *trace_state(lua_State *L, lua_CFunction dummy, void *ud)
{
  jit_State *J = (jit_State *)ud;
  UNUSED(dummy);
  do {
  retry:
    switch (J->state) {
    case LJ_TRACE_START:
      J->state = LJ_TRACE_RECORD;  /* trace_start() may change state. */
      trace_start(J);
      lj_dispatch_update(J2G(J));
      break;

    case LJ_TRACE_RECORD:
      trace_pendpatch(J, 0);
      setvmstate(J2G(J), RECORD);
      lj_vmevent_send(L, RECORD,
	setintV(L->top++, J->cur.traceno);
	setfuncV(L, L->top++, J->fn);
	setintV(L->top++, J->pt ? (int32_t)proto_bcpos(J->pt, J->pc) : -1);
	setintV(L->top++, J->framedepth);
      );
      lj_record_ins(J);
      break;

    case LJ_TRACE_END:
      trace_pendpatch(J, 1);
      J->loopref = 0;
      if ((J->flags & JIT_F_OPT_LOOP) &&
	  J->cur.link == J->cur.traceno && J->framedepth + J->retdepth == 0) {
	setvmstate(J2G(J), OPT);
	lj_opt_dce(J);
	if (lj_opt_loop(J)) {  /* Loop optimization failed? */
	  J->cur.link = 0;
	  J->loopref = J->cur.nins;
	  J->state = LJ_TRACE_RECORD;  /* Try to continue recording. */
	  break;
	}
	J->loopref = J->chain[IR_LOOP];  /* Needed by assembler. */
      }
      lj_opt_split(J);
      J->state = LJ_TRACE_ASM;
      break;

    case LJ_TRACE_ASM:
      setvmstate(J2G(J), ASM);
      lj_asm_trace(J, &J->cur);
      trace_stop(J);
      setvmstate(J2G(J), INTERP);
      J->state = LJ_TRACE_IDLE;
      lj_dispatch_update(J2G(J));
      return NULL;

    default:  /* Trace aborted asynchronously. */
      setintV(L->top++, (int32_t)LJ_TRERR_RECERR);
      /* fallthrough */
    case LJ_TRACE_ERR:
      trace_pendpatch(J, 1);
      if (trace_abort(J))
	goto retry;
      setvmstate(J2G(J), INTERP);
      J->state = LJ_TRACE_IDLE;
      lj_dispatch_update(J2G(J));
      return NULL;
    }
  } while (J->state > LJ_TRACE_RECORD);
  return NULL;
}

trace_state记录指令流,通过对LJ_TRACE_START流程的处理,对所有的dispach都加了一层hook。使得每条指令都会调用lj_vm_record。

#if LJ_HASJIT
  |  movzx RD, byte [DISPATCH+DISPATCH_GL(hookmask)]
  |  test RDL, HOOK_VMEVENT		// No recording while in vmevent.
  |  jnz >5
  |  // Decrement the hookcount for consistency, but always do the call.
  |  test RDL, HOOK_ACTIVE
  |  jnz >1
  |  test RDL, LUA_MASKLINE|LUA_MASKCOUNT
  |  jz >1
  |  dec dword [DISPATCH+DISPATCH_GL(hookcount)]
  |  jmp >1
#endif
  |1:
  |  mov L:RB, SAVE_L
  |  mov L:RB->base, BASE
  |  mov FCARG2, PC			// Caveat: FCARG2 == BASE
  |  mov FCARG1, L:RB
  |  // SAVE_PC must hold the _previous_ PC. The callee updates it with PC.
  |  call extern lj_dispatch_ins@8	// (lua_State *L, BCIns *pc)

luajit会记录指令并将其编译为SSA,最终再生成机器指令。具体实现见lj_record_ins。其它流程暂时还没深入分析。

luajit的性能监控与调试

luajit提供的调试机制也与原生的lua类似,能跟踪函数级,逐行跟踪,其实现原理是通过hook GG_State->dispatch[op]的操作,在指令执行时加hook(例如如果只想跟踪函数的执行,则只需要对BC_CALLX这类指令加hook即可,如果同时也希望在函数返回时接收通知,hook BC_RETX这类指令)。但是luajit实现的这些操作与原生lua存在微妙的差异,这也使得luaprofiler工具无法在luajit中正确使用。主要差异有以下几点:

  • 1、luajit碰到lua中调用C函数时,由于生成了BC_CALL指令,因此能正确被hook到函数的调用通知,但未生成BC_RET指令,从而无法得到函数的返回通知。其原因为c函数调用return后,luajit并未实现相关代码来收尾(具体汇编没做更多分析),但原生lua会执行lua_postcall最终能生成返回通知(当然c函数中再直接调用其他C函数是无法hook到,当然也不需要);
  • 2、luajit对大部分lua代码实现的函数调用能正确的通知call与ret,但是对于尾调用这类特殊的调用,它的实现也存在一些缺陷,也无法给出正确的ret通知,而原生lua是可以给出正确的call与ret通知的; 基于上述两点,luaprofiler就无法正常在luajit中使用了,而原生lua却可以较好的支持函数级的性能跟踪。目前也开始了解部分luajit的profiler,它们的具体实现还没仔细阅读,但基本上针对函数级别的或多或少会存在缺陷,而针对行级别可能可以实现,但粒度太小,对程序的性能影响也会较大,只能是做一个较为粗略的分析。

PS

PS:这里也补充下关于luajit源码分析相关的内容,luajit源码对比lua源码来说,其阅读难度较大,主要因为太多的实现直接使用机器指令,阅读时不能直接跟踪实现,可能也是这个原因使得现在网上基于luajit的源码分析资料非常少(基本没有),建议如果阅读最好使用调试工具跟踪并反汇编,同时使用IDA工具将整个luajit项目反汇编,这样也获得lvm中的机器指令的汇编代码(机器指令无可阅读性)。

一篇讲JIT的好文章 http://www.tuicool.com/articles/aUzuUn

本文作者:马良

原文链接:【源码解读】luajit源码入门,转载请注明来源!

2