FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Avoid per-tail-call owner allocations by moreal · Pull Request #8472 · RustPython/RustPython · GitHub

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .py  (1) .rs  (3) All 2 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
268 changes: 268 additions & 0 deletions benches/tailcall_baseline.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
"""Repeatable baseline benchmark for the Python-to-Python call trampoline.

Run this with a release RustPython binary, not CPython:

./target/release/rustpython benches/tailcall_baseline.py

Each timed loop runs inside one RustPython process. Cases are rotated between
rounds so that every case is sampled at different points in the run.
"""

import dis
import sys
import time


DEFAULT_SAMPLES = 14
DEFAULT_ITERATIONS = 1_000_000
DEFAULT_DEEP_ITERATIONS = 10_000
DEFAULT_DEEP_DEPTH = 100


def add_one(value):
return value + 1


class Adder:
def add_one(self, value):
return value + 1


bound_add_one = Adder().add_one


def shallow_inner(value):
return value + 1


def shallow_outer(value):
return shallow_inner(value)


def recursive_add(depth, value):
if depth:
return recursive_add(depth - 1, value)
return value + 1


def bench_inline(iterations, _depth):
value = 0
start = time.perf_counter_ns()
for _index in range(iterations):
value = value + 1
elapsed = time.perf_counter_ns() - start
assert value == iterations
return elapsed


def bench_exact_function(iterations, _depth):
function = add_one
value = 0
start = time.perf_counter_ns()
for _index in range(iterations):
value = function(value)
elapsed = time.perf_counter_ns() - start
assert value == iterations
return elapsed


def bench_exact_bound_method(iterations, _depth):
method = bound_add_one
value = 0
start = time.perf_counter_ns()
for _index in range(iterations):
value = method(value)
elapsed = time.perf_counter_ns() - start
assert value == iterations
return elapsed


def bench_shallow_nested(iterations, _depth):
function = shallow_outer
value = 0
start = time.perf_counter_ns()
for _index in range(iterations):
value = function(value)
elapsed = time.perf_counter_ns() - start
assert value == iterations
return elapsed


def bench_deep_recursive(iterations, depth):
function = recursive_add
value = 0
start = time.perf_counter_ns()
for _index in range(iterations):
value = function(depth, value)
elapsed = time.perf_counter_ns() - start
assert value == iterations
return elapsed


def shallow_activation_generator(iterations):
"""Time calls which each activate a fresh trampoline in their callee."""
function = shallow_outer
value = 0
start = time.perf_counter_ns()
for _index in range(iterations):
value = function(value)
elapsed = time.perf_counter_ns() - start
assert value == iterations
yield elapsed


def bench_shallow_activations(iterations, _depth):
# Generator frames do not issue TailCall themselves. Each shallow_outer()
# invocation therefore starts and finishes a new trampoline when it calls
# shallow_inner(), including a fresh frame_stack allocation.
return next(shallow_activation_generator(iterations))


def deep_activation_generator(iterations, depth):
"""Time deep calls which each allocate and spill a fresh frame stack."""
function = recursive_add
value = 0
start = time.perf_counter_ns()
for _index in range(iterations):
value = function(depth, value)
elapsed = time.perf_counter_ns() - start
assert value == iterations
yield elapsed


def bench_deep_activations(iterations, depth):
return next(deep_activation_generator(iterations, depth))


def parse_positive_int(name, default):
prefix = "--" + name + "="
for argument in sys.argv[1:]:
if argument.startswith(prefix):
value = int(argument[len(prefix) :])
if value <= 0:
raise ValueError(prefix + " must be positive")
return value
return default


def median(values):
ordered = sorted(values)
midpoint = len(ordered) // 2
if len(ordered) % 2:
return ordered[midpoint]
return (ordered[midpoint - 1] + ordered[midpoint]) / 2


def require_instruction(function, opname):
instructions = dis.get_instructions(function, adaptive=True)
if not any(instruction.opname == opname for instruction in instructions):
raise RuntimeError(function.__name__ + " did not specialize to " + opname)


def verify_specializations():
expected = [
(bench_exact_function, "CALL_PY_EXACT_ARGS"),
(bench_exact_bound_method, "CALL_BOUND_METHOD_EXACT_ARGS"),
(bench_shallow_nested, "CALL_PY_EXACT_ARGS"),
(shallow_outer, "CALL_PY_EXACT_ARGS"),
(bench_deep_recursive, "CALL_PY_EXACT_ARGS"),
(recursive_add, "CALL_PY_EXACT_ARGS"),
(shallow_activation_generator, "CALL_PY_EXACT_ARGS"),
(deep_activation_generator, "CALL_PY_EXACT_ARGS"),
]
for function, opname in expected:
require_instruction(function, opname)
return ";".join(function.__name__ + ":" + opname for function, opname in expected)


def main():
if sys.implementation.name != "rustpython":
raise RuntimeError("run this benchmark with a release RustPython binary")

samples = parse_positive_int("samples", DEFAULT_SAMPLES)
iterations = parse_positive_int("iterations", DEFAULT_ITERATIONS)
deep_iterations = parse_positive_int(
"deep-iterations", DEFAULT_DEEP_ITERATIONS
)
deep_depth = parse_positive_int("deep-depth", DEFAULT_DEEP_DEPTH)

cases = [
("inline", bench_inline, iterations, 0, 0),
("exact_function", bench_exact_function, iterations, 0, 1),
("exact_bound_method", bench_exact_bound_method, iterations, 0, 1),
("shallow_nested_steady", bench_shallow_nested, iterations, 0, 2),
(
"deep_recursive_steady",
bench_deep_recursive,
deep_iterations,
deep_depth,
deep_depth + 1,
),
(
"shallow_nested_activation",
bench_shallow_activations,
iterations,
0,
2,
),
(
"deep_recursive_activation",
bench_deep_activations,
deep_iterations,
deep_depth,
deep_depth + 1,
),
]
results = {name: [] for name, _function, _iterations, _depth, _calls in cases}

# Warm every bytecode path before collecting the interleaved samples.
for _name, function, _iterations, depth, _calls in cases:
function(100, depth)
specializations = verify_specializations()

print("benchmark=tailcall_baseline_v2")
print("implementation=" + sys.implementation.name)
print("version=" + sys.version.replace("\n", " "))
print("samples=" + str(samples))
print("iterations=" + str(iterations))
print("deep_iterations=" + str(deep_iterations))
print("deep_depth=" + str(deep_depth))
print("specializations=" + specializations)
print("round,case,iterations,total_ns,ns_per_iteration")

for round_index in range(samples):
offset = round_index % len(cases)
interleaved = cases[offset:] + cases[:offset]
for name, function, case_iterations, depth, _calls in interleaved:
total_ns = function(case_iterations, depth)
ns_per_iteration = total_ns / case_iterations
results[name].append(ns_per_iteration)
print(
str(round_index + 1)
+ ","
+ name
+ ","
+ str(case_iterations)
+ ","
+ str(total_ns)
+ ","
+ ("%.3f" % ns_per_iteration)
)

inline_median = median(results["inline"])
print("case,median_ns_per_iteration,delta_vs_inline_ns,python_calls_per_iteration")
for name, _function, _iterations, _depth, calls in cases:
case_median = median(results[name])
print(
name
+ ","
+ ("%.3f" % case_median)
+ ","
+ ("%.3f" % (case_median - inline_median))
+ ","
+ str(calls)
)


if __name__ == "__main__":
main()
17 changes: 8 additions & 9 deletions crates/vm/src/frame.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -10680,11 +10680,10 @@ impl ExecutingFrame<'_> {
}
}

// Pop the callable and transfer ownership to the trampoline via
// the VM side channel, avoiding a per-frame mutex lock on
// temporary_refs.
// Pop the callable and transfer ownership to the trampoline. This one
// reference keeps every field borrowed by the callee frame alive.
let callable = self.pop_value();
unsafe { &mut *vm.pending_tailcall_refs.get() }.push(callable);
vm.set_pending_tailcall_owner(callable);

vm.set_pending_tailcall(callee_iframe);
}
Expand Down Expand Up @@ -10734,13 +10733,13 @@ impl ExecutingFrame<'_> {
*dst = Some(arg);
}
self.pop_value_opt(); // null (self_or_null)
let callable = self.pop_value(); // callable (bound method)
self.pop_value(); // callable (bound method)
fastlocals[0] = Some(bound_self);

// Transfer ownership to the trampoline via the VM side channel.
let refs = unsafe { &mut *vm.pending_tailcall_refs.get() };
refs.push(bound_function);
refs.push(callable);
// The function owns every field borrowed by the callee frame.
// bound_self is owned by fastlocals; the bound-method object itself is
// no longer needed and was dropped above, matching the recursive path.
vm.set_pending_tailcall_owner(bound_function);

vm.set_pending_tailcall(callee_iframe);
}
Expand Down
Loading
Loading

Back | FazBrowse Home | New Git URL