Skip to content

perf(compiler): 2.8x faster optimize_assembly -- 39% less bench time - #59

Open
dexhunter wants to merge 1 commit into
sql-hkr:mainfrom
dexhunter:perf/optimize-assembly-cache-lines
Open

dexhunter wants to merge 1 commit into
sql-hkr:mainfrom
dexhunter:perf/optimize-assembly-cache-lines

Conversation

@dexhunter

Copy link
Copy Markdown
Contributor

TL;DR

  • This PR makes optimize_assembly in src/tiny8/compiler.py faster. It is the peephole step that compile_c runs on all generated code.
  • Each line is now stripped and uppercased once, not once per rule. Each line is also checked only against the rule for its opcode.
  • Benchmark based on test_compiler_for_loop_and_break: 138.75 ms -> 83.95 ms per batch, 39.4% less time.
  • On the loop/break test program, optimize_assembly alone is 2.8x faster: 87.5 µs -> 30.7 µs.
  • Tests: pytest passes all 342 tests. ruff check and ruff format --check pass.

What & Why

Before this change, every pass called strip() and upper() on the same line many times, once for each rule. Every line was also checked against all six rules, even when its opcode cannot start any of them. The code did this work again for each of the five passes.

Now the function computes the stripped uppercase form and the opcode of each line one time. It keeps them in lists. The passes move line indexes and reuse these lists. The rule for the current opcode is the only rule that runs. Lines with other opcodes are copied without more checks. If no line starts with a rule opcode, the function returns early.

The output stays identical because each rule makes the same comparison as before. The old code compared uppercased operands, so it is equal to compare parts of the uppercased line. Rule 6 (JMP label / label:) still compares the original text, so it stays case-sensitive. The five-pass limit, the rule order and the returned line text do not change. The function still does not change its input list.

Results

Times are the best of 7 timeit repeats, median of 3 runs, on one CPU core with Python 3.12.

Situation Before After Change
optimize_assembly, loop/break test program (73 lines) 87.5 µs 30.7 µs 2.8x faster
optimize_assembly, 9 compiler test programs (1,140 lines) 1.45 ms 0.55 ms 61.8% less time
optimize_assembly, 9 test programs x10 (11,400 lines) 14.3 ms 5.78 ms 59.6% less time
Full compile_c of the loop/break test program 215.7 µs 161.4 µs 25.2% less time
optimize_assembly, 2-line input, no rule matches 0.92 µs 1.21 µs +0.3 µs (setup cost, no help)

For very short inputs, the new setup step costs about 0.3 µs. This cost is small next to the time to compile any real program.

Why this is safe and easy to maintain

The change touches only optimize_assembly. Each rule keeps its own block with the same comments and order, so a reader can compare it with the old code. The output is the same for all inputs, including mixed case, extra spaces, comments, near misses and changes that need more than one pass. The tests in tests/test_compiler.py cover every rule. We also compared the full output and CPU state with the old code on many generated programs and edge cases.

Validation

PYTHONPATH=src python -m pytest
# 342 passed
PYTHONPATH=src python -m pytest tests/test_compiler.py tests/test_tui.py tests/test_control_flow.py tests/test_cpu_edge_cases.py
# 172 passed
ruff check src/tiny8/compiler.py
# All checks passed!
ruff format --check src/tiny8/compiler.py
# 1 file already formatted

Benchmark: the workload compiles, assembles and runs the test_compiler_for_loop_and_break program. It also runs optimize_assembly on the unoptimized output of 9 compiler test programs and on edge cases. Three runs gave 0.608, 0.606 and 0.606 of the old time. A short way to reproduce the direct timing:

PYTHONPATH=src python - <<'EOF'
import timeit
from tiny8.compiler import CodeGenerator, Lexer, Parser, optimize_assembly

src = """int main() { int sum = 0; int i = 0;
  for (i = 0; i < 10; i = i + 1) { if (i == 4) { break; } sum = sum + i; }
  poke(0x84, sum); return 0; }"""
gen = CodeGenerator()
gen.generate(Parser(Lexer(src).tokenize()).parse(), optimize=False)
lines = list(gen.asm)
t = timeit.Timer(lambda: optimize_assembly(lines))
n, _ = t.autorange()
print(f"{len(lines)} lines: {min(t.repeat(7, n)) / n * 1e6:.1f} us")
EOF

Checklist

  • Code follows style guidelines (ruff check passes)
  • All tests pass (pytest succeeds)
  • New tests added for new functionality (no new functionality; existing tests cover the rules)
  • Documentation updated (no user-facing change)
  • Commit messages are clear and descriptive
  • PR description explains changes and motivation
  • Branch is up-to-date with main (checked at publish time)
  • No unrelated changes included

AI assistance

Codex (gpt-6.1-sol) generated the optimization candidates, and Weco Observe tracked them. Automated checks picked this candidate: output comparison against the old code, the native test suite and benchmarks. No human review is claimed.

Weco share link: https://dashboard.weco.ai/share/aEGnUnfD2URSr8uIXsb-UneA_cAu0Avi

Strip and uppercase each line once and keep the result in lists that
all passes reuse. Check each line only against the rule for its opcode,
and return early when no line starts with a rule opcode. Rule 6 still
compares the original text, and the five-pass limit is unchanged.

The benchmark based on test_compiler_for_loop_and_break takes 39% less
time (138.75 ms -> 83.95 ms). On the loop/break test program,
optimize_assembly alone goes from 87.5 us to 30.7 us.

Co-Authored-By: Aiden <aiden@weco.ai>
@codecov

codecov Bot commented Oct 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.30508% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/tiny8/compiler.py 98.30% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant