当前位置: 首页 > news >正文

LuaJit分析(九)LuaJit中的JIT原理分析

Jit in luajit

Luajit是一款高性能的lua解释器,与官方的lua解释器相比,luajit的高速除了将解释器直接以汇编代码实现外,还支持jit模式(Just in time)。Jit模式即将luajit的字节码编译成处理器能够直接执行的机器码,从而比解释执行速度更快。

Luajit存在97个字节码指令,例如 FORL指令对应一个数字类型的for循环语句,同时还有IFORL指令(强制解释模式执行)和JFORL指令(Jit模式执行),同时解释器实现了对各个字节码指令的翻译,这里以X86的翻译器为例。

Luajit优化一段指令序列,当一个指令的地址被识别为hot后,并开始跟踪记录指令线性序列、在退出跟踪时将指令序列编译成机器码。但是luajit只对FUNCF、FORL、ITERL、LOOP这四个指令进行了跟踪,即循环和一个函数的开始,例如,在解释执行FORL指令:

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

它首先判断是否是JIT模式,如果是jit模式,则调用hotloop块进行热点判断,同样的,如果是FUNCF指令,则调用hotcall块:

case BC_FUNCF:|.if JIT|  hotcall RB|.endif
case BC_FUNCV:  /* NYI: compiled vararg functions. */| // Fall through. Assumes BC_IFUNCF/BC_IFUNCV follow and ins_AD is a no-op.
break;

hotloop块的定义如下:

|// Decrement hashed hotcount and trigger trace recorder if zero.
|.macro hotloop, reg
|  mov reg, PC
|  shr reg, 1
|  and reg, HOTCOUNT_PCMASK
|  sub word [DISPATCH+reg+GG_DISP2HOT], HOTCOUNT_LOOP
|  jb ->vm_hotloop
|.endmacro

 它将当前指令的地址右移一位,并与HOTCOUNT_PCMASK与操作,得到一个索引(哈希运算),根据这个索引在数值中找到计数值,减去HOTCOUNT_LOOP,当这个计数值小于0时,跳转到vm_hotloop继续执行。

|->vm_hotloop:               // Hot loop counter underflow.
|.if JIT
|  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

首先获取当前的函数,并得到字节码PC指针,获取栈大小并保存到RD中,接着讲top的位置保存到RD中,在进行一些参数设置后,调用lj_trace_hot用于跟踪热点,该函数位于lj_trace.c中:

/* A hotcount triggered. Start recording a root trace. */
void LJ_FASTCALL lj_trace_hot(jit_State *J, const BCIns *pc)
{/* Note: pc is the interpreter bytecode PC here. It's offset by 1. */ERRNO_SAVE/* Reset hotcount. */hotcount_set(J2GG(J), pc, J->param[JIT_P_hotloop]*HOTCOUNT_LOOP);/* 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_START后,开始调用lj_trace_ins进行热点跟踪:

/* A bytecode instruction is about to be executed. Record it. */
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;
}

这里的pc是指向的字节码指令,在循环中不断执行和跟踪,这里的跟踪通过trace_state函数实现,这个函数存在7种状态:

/* Trace compiler state. */
typedef enum {LJ_TRACE_IDLE,  /* Trace compiler idle. */LJ_TRACE_ACTIVE = 0x10,LJ_TRACE_RECORD,  /* Bytecode recording active. */LJ_TRACE_START, /* New trace started. */LJ_TRACE_END,   /* End of trace. */LJ_TRACE_ASM,   /* Assemble trace. */LJ_TRACE_ERR    /* Trace aborted with error. */
} TraceState;

IDLE表示空闲、RECORD表示正在跟踪记录、END表示结束、ASM表示开始编译机器指令,这个状态转换函数的实现如下:

/* State machine for the trace compiler. Protected callback. */
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,/* Save/restore tmptv state for trace recorder. */TValue savetv = J2G(J)->tmptv;TValue savetv2 = J2G(J)->tmptv2;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);J2G(J)->tmptv = savetv;J2G(J)->tmptv2 = savetv2;);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->cur.linktype = LJ_TRLINK_NONE;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);lj_opt_sink(J);if (!J->loopref) J->cur.snap[J->cur.nsnap-1].count = SNAPCOUNT_DONE;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;
}

它根据不同的状态执行不同的操作函数,我们可以简化为:

/* State machine for the trace compiler. Protected callback. */
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:lj_record_ins(J);break;case LJ_TRACE_END:trace_pendpatch(J, 1);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);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_start用于初始化trace结构,分配一个traceno等,它是一个数组的下标,其中比较重要的是lj_record_ins函数,它用于记录一个字节码指令,并保存为一个SSA中间代码IR形式,IR的定义在lj_ir.c中:

/* -- IR instructions ----------------------------------------------------- */
/* IR instruction definition. Order matters, see below. ORDER IR */
#define IRDEF(_) \/* Guarded assertions. */ \/* Must be properly aligned to flip opposites (^1) and (un)ordered (^4). */ \_(LT,   N , ref, ref) \_(GE,   N , ref, ref) \_(LE,   N , ref, ref) \_(GT,   N , ref, ref) \\_(ULT,  N , ref, ref) \_(UGE,  N , ref, ref) \_(ULE,  N , ref, ref) \_(UGT,  N , ref, ref) \\_(EQ,   C , ref, ref) \_(NE,   C , ref, ref) \\_(ABC,  N , ref, ref) \_(RETF, S , ref, ref) \\/* Miscellaneous ops. */ \_(NOP,  N , ___, ___) \_(BASE, N , lit, lit) \_(PVAL, N , lit, ___) \_(GCSTEP, S , ___, ___) \_(HIOP, S , ref, ref) \_(LOOP, S , ___, ___) \_(USE,  S , ref, ___) \_(PHI,  S , ref, ref) \_(RENAME, S , ref, lit) \_(PROF, S , ___, ___) \\/* Constants. */ \_(KPRI, N , ___, ___) \_(KINT, N , cst, ___) \_(KGC,  N , cst, ___) \_(KPTR, N , cst, ___) \_(KKPTR,  N , cst, ___) \_(KNULL,  N , cst, ___) \_(KNUM, N , cst, ___) \_(KINT64, N , cst, ___) \_(KSLOT,  N , ref, lit) \\/* Bit ops. */ \_(BNOT, N , ref, ___) \_(BSWAP,  N , ref, ___) \_(BAND, C , ref, ref) \_(BOR,  C , ref, ref) \_(BXOR, C , ref, ref) \_(BSHL, N , ref, ref) \_(BSHR, N , ref, ref) \_(BSAR, N , ref, ref) \_(BROL, N , ref, ref) \_(BROR, N , ref, ref) \\/* Arithmetic ops. ORDER ARITH */ \_(ADD,  C , ref, ref) \_(SUB,  N , ref, ref) \_(MUL,  C , ref, ref) \_(DIV,  N , ref, ref) \_(MOD,  N , ref, ref) \_(POW,  N , ref, ref) \_(NEG,  N , ref, ref) \\_(ABS,  N , ref, ref) \_(ATAN2,  N , ref, ref) \_(LDEXP,  N , ref, ref) \_(MIN,  C , ref, ref) \_(MAX,  C , ref, ref) \_(FPMATH, N , ref, lit) \\/* Overflow-checking arithmetic ops. */ \_(ADDOV,  CW, ref, ref) \_(SUBOV,  NW, ref, ref) \_(MULOV,  CW, ref, ref) \\/* Memory ops. A = array, H = hash, U = upvalue, F = field, S = stack. */ \\/* Memory references. */ \_(AREF, R , ref, ref) \_(HREFK,  R , ref, ref) \_(HREF, L , ref, ref) \_(NEWREF, S , ref, ref) \_(UREFO,  LW, ref, lit) \_(UREFC,  LW, ref, lit) \_(FREF, R , ref, lit) \_(STRREF, N , ref, ref) \_(LREF, L , ___, ___) \\/* Loads and Stores. These must be in the same order. */ \_(ALOAD,  L , ref, ___) \_(HLOAD,  L , ref, ___) \_(ULOAD,  L , ref, ___) \_(FLOAD,  L , ref, lit) \_(XLOAD,  L , ref, lit) \_(SLOAD,  L , lit, lit) \_(VLOAD,  L , ref, ___) \\_(ASTORE, S , ref, ref) \_(HSTORE, S , ref, ref) \_(USTORE, S , ref, ref) \_(FSTORE, S , ref, ref) \_(XSTORE, S , ref, ref) \\/* Allocations. */ \_(SNEW, N , ref, ref)  /* CSE is ok, not marked as A. */ \_(XSNEW,  A , ref, ref) \_(TNEW, AW, lit, lit) \_(TDUP, AW, ref, ___) \_(CNEW, AW, ref, ref) \_(CNEWI,  NW, ref, ref)  /* CSE is ok, not marked as A. */ \\/* Buffer operations. */ \_(BUFHDR, L , ref, lit) \_(BUFPUT, L , ref, ref) \_(BUFSTR, A , ref, ref) \\/* Barriers. */ \_(TBAR, S , ref, ___) \_(OBAR, S , ref, ref) \_(XBAR, S , ___, ___) \\/* Type conversions. */ \_(CONV, NW, ref, lit) \_(TOBIT,  N , ref, ref) \_(TOSTR,  N , ref, lit) \_(STRTO,  N , ref, ___) \\/* Calls. */ \_(CALLN,  N , ref, lit) \_(CALLA,  A , ref, lit) \_(CALLL,  L , ref, lit) \_(CALLS,  S , ref, lit) \_(CALLXS, S , ref, ref) \_(CARG, N , ref, ref) \\/* End of list. */


多种情况都会出现结束记录的情况,如遇到了已经编译的指令。在LJ_TRACE_ASM状态下会进行代码的编译操作lj_asm_trace函数位于lj_asm.c中,函数中有一个循环如下:

  /* Assemble a trace in linear backwards order. */for (as->curins--; as->curins > as->stopins; as->curins--) {IRIns *ir = IR(as->curins);lua_assert(!(LJ_32 && irt_isint64(ir->t)));  /* Handled by SPLIT. */if (!ra_used(ir) && !ir_sideeff(ir) && (as->flags & JIT_F_OPT_DCE))
continue;  /* Dead-code elimination can be soooo easy. */if (irt_isguard(ir->t))
asm_snap_prep(as);RA_DBG_REF();checkmclim(as);asm_ir(as, ir);}

它调用asm_ir将所有的ir指令转换成机器码,在lj_asm_trace函数后,接着调用trace_stop函数结束一个跟踪,该函数实现如下:

/* Stop tracing. */
static void trace_stop(jit_State *J)
{BCIns *pc = mref(J->cur.startpc, BCIns);BCOp op = bc_op(J->cur.startins);GCproto *pt = &gcref(J->cur.startpt)->pt;TraceNo traceno = J->cur.traceno;GCtrace *T = J->curfinal;lua_State *L;switch (op) {case BC_FORL:setbc_op(pc+bc_j(J->cur.startins), BC_JFORI);  /* Patch FORI, too. *//* fallthrough */case BC_LOOP:case BC_ITERL:case BC_FUNCF:/* Patch bytecode of starting instruction in root trace. */setbc_op(pc, (int)op+(int)BC_JLOOP-(int)BC_LOOP);setbc_d(pc, traceno);addroot:/* Add to root trace chain in prototype. */J->cur.nextroot = pt->trace;pt->trace = (TraceNo1)traceno;break;case BC_RET:case BC_RET0:case BC_RET1:*pc = BCINS_AD(BC_JLOOP, J->cur.snap[0].nslots, traceno);goto addroot;case BC_JMP:/* Patch exit branch in parent to side trace entry. */lua_assert(J->parent != 0 && J->cur.root != 0);lj_asm_patchexit(J, traceref(J, J->parent), J->exitno, J->cur.mcode);/* Avoid compiling a side trace twice (stack resizing uses parent exit). */traceref(J, J->parent)->snap[J->exitno].count = SNAPCOUNT_DONE;/* Add to side trace chain in root trace. */{GCtrace *root = traceref(J, J->cur.root);root->nchild++;J->cur.nextside = root->nextside;root->nextside = (TraceNo1)traceno;}break;case BC_CALLM:case BC_CALL:case BC_ITERC:/* Trace stitching: patch link of previous trace. */traceref(J, J->exitno)->link = traceno;break;default:lua_assert(0);break;}/* Commit new mcode only after all patching is done. */lj_mcode_commit(J, J->cur.mcode);J->postproc = LJ_POST_NONE;trace_save(J, T);L = J->L;lj_vmevent_send(L, TRACE,setstrV(L, L->top++, lj_str_newlit(L, "stop"));setintV(L->top++, traceno);setfuncV(L, L->top++, J->fn););
}

它通过如下两个函数:

setbc_op(pc, (int)op+(int)BC_JLOOP-(int)BC_LOOP);
setbc_d(pc, traceno);

重新设置指令的opcode,即J_op = op + BC_JLOOP – BC_LOOP,那么如果将lj_bc.h中的指令随意打乱会影响到这里的正确性。

修改后的指令为:j_op  traceno

同时可以看到pt->trace字段记录的是一个traceno

pt->trace = (TraceNo1)traceno;

那么接下来看解释器中对JFORL的实现:

case BC_JFORI:case BC_JFORL:
#if !LJ_HASJITbreak;
#endifcase BC_FORI:case BC_IFORL:vk = (op == BC_IFORL || op == BC_JFORL);|  ins_AJ // RA = base, RD = target (after end of loop or start of loop)|  lea RA, [BASE+RA*8]if (LJ_DUALNUM) {|  cmp FOR_TIDX, LJ_TISNUM; jne >9if (!vk) {|  cmp FOR_TSTOP, LJ_TISNUM; jne ->vmeta_for|  cmp FOR_TSTEP, LJ_TISNUM; jne ->vmeta_for|  mov RB, dword FOR_IDX|  cmp dword FOR_STEP, 0; jl >5} else {
#ifdef LUA_USE_ASSERT|  cmp FOR_TSTOP, LJ_TISNUM; jne ->assert_bad_for_arg_type|  cmp FOR_TSTEP, LJ_TISNUM; jne ->assert_bad_for_arg_type
#endif|  mov RB, dword FOR_STEP|  test RB, RB; js >5|  add RB, dword FOR_IDX; jo >1|  mov dword FOR_IDX, RB}|  cmp RB, dword FOR_STOP|  mov FOR_TEXT, LJ_TISNUM|  mov dword FOR_EXT, RBif (op == BC_FORI) {|  jle >7|1:|6:|  branchPC RD} else if (op == BC_JFORI) {|  branchPC RD|  movzx RD, PC_RD|  jle =>BC_JLOOP|1:|6:} else if (op == BC_IFORL) {|  jg >7|6:|  branchPC RD|1:} else {|  jle =>BC_JLOOP|1:|6:}

当op = JFORL时,跳转到BC_JLOOP,如下:

case BC_JLOOP:|.if JIT|  ins_AD      // RA = base (ignored), RD = traceno|  mov RA, [DISPATCH+DISPATCH_J(trace)]|  mov TRACE:RD, [RA+RD*4]|  mov RDa, TRACE:RD->mcode|  mov L:RB, SAVE_L|  mov [DISPATCH+DISPATCH_GL(jit_base)], BASE|  mov [DISPATCH+DISPATCH_GL(tmpbuf.L)], L:RB|  // Save additional callee-save registers only used in compiled code.|.if X64WIN|  mov TMPQ, r12|  mov TMPa, r13|  mov CSAVE_4, r14|  mov CSAVE_3, r15|  mov RAa, rsp|  sub rsp, 9*16+4*8|  movdqa [RAa], xmm6|  movdqa [RAa-1*16], xmm7|  movdqa [RAa-2*16], xmm8|  movdqa [RAa-3*16], xmm9|  movdqa [RAa-4*16], xmm10|  movdqa [RAa-5*16], xmm11|  movdqa [RAa-6*16], xmm12|  movdqa [RAa-7*16], xmm13|  movdqa [RAa-8*16], xmm14|  movdqa [RAa-9*16], xmm15|.elif X64|  mov TMPQ, r12|  mov TMPa, r13|  sub rsp, 16|.endif|  jmp RDa|.endif
break;

先根据RD中保存的traceno获取到trace结构,并将trace结构中保存的机器码赋值在Rda中,进行堆栈转换后,jmp Rda直接跳转到机器码处执行。

在x86中,当字节码执行结束,继续执行下一个字节码时,都会使用ins_next块,它的定义如下:

|.macro ins_NEXT
|  mov RC, [PC]
|  movzx RA, RCH
|  movzx OP, RCL
|  add PC, 4
|  shr RC, 16
|.if X64
|  jmp aword [DISPATCH+OP*8]
|.else
|  jmp aword [DISPATCH+OP*4]
|.endif
|.endmacro

它从PC指向的字节码中获取了opcode,并跳转到DISPATCH + OP *4的地方执行,可以看出OP实质上保存的是数组的下标而这些数组元素都指向了vm_record汇编块:

|->vm_record:                        // Dispatch target for recording phase.|.if JIT|  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||->vm_rethook:               // Dispatch target for return hooks.|  movzx RD, byte [DISPATCH+DISPATCH_GL(hookmask)]|  test RDL, HOOK_ACTIVE            // Hook already active?|  jnz >5|  jmp >1||->vm_inshook:               // Dispatch target for instr/line hooks.|  movzx RD, byte [DISPATCH+DISPATCH_GL(hookmask)]|  test RDL, HOOK_ACTIVE            // Hook already active?|  jnz >5||  test RDL, LUA_MASKLINE|LUA_MASKCOUNT|  jz >5|  dec dword [DISPATCH+DISPATCH_GL(hookcount)]|  jz >1|  test RDL, LUA_MASKLINE|  jz >5|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, const BCIns *pc)|3:|  mov BASE, L:RB->base|4:|  movzx RA, PC_RA|5:|  movzx OP, PC_OP|  movzx RD, PC_RD|.if X64|  jmp aword [DISPATCH+OP*8+GG_DISP2STATIC]   // Re-dispatch to static ins.|.else|  jmp aword [DISPATCH+OP*4+GG_DISP2STATIC]   // Re-dispatch to static ins.|.endif

调用lj_dispatch_ins后,最终跳转到DISPATCH+OP*4+GG_DISP2STATIC这个地址继续执行,这个地址正是每个opcode对应的解释器汇编块。

Jit的正常运行还涉及堆栈状态的转换、jit模式到解释模式的跳转等(SSA守护代码),远不止这些。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • WebRTC协议下的视频汇聚融合技术:EasyCVR构建高效视频交互体验
  • Uniapp:WebSocket 重连之后累加触发 uni.onSocketOpen()
  • 2024/9/3黑马头条跟学笔记(一)
  • c/c++:CMakeLists.txt中添加编译/连接选项使用内存错误检测工具Address Sanitizer(ASan)
  • VM Workstation虚拟机AlmaLinux 9.4操作系统安装(桌面版安装详细教程)(宝塔面板的安装),填补CentOS终止支持维护的空白
  • 开源项目管理工具 Plane 安装和使用教程
  • opencv车道偏离系统-代码+原理-人工智能-自动驾驶
  • 【Next】3. 开发规范
  • 哪个编程工具让你的工作效率翻倍?
  • zhidianyun01/基于 ThinkPHP+Mysql 灵活用工+灵活用工源码+灵活用工平台源码
  • 怎样通过c51实现环境监测设计
  • shell脚本—————局域网IP扫描
  • vscode常用插件及设置
  • 在繁忙工作环境中提升开发效率:JetBrains IntelliJ IDEA 的应用
  • Java异常处理-如何选择异常类型
  • CentOS 7 防火墙操作
  • css的样式优先级
  • Docker容器管理
  • Laravel Mix运行时关于es2015报错解决方案
  • PHP的类修饰符与访问修饰符
  • php面试题 汇集2
  • Python - 闭包Closure
  • Spring Cloud Alibaba迁移指南(一):一行代码从 Hystrix 迁移到 Sentinel
  • TiDB 源码阅读系列文章(十)Chunk 和执行框架简介
  • Travix是如何部署应用程序到Kubernetes上的
  • 大主子表关联的性能优化方法
  • 订阅Forge Viewer所有的事件
  • 翻译--Thinking in React
  • 驱动程序原理
  • 如何合理的规划jvm性能调优
  • 如何解决微信端直接跳WAP端
  • 新版博客前端前瞻
  • 【运维趟坑回忆录 开篇】初入初创, 一脸懵
  • ​Spring Boot 分片上传文件
  • # 睡眠3秒_床上这样睡觉的人,睡眠质量多半不好
  • #NOIP 2014# day.1 生活大爆炸版 石头剪刀布
  • (14)学习笔记:动手深度学习(Pytorch神经网络基础)
  • (4)通过调用hadoop的java api实现本地文件上传到hadoop文件系统上
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第5节(封闭类和Final方法)
  • (补充):java各种进制、原码、反码、补码和文本、图像、音频在计算机中的存储方式
  • (二刷)代码随想录第15天|层序遍历 226.翻转二叉树 101.对称二叉树2
  • (附源码)springboot宠物管理系统 毕业设计 121654
  • (附源码)ssm本科教学合格评估管理系统 毕业设计 180916
  • (附源码)计算机毕业设计SSM疫情居家隔离服务系统
  • (已解决)报错:Could not load the Qt platform plugin “xcb“
  • (原+转)Ubuntu16.04软件中心闪退及wifi消失
  • ***原理与防范
  • .bat批处理(五):遍历指定目录下资源文件并更新
  • .helper勒索病毒的最新威胁:如何恢复您的数据?
  • .mysql secret在哪_MYSQL基本操作(上)
  • .Net Core 笔试1
  • .NET Entity FrameWork 总结 ,在项目中用处个人感觉不大。适合初级用用,不涉及到与数据库通信。
  • .NET Framework 3.5中序列化成JSON数据及JSON数据的反序列化,以及jQuery的调用JSON
  • .NET 中 GetHashCode 的哈希值有多大概率会相同(哈希碰撞)
  • .NET项目中存在多个web.config文件时的加载顺序