Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions benchmarks/typedpython/py/binary_trees.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


class Node:
__slots__ = ("left", "right")
left: "Node | None"
right: "Node | None"

Expand Down
39 changes: 36 additions & 3 deletions benchmarks/typedpython/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
interpreter with the gate's dependencies, e.g. that module's .venv): each benchmark is copied with
the `# typedpython: compiled` marker, compiled through `typedpython.pipeline`, and its `main()` is
run from the built extension. Without the option the column reports n/a.

binary-trees declares `__slots__` (TypedPython compiles fixed-layout classes only, SPEC N-11). The
speedup is reported against that CPython version; `CPython (no __slots__)` runs the same program
with the `__slots__` line removed, so what adding the slots changes in CPython is recorded too.
"""
import argparse
import datetime
Expand Down Expand Up @@ -40,7 +44,10 @@
"binary-trees": ("binary_trees.py", "binary_trees.mjs", "BinaryTrees", "binary_trees", "10", "16"),
"wordfreq": ("wordfreq.py", "wordfreq.mjs", "Wordfreq", "wordfreq", "100000", "3000000"),
}
COLUMNS = ["CPython", "Node", "Java", "Rust", "TypedPython @compiled"]
NOSLOTS = "CPython (no __slots__)"
COLUMNS = ["CPython", NOSLOTS, "Node", "Java", "Rust", "TypedPython @compiled"]
SLOTS_LINE = ' __slots__ = ("left", "right")\n'
NOSLOTS_BENCHMARKS = {"binary-trees"}
NOT_BUILT = "n/a (not built)"


Expand Down Expand Up @@ -99,6 +106,18 @@ def build_compiled(tp_dir: Path, tp_python: str) -> dict[str, tuple[str, list[st
return built


def noslots_source(name: str) -> Path:
"""The benchmark with its `__slots__` line removed, written under tp-build/noslots/."""
py = BENCHMARKS[name][0]
source = (ROOT / "py" / py).read_text()
if SLOTS_LINE not in source:
sys.exit(f"{name}: expected the line {SLOTS_LINE.strip()!r} in py/{py}")
out = TP_BUILD / "noslots" / py
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(source.replace(SLOTS_LINE, "", 1))
return out


def commands(name: str, size: str, python: str, java_source: bool,
compiled: dict[str, tuple[str, list[str]] | str] | None = None) -> dict[str, list[str]]:
py, js, cls, rs, _, _ = BENCHMARKS[name]
Expand All @@ -110,6 +129,8 @@ def commands(name: str, size: str, python: str, java_source: bool,
"Java": java,
"Rust": [str(TARGET / "release" / rs), size],
}
if name in NOSLOTS_BENCHMARKS:
cmds[NOSLOTS] = [python, str(noslots_source(name)), size]
entry = (compiled or {}).get(name)
if isinstance(entry, tuple):
cmds["TypedPython @compiled"] = [python, "-c", RUN_SNIPPET, Path(py).stem, entry[0], size]
Expand Down Expand Up @@ -179,7 +200,8 @@ def measure(args: argparse.Namespace) -> int:
print("| benchmark | size | " + " | ".join(COLUMNS) + " |")
print("|---|---|" + "---|" * len(COLUMNS))
for name, row in results.items():
cells = [f"{row[c]['median_s']:.3f} s" if isinstance(row.get(c), dict) else str(row.get(c, NOT_BUILT))
cells = [f"{row[c]['median_s']:.3f} s" if isinstance(row.get(c), dict)
else str(row.get(c, "—" if c == NOSLOTS else NOT_BUILT))
for c in COLUMNS] # type: ignore[index]
flag = "" if row["outputs_agree"] else " (OUTPUTS DIFFER)"
print(f"| {name}{flag} | {row['size']} | " + " | ".join(cells) + " |")
Expand Down Expand Up @@ -216,6 +238,7 @@ def interleaved(args: argparse.Namespace) -> int:
size = args.size.get(name, spec[5])
cmds = commands(name, size, args.python, False, compiled)
a_cmd, b_cmd = cmds["CPython"], cmds["TypedPython @compiled"]
c_cmd = cmds.get(NOSLOTS)
pairs = []
for i in range(args.pairs):
order = (a_cmd, b_cmd) if i % 2 == 0 else (b_cmd, a_cmd) # alternate which runs first
Expand All @@ -225,11 +248,21 @@ def interleaved(args: argparse.Namespace) -> int:
if o_a != o_b:
print(f"{name}: OUTPUTS DIFFER in pair {i}: {o_a!r} vs {o_b!r}")
return 1
pairs.append({"cpython_s": t_a, "compiled_s": t_b, "ratio": t_a / t_b, "load": load})
pair = {"cpython_s": t_a, "compiled_s": t_b, "ratio": t_a / t_b, "load": load}
if c_cmd is not None:
t_c, o_c = run_once(c_cmd)
if o_c != o_a:
print(f"{name}: OUTPUTS DIFFER in pair {i} (no __slots__): {o_c!r} vs {o_a!r}")
return 1
pair.update(cpython_noslots_s=t_c, ratio_vs_noslots=t_c / t_b)
pairs.append(pair)
ratios = [p["ratio"] for p in pairs]
results[name] = {"size": size, "compiled_functions": entry[1], "pairs": pairs,
"median_ratio": statistics.median(ratios), "min_ratio": min(ratios),
"max_ratio": max(ratios)}
if c_cmd is not None:
results[name]["median_ratio_vs_noslots"] = statistics.median(
p["ratio_vs_noslots"] for p in pairs)
print(f"measured {name}", file=sys.stderr)

print()
Expand Down
15 changes: 13 additions & 2 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,8 +483,19 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
fill the slots directly (the same observable result as running that `__init__`); any other `__init__`
is called as an object;
- methods compile like functions with `self` guarded `type(self) is C`; anything else on the class stays
interpreted.
A class outside these rules stays interpreted with a reason. `Status: planned`.
interpreted;
- a function that writes a slot is never "closed" (no `entry_globals` snapshot): overwriting a slot can
drop the last reference to the old value, and its `__del__` is user code that can rebind globals. This
is stricter than the IR contract requires, on purpose;
- a change after init keeps CPython's results: every slot access checks `type(x) is C` and the
class's `tp_version_tag` against the one captured at init (a tag is never reused; 0 never matches),
and every `C(args)` checks that the module global still is the captured class (a module-dict
watcher, or a dict lookup per call when no watcher id is free). On a mismatch an impure function
takes the generic path (`getattr`/`setattr`/calling the current global) and a pure one deopts.
A class that defines `__getattr__`, `__getattribute__`, `__setattr__` or `__delattr__` is not
compiled.
A class outside these rules stays interpreted with a reason. Methods are not compiled yet.
`Status: implemented (module-level functions; methods planned)`.

---

Expand Down
Loading
Loading