lumbda/bench.py
russell@unturf.com f584ad76f2 Add auto-compile, VM optimization, disassemble, call/cc compilation
- auto-compile! parameter: compile defines on the fly for zero-effort speedup
- VM optimization: local aliases, inlined truthiness checks (~15% faster)
- disassemble builtin: human-readable bytecode listing
- call/cc compiled natively in VM (escape-only, no longer falls back to eval)
- Makefile: add lint, bench-verbose, clean targets
- 518 tests green
2026-04-13 14:49:32 -04:00

449 lines
14 KiB
Python

#!/usr/bin/env python3
"""
bench.py — benchmarks for uncommonlisp.
Compares interpreter time against equivalent CPython.
Usage: python3 bench.py [-v]
"""
import sys, time, math
sys.setrecursionlimit(10000)
from uncommonlisp import make_global_env, read_all, leval, PRELUDE, show, bc_compile_proc, Proc
VERBOSE = '-v' in sys.argv
def fresh():
g = make_global_env()
for e in read_all(PRELUDE): leval(e, g)
return g
def run(src, env):
result = None
for e in read_all(src): result = leval(e, env)
return result
def bench(name, lisp_src, python_fn, iters=3):
"""Time both lisp and python implementations; print results."""
env = fresh()
# Warm up
run(lisp_src, env)
python_fn()
# Time Lisp
lisp_times = []
for _ in range(iters):
env2 = fresh()
t = time.perf_counter()
result = run(lisp_src, env2)
lisp_times.append(time.perf_counter() - t)
lisp_best = min(lisp_times)
# Time Python
py_times = []
for _ in range(iters):
t = time.perf_counter()
py_result = python_fn()
py_times.append(time.perf_counter() - t)
py_best = min(py_times)
slowdown = lisp_best / py_best if py_best > 0 else float('inf')
status = '' if result == py_result or str(result) == str(py_result) else ''
print(f'{status} {name:<30} lisp={lisp_best*1000:7.1f}ms py={py_best*1000:7.1f}ms '
f'ratio={slowdown:5.1f}x')
if VERBOSE:
print(f' lisp result: {show(result)!r}')
print(f' py result: {py_result!r}')
return lisp_best, py_best
print('uncommonlisp benchmarks')
print('=' * 75)
# ── 1. Fibonacci (iterative, named let) ──────────────────────────────────────
bench(
'fib(35) named-let',
'''
(define (fib n)
(let loop ((a 0) (b 1) (i 0))
(if (= i n) a (loop b (+ a b) (+ i 1)))))
(fib 35)
''',
lambda: (lambda a=0, b=1, i=0, n=35:
[setattr(sys.modules[__name__], '_fib', None)] and
(lambda: exec('''
def _fib(n):
a, b = 0, 1
for _ in range(n): a, b = b, a+b
return a
''', globals()) or _fib(35))())()
)
# Simpler lambda-based benchmark
def py_fib(n=35):
a, b = 0, 1
for _ in range(n): a, b = b, a + b
return a
bench(
'fib(35) iterative',
'''
(define (fib n)
(let loop ((a 0) (b 1) (i 0))
(if (= i n) a (loop b (+ a b) (+ i 1)))))
(fib 35)
''',
lambda: py_fib(35)
)
# ── 2. fib(20) tree-recursive ─────────────────────────────────────────────────
def py_fib_rec(n):
if n <= 1: return n
return py_fib_rec(n-1) + py_fib_rec(n-2)
bench(
'fib(20) tree-recursive',
'''
(define (fib n)
(if (<= n 1) n (+ (fib (- n 1)) (fib (- n 2)))))
(fib 20)
''',
lambda: py_fib_rec(20)
)
# ── 3. Tak function ───────────────────────────────────────────────────────────
def py_tak(x, y, z):
if y >= x: return z
return py_tak(py_tak(x-1,y,z), py_tak(y-1,z,x), py_tak(z-1,x,y))
bench(
'tak(15,10,6)',
'''
(define (tak x y z)
(if (>= y x) z
(tak (tak (- x 1) y z)
(tak (- y 1) z x)
(tak (- z 1) x y))))
(tak 15 10 6)
''',
lambda: py_tak(15, 10, 6)
)
# ── 4. Tail-recursive sum ─────────────────────────────────────────────────────
def py_sum(n):
acc = 0
while n > 0: acc += n; n -= 1
return acc
bench(
'sum-to(50000) tail-recursive',
'''
(define (sum-to n)
(let loop ((i n) (acc 0))
(if (= i 0) acc (loop (- i 1) (+ acc i)))))
(sum-to 50000)
''',
lambda: py_sum(50000)
)
# ── 5. List operations ────────────────────────────────────────────────────────
def py_list_ops():
lst = list(range(5000))
lst = list(reversed(lst))
lst = [x for x in lst if x % 2 == 1]
lst = [x * x for x in lst]
return sum(lst)
bench(
'list ops (5000 elements)',
'''
(define lst (iota 5000))
(define lst (reverse lst))
(define lst (filter odd? lst))
(define lst (map (lambda (x) (* x x)) lst))
(fold-left + 0 lst)
''',
py_list_ops
)
# ── 6. Higher-order / closures ────────────────────────────────────────────────
def py_adder_factory():
adders = [(lambda n: lambda x: x + n)(i) for i in range(100)]
return sum(f(10) for f in adders)
bench(
'closure factory (100 adders)',
'''
(define (make-adder n) (lambda (x) (+ x n)))
(define adders (map make-adder (iota 100)))
(fold-left + 0 (map (lambda (f) (f 10)) adders))
''',
py_adder_factory
)
# ── 7. Hash table ─────────────────────────────────────────────────────────────
def py_hash_ops():
h = {}
for i in range(1000):
h[i] = i * i
return sum(h.get(i, 0) for i in range(1000))
bench(
'hash-table (1000 set+ref)',
'''
(define h (make-hash-table))
(do ((i 0 (+ i 1))) ((= i 1000))
(hash-table-set! h i (* i i)))
(do ((i 0 (+ i 1)) (s 0 (+ s (hash-table-ref/default h i 0))))
((= i 1000) s))
''',
py_hash_ops
)
# ── 8. String operations ──────────────────────────────────────────────────────
def py_string_ops():
parts = [str(i) for i in range(200)]
joined = ', '.join(parts)
return len(joined)
bench(
'string-join (200 numbers)',
'''
(string-length
(string-join (map number->string (iota 200)) ", "))
''',
py_string_ops
)
# ── 9. Ackermann (small) ─────────────────────────────────────────────────────
def py_ack(m, n):
if m == 0: return n + 1
if n == 0: return py_ack(m-1, 1)
return py_ack(m-1, py_ack(m, n-1))
bench(
'ackermann(3,4)',
'''
(define (ack m n)
(cond ((= m 0) (+ n 1))
((= n 0) (ack (- m 1) 1))
(else (ack (- m 1) (ack m (- n 1))))))
(ack 3 4)
''',
lambda: py_ack(3, 4)
)
# ── 10. Mergesort ─────────────────────────────────────────────────────────────
def py_msort(lst):
if len(lst) <= 1: return lst
mid = len(lst) // 2
L = py_msort(lst[:mid]); R = py_msort(lst[mid:])
result = []; i = j = 0
while i < len(L) and j < len(R):
if L[i] <= R[j]: result.append(L[i]); i += 1
else: result.append(R[j]); j += 1
return result + L[i:] + R[j:]
bench(
'mergesort (200 elements)',
'''
(define (merge a b)
(cond ((null? a) b) ((null? b) a)
((< (car a) (car b)) (cons (car a) (merge (cdr a) b)))
(else (cons (car b) (merge a (cdr b))))))
(define (split lst)
(let loop ((l lst) (a (quote ())) (b (quote ())))
(if (null? l) (list a b) (loop (cdr l) b (cons (car l) a)))))
(define (msort lst)
(if (or (null? lst) (null? (cdr lst))) lst
(let ((h (split lst)))
(merge (msort (car h)) (msort (cadr h))))))
(length (msort (reverse (iota 200))))
''',
lambda: len(py_msort(list(range(199, -1, -1))))
)
print('=' * 75)
print('ratio = lisp time / python time (lower is better for lisp)')
# ═══════════════════════════════════════════════════════════════════════════
# Compiled (bytecode) benchmarks
# ═══════════════════════════════════════════════════════════════════════════
print()
print('COMPILED (bytecode) benchmarks')
print('=' * 75)
def compile_env(env):
"""Compile all user-defined Procs in env."""
for k, v in list(env.b.items()):
if isinstance(v, Proc):
env.b[k] = bc_compile_proc(v, env)
def bench_compiled(name, lisp_src, python_fn, iters=3):
"""Time compiled lisp vs python. Separates definition from invocation."""
# Parse all expressions; everything except the last is setup (defines)
exprs = read_all(lisp_src)
setup_exprs = exprs[:-1]
call_expr = exprs[-1]
lisp_times = []
for _ in range(iters):
env = fresh()
for e in setup_exprs: leval(e, env)
compile_env(env)
t = time.perf_counter()
result = leval(call_expr, env)
lisp_times.append(time.perf_counter() - t)
lisp_best = min(lisp_times)
py_times = []
for _ in range(iters):
t = time.perf_counter()
py_result = python_fn()
py_times.append(time.perf_counter() - t)
py_best = min(py_times)
slowdown = lisp_best / py_best if py_best > 0 else float('inf')
status = '' if result == py_result or str(result) == str(py_result) else ''
print(f'{status} {name:<30} bc={lisp_best*1000:7.1f}ms py={py_best*1000:7.1f}ms '
f'ratio={slowdown:5.1f}x')
return lisp_best, py_best
bench_compiled('fib(35) iterative', '''
(define (fib n)
(let loop ((a 0) (b 1) (i 0))
(if (= i n) a (loop b (+ a b) (+ i 1)))))
(fib 35)
''', lambda: py_fib(35))
bench_compiled('fib(20) tree-recursive', '''
(define (fib n)
(if (<= n 1) n (+ (fib (- n 1)) (fib (- n 2)))))
(fib 20)
''', lambda: py_fib_rec(20))
bench_compiled('tak(15,10,6)', '''
(define (tak x y z)
(if (>= y x) z
(tak (tak (- x 1) y z)
(tak (- y 1) z x)
(tak (- z 1) x y))))
(tak 15 10 6)
''', lambda: py_tak(15, 10, 6))
bench_compiled('sum-to(50000) tail-recursive', '''
(define (sum-to n)
(let loop ((i n) (acc 0))
(if (= i 0) acc (loop (- i 1) (+ acc i)))))
(sum-to 50000)
''', lambda: py_sum(50000))
bench_compiled('list ops (5000 elements)', '''
(define lst (iota 5000))
(define lst (reverse lst))
(define lst (filter odd? lst))
(define lst (map (lambda (x) (* x x)) lst))
(fold-left + 0 lst)
''', py_list_ops)
bench_compiled('hash-table (1000 set+ref)', '''
(define h (make-hash-table))
(do ((i 0 (+ i 1))) ((= i 1000))
(hash-table-set! h i (* i i)))
(do ((i 0 (+ i 1)) (s 0 (+ s (hash-table-ref/default h i 0))))
((= i 1000) s))
''', py_hash_ops)
bench_compiled('ackermann(3,4)', '''
(define (ack m n)
(cond ((= m 0) (+ n 1))
((= n 0) (ack (- m 1) 1))
(else (ack (- m 1) (ack m (- n 1))))))
(ack 3 4)
''', lambda: py_ack(3, 4))
bench_compiled('mergesort (200 elements)', '''
(define (merge a b)
(cond ((null? a) b) ((null? b) a)
((< (car a) (car b)) (cons (car a) (merge (cdr a) b)))
(else (cons (car b) (merge a (cdr b))))))
(define (split lst)
(let loop ((l lst) (a (quote ())) (b (quote ())))
(if (null? l) (list a b) (loop (cdr l) b (cons (car l) a)))))
(define (msort lst)
(if (or (null? lst) (null? (cdr lst))) lst
(let ((h (split lst)))
(merge (msort (car h)) (msort (cadr h))))))
(length (msort (reverse (iota 200))))
''', lambda: len(py_msort(list(range(199, -1, -1)))))
print('=' * 75)
print('ratio = compiled time / python time (lower is better)')
# ═══════════════════════════════════════════════════════════════════════════
# Auto-compile mode (zero-effort: just define and go)
# ═══════════════════════════════════════════════════════════════════════════
print()
print('AUTO-COMPILE mode (auto-compile! #t)')
print('=' * 75)
from uncommonlisp import _auto_compile
def bench_auto(name, lisp_src, python_fn, iters=3):
"""Time auto-compiled lisp vs python. Defines + runs in one pass."""
exprs = read_all(lisp_src)
lisp_times = []
for _ in range(iters):
env = fresh()
_auto_compile[0] = True
for e in exprs[:-1]: leval(e, env)
t = time.perf_counter()
result = leval(exprs[-1], env)
lisp_times.append(time.perf_counter() - t)
_auto_compile[0] = False
lisp_best = min(lisp_times)
py_times = []
for _ in range(iters):
t = time.perf_counter()
py_result = python_fn()
py_times.append(time.perf_counter() - t)
py_best = min(py_times)
slowdown = lisp_best / py_best if py_best > 0 else float('inf')
status = '' if result == py_result or str(result) == str(py_result) else ''
print(f'{status} {name:<30} auto={lisp_best*1000:6.1f}ms py={py_best*1000:6.1f}ms '
f'ratio={slowdown:5.1f}x')
return lisp_best, py_best
bench_auto('fib(35) iterative', '''
(define (fib n)
(let loop ((a 0) (b 1) (i 0))
(if (= i n) a (loop b (+ a b) (+ i 1)))))
(fib 35)
''', lambda: py_fib(35))
bench_auto('fib(20) tree-recursive', '''
(define (fib n)
(if (<= n 1) n (+ (fib (- n 1)) (fib (- n 2)))))
(fib 20)
''', lambda: py_fib_rec(20))
bench_auto('sum-to(50000) tail-recursive', '''
(define (sum-to n)
(let loop ((i n) (acc 0))
(if (= i 0) acc (loop (- i 1) (+ acc i)))))
(sum-to 50000)
''', lambda: py_sum(50000))
bench_auto('ackermann(3,4)', '''
(define (ack m n)
(cond ((= m 0) (+ n 1))
((= n 0) (ack (- m 1) 1))
(else (ack (- m 1) (ack m (- n 1))))))
(ack 3 4)
''', lambda: py_ack(3, 4))
print('=' * 75)