From b31e03216e7a59c19d7ac5e7bd26d2a0a267d55a Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 14 Apr 2026 14:05:25 -0400 Subject: [PATCH] Add OP_SELF_TAIL_CALL + cache len(instrs): 37-50% faster loops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OP_SELF_TAIL_CALL: named-let loops reuse env instead of allocating new child env each iteration. Eliminates 50,000 dict allocations per sum-to(50000). - Cache n_instrs = len(instrs): avoid per-dispatch len() call. Updated in all code-switching paths (CALL, TAIL_CALL, RETURN, CALL_CC, continuation resume). - Combined with LOOK_ADD1/LOOK_SUB1 from previous commit: sum-to: 99x → 50x, ackermann: 79x → 50x, fib-rec: 85x → 63x. - Serialization updated for new opcode operand format. - 571 tests green. --- uncommonlisp.py | 50 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/uncommonlisp.py b/uncommonlisp.py index fd612f1..9f0f5fe 100644 --- a/uncommonlisp.py +++ b/uncommonlisp.py @@ -1116,6 +1116,7 @@ OP_LOOK_ADD1 = 81 # lookup + increment: arg = sym OP_LOOK_SUB1 = 82 # lookup + decrement: arg = sym OP_CONST_EQ_JF = 83 # push const, compare TOS, branch: arg = (const, jump_addr) OP_LOOK_CONST_CALL2 = 84 # lookup func, push const, call(2): arg = (sym, const) +OP_SELF_TAIL_CALL = 85 # self-recursive tail call (reuse env): arg = (n_args, params_tuple) # Specialization table: {symbol: {arity: opcode}} _BC_SPECIALIZE = { @@ -1169,7 +1170,7 @@ _BC_FOLDABLE = { class CodeObj: """Compiled bytecode chunk.""" - __slots__ = ('instrs', 'name', 'source_map', 'ic', '_cur_line') + __slots__ = ('instrs', 'name', 'source_map', 'ic', '_cur_line', '_self_name', '_self_params') def __init__(self, name=None): self.instrs = []; self.name = name self.source_map = [] # parallel to instrs: line number or None @@ -1363,7 +1364,8 @@ def _bc(expr, code, env, tail=False): # Named let: (let loop ((v init)...) body...) name = a[0]; binds = _L(a[1]); body = a[2:] bps = [_L(b)[0] for b in binds] - inner = _bc_lambda(body, bps, None, env, name=str(name)) + inner = _bc_lambda(body, bps, None, env, name=str(name), + self_name=str(name), self_params=bps) code.emit(OP_PUSH_ENV) code.emit(OP_MAKE_CLOSURE, (inner, bps, None)) code.emit(OP_DUP) @@ -1490,6 +1492,12 @@ def _bc(expr, code, env, tail=False): for arg in call_args: _bc(arg, code, env) code.emit(spec[n]); return + # --- Self tail call optimization --- + if tail and isinstance(head, Symbol) and hasattr(code, '_self_name') and str(head) == code._self_name: + params = code._self_params + for arg in call_args: _bc(arg, code, env) + code.emit(OP_SELF_TAIL_CALL, (len(call_args), tuple(params))); return + # --- Function call --- _bc(head, code, env) for arg in call_args: _bc(arg, code, env) @@ -1503,9 +1511,12 @@ def _bc_body(body, code, env, tail=False): _bc(body[-1], code, env, tail=tail) -def _bc_lambda(body, params, rest, env, name=None): +def _bc_lambda(body, params, rest, env, name=None, self_name=None, self_params=None): """Compile a lambda body into a CodeObj.""" inner = CodeObj(name=name) + if self_name: + inner._self_name = self_name + inner._self_params = self_params # Handle internal defines (letrec* semantics) def_names = [] body_list = list(body) @@ -1622,7 +1633,8 @@ def vm_exec(code, env): c = ci.cont frames = [(i, p, _deep_copy_env(e), list(s)) for i, p, e, s in c.frames] stack = list(c.stack); stack.append(ci.val) - ip = c.ip; instrs = c.instrs; env = _deep_copy_env(c.env) + ip = c.ip; instrs = c.instrs; n_instrs = len(instrs) + env = _deep_copy_env(c.env) smap = None except LispErr as e: if e.source_line is None and smap and ip > 0 and ip - 1 < len(smap): @@ -1636,7 +1648,8 @@ def _vm_loop(instrs, ip, stack, env, frames, vm_id): _ap = stack.append; _po = stack.pop _isinstance = isinstance; _CP = CompiledProc; _Pr = Proc _ic = {} # inline cache: {instr_idx: (cached_env, cached_val)} - while ip < len(instrs): + n_instrs = len(instrs) + while ip < n_instrs: op, arg = instrs[ip]; ip += 1 if op == OP_CONST: _ap(arg) elif op == OP_LOOKUP: @@ -1677,7 +1690,7 @@ def _vm_loop(instrs, ip, stack, env, frames, vm_id): if _isinstance(func, _CP): frames.append((instrs, ip, env, stack)) env = func.env.child(func.params, func.rest, args_) - instrs = func.code.instrs; ip = 0 + instrs = func.code.instrs; ip = 0; n_instrs = len(instrs) stack = []; _ap = stack.append; _po = stack.pop continue elif _isinstance(func, _Pr): @@ -1692,7 +1705,7 @@ def _vm_loop(instrs, ip, stack, env, frames, vm_id): func = _po() if _isinstance(func, _CP): env = func.env.child(func.params, func.rest, args_) - instrs = func.code.instrs; ip = 0 + instrs = func.code.instrs; ip = 0; n_instrs = len(instrs) stack.clear() if _portal_checkpoint[0] is not None: _check_portal_checkpoint(instrs, ip, stack, env, frames, vm_id) @@ -1703,20 +1716,20 @@ def _vm_loop(instrs, ip, stack, env, frames, vm_id): for e in body[:-1]: leval(e, c) ret = leval(body[-1], c) if not frames: return ret - instrs, ip, env, stack = frames.pop() + instrs, ip, env, stack = frames.pop(); n_instrs = len(instrs) _ap = stack.append; _po = stack.pop _ap(ret); continue elif callable(func): ret = func(args_, env) if not frames: return ret - instrs, ip, env, stack = frames.pop() + instrs, ip, env, stack = frames.pop(); n_instrs = len(instrs) _ap = stack.append; _po = stack.pop _ap(ret); continue else: raise LispErr(f'not callable: {show(func)}') elif op == OP_RETURN: ret = _po() if stack else VOID if not frames: return ret - instrs, ip, env, stack = frames.pop() + instrs, ip, env, stack = frames.pop(); n_instrs = len(instrs) _ap = stack.append; _po = stack.pop _ap(ret); continue elif op == OP_MAKE_CLOSURE: @@ -1732,7 +1745,7 @@ def _vm_loop(instrs, ip, stack, env, frames, vm_id): if _isinstance(proc, _CP): frames.append((instrs, ip, env, stack)) env = proc.env.child(proc.params, proc.rest, [cont]) - instrs = proc.code.instrs; ip = 0 + instrs = proc.code.instrs; ip = 0; n_instrs = len(instrs) stack = []; _ap = stack.append; _po = stack.pop continue elif _isinstance(proc, _Pr): @@ -1772,6 +1785,13 @@ def _vm_loop(instrs, ip, stack, env, frames, vm_id): elif op == OP_CONST_EQ_JF: c, addr = arg if _po() != c: ip = addr + elif op == OP_SELF_TAIL_CALL: + n, params = arg + if n: args_ = stack[-n:]; del stack[-n:] + else: args_ = [] + b = env.b + for p, a in zip(params, args_): b[p] = a + ip = 0; stack.clear(); continue elif op == OP_LOOK_CONST_CALL2: sym, c = arg func = env.lookup(sym) @@ -1779,7 +1799,7 @@ def _vm_loop(instrs, ip, stack, env, frames, vm_id): frames.append((instrs, ip, env, stack)) env = func.env.child(func.params, func.rest, [stack[-1], c]) del stack[-1:] - instrs = func.code.instrs; ip = 0 + instrs = func.code.instrs; ip = 0; n_instrs = len(instrs) stack = []; _ap = stack.append; _po = stack.pop continue elif callable(func): @@ -1822,6 +1842,9 @@ def _serialize_operand(val): return {'t': 'closure', 'code': _serialize_code(code), 'params': [str(p) for p in params], 'rest': str(rest) if rest else None} + # OP_SELF_TAIL_CALL: (n_args, params_tuple) + if len(val) == 2 and isinstance(val[0], int) and isinstance(val[1], tuple): + return {'t': 'stc', 'n': val[0], 'p': [str(p) for p in val[1]]} return {'t': 'repr', 'v': repr(val)} def _deserialize_operand(data): @@ -1851,6 +1874,8 @@ def _deserialize_operand(data): params = [S(p) for p in data['params']] rest = S(data['rest']) if data['rest'] else None return (code, params, rest) + if t == 'stc': + return (data['n'], tuple(S(p) for p in data['p'])) return data def _serialize_code(code): @@ -2228,6 +2253,7 @@ def _disassemble(proc): OP_VEC_REF: 'VEC_REF', OP_VEC_SET: 'VEC_SET', OP_LOOK_LOOK: 'LOOK²', OP_LOOK_ADD1: 'LOOK+1', OP_LOOK_SUB1: 'LOOK-1', OP_CONST_EQ_JF: 'CONST=JF', OP_LOOK_CONST_CALL2: 'LOOK_C_CALL2', + OP_SELF_TAIL_CALL: 'SELF_TCALL', } lines = [f'--- {proc.name or "λ"} ' f'({" ".join(str(p) for p in proc.params)}'