Add bytecode inlining at optimization >= 2.

Currently very simple hueristic, and not supported
on many functions that instatiate closures or load/set upvalues.

We also now, more than ever, need a slot-compaction routine to reduce
the number of slots used by functions.
This commit is contained in:
Calvin Rose
2026-09-24 06:19:38 -05:00
parent 6dadb83570
commit b0acda4fa5
8 changed files with 292 additions and 18 deletions
+162 -8
View File
@@ -541,8 +541,8 @@ void janet_bytecode_local_value_numbering(JanetFuncDef *def, BytecodeBB *blocks,
uint32_t *code = def->bytecode;
for (int32_t blocki = 0; blocki < n_blocks; blocki++) {
BytecodeBB block = blocks[blocki];
int32_t nextv = 1; /* Reserve negative values */
for (int32_t i = 0; i < def->slotcount; i++) ctx.value_numbers[i] = 0; /* initialize to unknown */
int32_t nextv = 1 + def->slotcount; /* Reserve negative values */
for (int32_t i = 0; i < def->slotcount; i++) ctx.value_numbers[i] = i + 1; /* initialize to some unique value */
janet_table_clear(ctx.value_lookup);
for (int32_t pc = block.start; pc < block.end; pc++) {
uint32_t A = (code[pc] & 0xFF00U) >> 8;
@@ -781,18 +781,18 @@ void janet_bytecode_local_value_numbering(JanetFuncDef *def, BytecodeBB *blocks,
case JOP_SHIFT_RIGHT_UNSIGNED_IMMEDIATE:
case JOP_GET_INDEX: {
int32_t Bv = ctx.value_numbers[B];
int16_t C_imm = (int8_t) C; /* To 16 bit and sign extend */
uint16_t encoded_C = (uint16_t)C_imm;
int32_t Cv = -4 - (int32_t)encoded_C;
/* Check constant prop */
{
int16_t C_imm = (int8_t) C; /* To 16 bit and sign extend */
uint16_t encoded_C = (uint16_t)C_imm;
int32_t Cv = -4 - (int32_t)encoded_C;
VN check_const_prop = vn_const_prop(&ctx, opcode, Bv, Cv);
if (check_const_prop.value_number) {
code[pc] = vn_move_or_load(&ctx, A, check_const_prop, code[pc]);
continue;
}
}
double key = vn_encode_2arg(opcode, Bv, C);
double key = vn_encode_2arg(opcode, Bv, Cv);
VN vn = vn_check_key(&ctx, key);
if (vn.value_number) {
code[pc] = vn_move_or_load(&ctx, A, vn, code[pc]);
@@ -800,7 +800,11 @@ void janet_bytecode_local_value_numbering(JanetFuncDef *def, BytecodeBB *blocks,
}
B = vn_find_reg(&ctx, Bv, B);
code[pc] = recon_abc(code[pc], A, B, C);
ctx.value_numbers[A] = nextv++;
VN vnnext;
vnnext.has_slot = 1;
vnnext.slot = A;
vnnext.value_number = nextv++;
vn_add_lookup(&ctx, key, vnnext);
continue;
}
@@ -1108,6 +1112,13 @@ void janet_bytecode_jump_threading(JanetFuncDef *def) {
switch (code[i] & 0x7F) {
default:
continue;
case JOP_MOVE_NEAR:
case JOP_MOVE_FAR:
if (((codes[i] >> 8) & 0xFF) == (codes[i] >> 16)) {
/* Not really jump threading, but an easy optimization */
code[i] = JOP_NOOP;
}
continue;
case JOP_JUMP:
target = i + (codes[i] >> 8);
break;
@@ -1219,7 +1230,6 @@ static void janet_bytecode_movopt_basic_block(JanetFuncDef *def, BytecodeBB bb,
pin = 1;
break;
/* Read A */
case JOP_ERROR:
case JOP_TYPECHECK:
case JOP_JUMP_IF:
case JOP_JUMP_IF_NOT:
@@ -1231,6 +1241,7 @@ static void janet_bytecode_movopt_basic_block(JanetFuncDef *def, BytecodeBB bb,
ins[0] = Ia;
break;
/* Read D */
case JOP_ERROR:
case JOP_RETURN:
case JOP_PUSH:
case JOP_PUSH_ARRAY:
@@ -1435,12 +1446,155 @@ void janet_bytecode_movopt_full(JanetFuncDef *def, BytecodeBB *blocks, int32_t n
janet_free(bitsets);
}
/*
* Inlining
*/
/* Rewrite a chunk of code for inline use. Returns a vector of uint32_ts to be freed with janet_v_free.
* Use dest_slot = -1 to mean an inline tail call.
* TODO - replace some of the asserts with return NULL to indicate inlining failed.
* */
uint32_t *janet_bytecode_inline_chunk(JanetFuncDef *idef,
int32_t dest_slot, uint32_t slot_delta, uint32_t const_delta) {
int is_tail = dest_slot < 0;
uint32_t *icode = idef->bytecode;
uint32_t *code = NULL;
int32_t *jump_pcs = NULL;
int32_t *remap = NULL;
/* Skip functions that are too hard for now */
janet_assert(slot_delta < 256, "slot delta too big");
janet_assert(idef->defs_length == 0, "no inlining closure functions");
janet_assert(idef->environments_length == 0, "no inlining functions that reference closure environments");
/* Patch instructions with slot shift and constant index shift. */
for (int32_t pc = 0; pc < idef->bytecode_length; pc++) {
enum JanetInstructionType itype = janet_instructions[icode[pc] & 0x7F];
uint32_t newi = icode[pc];
/* rewrite slots. For now, assume adding slot delta will not overflow any slot indices */
switch (itype) {
case JINT_SES:
janet_assert(0, "upvalue instruction not supported for inlining");
case JINT_0:
case JINT_L:
break;
case JINT_S:
janet_assert((newi & 0x7F) != JOP_LOAD_SELF, "cannot inline lds instruction");
/* fallthrough */
case JINT_SI:
case JINT_SU:
case JINT_ST:
case JINT_SL:
case JINT_SD:
newi += slot_delta << 8;
break;
case JINT_SC:
newi += slot_delta << 8 | (const_delta << 16);
break;
case JINT_SS:
case JINT_SSI:
case JINT_SSU:
newi += (slot_delta << 8) | (slot_delta << 16);
break;
case JINT_SSS:
newi += (slot_delta << 8) | (slot_delta << 16) | (slot_delta << 24);
break;
}
if (is_tail) {
janet_v_push(code, newi);
} else {
/* Account for new locations for jump targets */
janet_v_push(remap, janet_v_count(code));
/* Replace returns with move and jump to end of the block
* Also fix tail calls that are no longer in tail position. */
switch (icode[pc] & 0x7F) {
default:
janet_v_push(code, newi);
break;
case JOP_RETURN: {
uint32_t retreg = newi >> 8;
janet_assert(retreg < 0x10000U, "retreg too big");
janet_v_push(code, JOP_MOVE_NEAR | (dest_slot << 8) | (retreg << 16));
if (pc < idef->bytecode_length - 1) {
/* Last instruction doesn't need a jump. Jump-
* threading would take care of this, but why not
* generate better code earlier? */
janet_v_push(jump_pcs, janet_v_count(code));
janet_v_push(code, JOP_JUMP);
}
}
break;
case JOP_RETURN_NIL: {
janet_v_push(code, JOP_LOAD_NIL | (dest_slot << 8));
if (pc < idef->bytecode_length - 1) { /* Ditto */
janet_v_push(jump_pcs, janet_v_count(code));
janet_v_push(code, JOP_JUMP);
}
}
break;
case JOP_TAILCALL: {
/* Convert to a normal call and jump */
uint32_t callee = newi >> 8;
janet_assert(callee < 0x10000U, "callee too big");
janet_v_push(code, JOP_CALL | (dest_slot << 8) | (callee << 16));
if (pc < idef->bytecode_length - 1) { /* Ditto */
janet_v_push(jump_pcs, janet_v_count(code));
janet_v_push(code, JOP_JUMP);
}
}
break;
}
}
}
if (!is_tail) {
janet_assert(janet_v_count(remap) == idef->bytecode_length, "bad remap");
/* Fix jumps to account for the inserted jumps */
for (int32_t old_pc = 0; old_pc < idef->bytecode_length; old_pc++) {
switch (idef->bytecode[old_pc] & 0x7F) {
default:
continue;
case JOP_JUMP_IF:
case JOP_JUMP_IF_NOT:
case JOP_JUMP_IF_NOT_NIL:
case JOP_JUMP_IF_NIL: {
int32_t pc = remap[old_pc];
int32_t old_jump = ((int32_t)(idef->bytecode[old_pc])) >> 16;
int32_t new_jump = remap[old_pc + old_jump] - pc;
code[pc] = (code[pc] & 0xFFFFU) | (uint32_t)(new_jump << 16);
}
continue;
case JOP_JUMP: {
int32_t pc = remap[old_pc];
int32_t old_jump = ((int32_t)(idef->bytecode[old_pc])) >> 8;
int32_t new_jump = remap[old_pc + old_jump] - pc;
code[pc] = JOP_JUMP | (uint32_t)(new_jump << 8);
}
continue;
}
}
janet_v_free(remap);
}
/* Now fix jumps to jump to the end of the generated block */
while (janet_v_count(jump_pcs)) {
int32_t pc = janet_v_last(jump_pcs);
janet_v_pop(jump_pcs);
int32_t target = janet_v_count(code) - pc;
janet_assert(target > 0, "backwards jump");
code[pc] = JOP_JUMP | ((uint32_t)target << 8);
}
janet_v_free(jump_pcs);
return code;
}
/* Entry point for optimization. The def should already be valid as determined by janet_verify.
* The compiler may skip this check for performance but the assembler will not. */
void janet_bytecode_optimize(JanetFuncDef *def, int32_t level) {
int32_t delta;
#ifdef JANET_DEBUG
int result = janet_verify(def);
if (result != 0) {
for (int32_t i = 0; i < def->bytecode_length; i++) {
janet_eprintf("%4d: %Q\n", i, janet_asm_decode_instruction(def->bytecode[i]));
}
}
janet_assert(result == 0, "input bytecode bad");
#endif
if (level >= 0) {
+87 -2
View File
@@ -28,6 +28,7 @@
#include "vector.h"
#include "util.h"
#include "state.h"
#include "regalloc.h"
#endif
JanetFopts janetc_fopts_default(JanetCompiler *c) {
@@ -608,6 +609,24 @@ void janetc_throwaway(JanetFopts opts, Janet x) {
}
}
static int function_can_inline(JanetFopts opts, JanetFunction *func, JanetSlot *slots) {
JanetFuncDef *def = func->def;
/* TODO - hoist this check to a flag on the func def */
if (opts.compiler->optimize < 2) return 0;
if (def->environments_length) return 0;
if (def->defs_length) return 0;
if (def->slotcount > 32) return 0;
if (def->bytecode_length > 40) return 0;
if (def->flags & (JANET_FUNCDEF_FLAG_NAMEDARGS | JANET_FUNCDEF_FLAG_STRUCTARG | JANET_FUNCDEF_FLAG_VARARG)) return 0;
if (janet_v_count(slots) != def->min_arity) return 0; /* TODO - optional arguments */
if (janet_v_count(slots) != def->max_arity) return 0;
/* Check for JOP_LOAD_SELF */
for (int32_t i = 0; i < def->bytecode_length; i++) {
if ((def->bytecode[i] & 0x7F) == JOP_LOAD_SELF) return 0;
}
return 1;
}
/* Compile a call or tailcall instruction */
static JanetSlot janetc_call(JanetFopts opts, JanetSlot *slots, JanetSlot fun, const Janet *form) {
JanetSlot retslot;
@@ -620,9 +639,76 @@ static JanetSlot janetc_call(JanetFopts opts, JanetSlot *slots, JanetSlot fun, c
if (o && (!o->can_optimize || o->can_optimize(opts, slots))) {
specialized = 1;
retslot = o->optimize(opts, slots);
} else if (function_can_inline(opts, f, slots)) {
/* Function inlining */
int is_tail;
JanetSlot target;
if ((opts.flags & JANET_FOPTS_TAIL) &&
/* Prevent top level tail calls for better errors */
!(c->scope->flags & JANET_SCOPE_TOP)) {
is_tail = 1;
} else {
is_tail = 0;
}
/* contiguous chunk of fun->def->slotcount slots */
if (!is_tail) target = janetc_gettarget(opts);
int32_t nslots = f->def->slotcount;
int32_t slot_delta = janetc_regalloc_n(&c->scope->ra, nslots);
if (slot_delta + f->def->slotcount < 256) {
/*janet_eprintf("inlining %V\n", janet_wrap_function(f));*/
/* Limit inlining when too many slots */
JanetScope *scope = c->scope;
while (scope) {
if (scope->flags & JANET_SCOPE_FUNCTION)
break;
scope = scope->parent;
}
/* Setup the function parameters */
JanetSlot dest;
dest.envindex = -1;
dest.flags = JANET_SLOTTYPE_ANY;
dest.constant = janet_wrap_nil();
for (int32_t i = 0; i < janet_v_count(slots); i++) {
dest.index = slot_delta + i;
janetc_copy(c, dest, slots[i]);
}
/* Initialize other slots to nil just like in a normal function call */
/* TODO - add pass to remove these if needed so we can add this back.
* The current compiler doesn't rely on nil-initialization behavior but might in the future
* (e.g. removing redundant ldn at the beginning of a function.
for (int32_t i = janet_v_count(slots); i < nslots; i++) {
janetc_emit(c, JOP_LOAD_NIL | ((uint32_t)(i + slot_delta) << 8));
}
*/
/* Do main inlining (patch the bytecode) */
int32_t const_delta = janet_v_count(scope->consts); /* Get const delta _after_ moving slots. janetc_copy can add constants here! */
uint32_t *insert_code = janet_bytecode_inline_chunk(f->def, is_tail ? -1 : target.index, slot_delta, const_delta);
for (int32_t i = 0; i < janet_v_count(insert_code); i++) {
/* TODO - better match up source code mapping - e.g. if inlined function
* is from the same source file */
janetc_emit(c, insert_code[i]);
}
/* Copy constants into our constants buffer */
for (int32_t i = 0; i < f->def->constants_length; i++) {
janet_v_push(scope->consts, f->def->constants[i]);
}
janet_v_free(insert_code);
specialized = 1;
if (is_tail) {
retslot = janetc_cslot(janet_wrap_nil());
retslot.flags = JANET_SLOT_RETURNED;
} else {
retslot = target;
}
} else {
/*janet_eprintf("inlining failed %V\n", janet_wrap_function(f));*/
;
}
janetc_regalloc_free_n(&c->scope->ra, slot_delta, nslots);
}
}
/* TODO janet function inlining (no c functions)*/
}
if (!specialized) {
int32_t min_arity = janetc_pushslots(c, slots);
@@ -937,7 +1023,6 @@ JanetSlot janetc_value(JanetFopts opts, Janet x) {
} else {
/* Function calls */
JanetSlot head = janetc_value(subopts, tup[0]);
subopts.flags = JANET_FUNCTION | JANET_CFUNCTION;
ret = janetc_call(opts, janetc_toslots(c, tup + 1, janet_tuple_length(tup) - 1), head, tup);
janetc_freeslot(c, head);
}
+4
View File
@@ -288,6 +288,10 @@ JanetSlot janetc_resolve(JanetCompiler *c, const uint8_t *sym);
/* Check if a symbol is already in scope for shadowing lints */
Shadowing janetc_shadowcheck(JanetCompiler *c, const uint8_t *sym);
/* Inlining. We can do inling both from bytecode and from our frontend */
uint32_t *janet_bytecode_inline_chunk(JanetFuncDef *idef,
int32_t dest_slot, uint32_t slot_delta, uint32_t const_delta);
/* Bytecode optimization */
void janet_bytecode_optimize(JanetFuncDef *def, int32_t level);
+27
View File
@@ -133,6 +133,29 @@ int32_t janetc_regalloc_1(JanetcRegisterAllocator *ra) {
return reg;
}
/* Find n contiguous registers but do not allocate them. */
int32_t janetc_regalloc_findn(JanetcRegisterAllocator *ra, int32_t n) {
int32_t run = 0;
int32_t i;
for (i = 0; i < (ra->count * 32); i++) {
if (ra->chunks[i >> 5] & ithbit(i & 0x1F)) {
run = 0;
} else {
run++;
}
if (run == n) return 1 + i - n;
}
return 1 + i - run;
}
int32_t janetc_regalloc_n(JanetcRegisterAllocator *ra, int32_t n) {
int32_t loc = janetc_regalloc_findn(ra, n);
for (int32_t i = 0; i < n; i++) janetc_regalloc_touch(ra, loc + i);
if (loc + n - 1 > ra->max)
ra->max = loc + n - 1;
return loc;
}
/* Free a register. The register must have been previously allocated
* without being freed. */
void janetc_regalloc_free(JanetcRegisterAllocator *ra, int32_t reg) {
@@ -141,6 +164,10 @@ void janetc_regalloc_free(JanetcRegisterAllocator *ra, int32_t reg) {
ra->chunks[chunk] &= ~ithbit(bit);
}
void janetc_regalloc_free_n(JanetcRegisterAllocator *ra, int32_t loc, int32_t n) {
for (int32_t i = 0; i < n; i++) janetc_regalloc_free(ra, loc + i);
}
/* Check if a register is set. */
int janetc_regalloc_check(JanetcRegisterAllocator *ra, int32_t reg) {
int32_t chunk = reg >> 5;
+2
View File
@@ -57,5 +57,7 @@ void janetc_regalloc_freetemp(JanetcRegisterAllocator *ra, int32_t reg, JanetcRe
void janetc_regalloc_clone(JanetcRegisterAllocator *dest, JanetcRegisterAllocator *src);
void janetc_regalloc_touch(JanetcRegisterAllocator *ra, int32_t reg);
int janetc_regalloc_check(JanetcRegisterAllocator *ra, int32_t reg);
int32_t janetc_regalloc_n(JanetcRegisterAllocator *ra, int32_t n);
void janetc_regalloc_free_n(JanetcRegisterAllocator *ra, int32_t loc, int32_t n);
#endif
+6 -4
View File
@@ -964,8 +964,9 @@ static JanetSlot janetc_while(JanetFopts opts, int32_t argn, const Janet *argv)
janet_def_addflags(def);
int32_t defindex = janetc_addfuncdef(c, def);
/* Do basic optimization */
int32_t optimize = (c->scope->flags & JANET_SCOPE_UNUSED) ? -1 : c->optimize;
janet_bytecode_optimize(def, optimize);
if (!(c->scope->flags & JANET_SCOPE_UNUSED)) {
janet_bytecode_optimize(def, c->optimize);
}
/* And then load the closure and call it. */
int32_t cloreg = janetc_regalloc_temp(&c->scope->ra, JANETC_REGTEMP_0);
janetc_emit(c, JOP_CLOSURE | ((uint32_t) cloreg << 8) | ((uint32_t) defindex << 16));
@@ -1026,7 +1027,6 @@ static JanetSlot janetc_fn(JanetFopts opts, int32_t argn, const Janet *argv) {
/* Begin function */
c->scope->flags |= JANET_SCOPE_CLOSURE;
int32_t optimize = (c->scope->flags & JANET_SCOPE_UNUSED) ? -1 : c->optimize;
janetc_scope(&fnscope, c, JANET_SCOPE_FUNCTION, "function");
if (argn == 0) {
@@ -1217,7 +1217,9 @@ static JanetSlot janetc_fn(JanetFopts opts, int32_t argn, const Janet *argv) {
if (arity + vararg > def->slotcount) def->slotcount = arity + vararg;
/* Do basic optimization */
janet_bytecode_optimize(def, optimize);
if (!(fnscope.flags & JANET_SCOPE_UNUSED)) {
janet_bytecode_optimize(def, c->optimize);
}
/* Instantiate closure */
ret = janetc_gettarget(opts);
+1 -1
View File
@@ -669,7 +669,7 @@ static JanetSignal run_vm(JanetFiber *fiber, Janet in) {
vm_pcnext();
VM_OP(JOP_ERROR)
vm_return(JANET_SIGNAL_ERROR, stack[A]);
vm_return(JANET_SIGNAL_ERROR, stack[D]);
VM_OP(JOP_TYPECHECK)
vm_assert_types(stack[A], E);
+3 -3
View File
@@ -118,7 +118,7 @@
(defn check-good-compile
[code msg]
(each opt [-1 0 1]
(each opt [0 1 2 3]
(setdyn *optimize* opt)
(def lints @[])
(def result4 (compile code (curenv) "suite-compile.janet" lints))
@@ -126,7 +126,7 @@
(defn check-bad-compile
[code msg]
(each opt [-1 0 1]
(each opt [0 1 2 3]
(setdyn *optimize* opt)
(def lints @[])
(def result4 (compile code (curenv) "suite-compile.janet" lints))
@@ -134,7 +134,7 @@
(defn check-lint-compile
[code msg]
(each opt [-1 0 1]
(each opt [0 1 2 3]
(setdyn *optimize* opt)
(def lints @[])
(def result4 (compile code (curenv) "suite-compile.janet" lints))