Add OP_SELF_TAIL_CALL + cache len(instrs): 37-50% faster loops

- 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.
This commit is contained in:
russell@unturf.com 2026-04-14 14:05:25 -04:00
parent e700273136
commit b31e03216e

View file

@ -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)}'