From 99a63f92aab1cbfaf47e97a02e725f63ef497b3e Mon Sep 17 00:00:00 2001 From: Lee Dogeon Date: Tue, 1 Sep 2026 10:03:29 +0900 Subject: [PATCH] benchmarks: add pyperformance workloads (#8633) Add dependency-free CPU-bound workloads adapted from pyperformance 1.14.0 to cover interpreter, standard-library serialization, regex, numeric, and garbage-collection paths under the existing Criterion and CodSpeed harnesses. Keep inputs deterministic and scale expensive cases for simulation while checking that benchmark execution does not leak interpreter state. Assisted-by: Codex:gpt-5.6-sol --- benches/README.md | 4 + benches/benchmarks/base64.py | 34 ++ benches/benchmarks/decimal_factorial.py | 42 ++ benches/benchmarks/deepcopy.py | 71 +++ benches/benchmarks/deltablue.py | 628 ++++++++++++++++++++++++ benches/benchmarks/float.py | 54 ++ benches/benchmarks/gc_collect.py | 55 +++ benches/benchmarks/gc_traversal.py | 28 ++ benches/benchmarks/generators.py | 42 ++ benches/benchmarks/json_dumps.py | 28 ++ benches/benchmarks/nqueens.py | 55 +++ benches/benchmarks/pickle.py | 33 ++ benches/benchmarks/pidigits.py | 57 +++ benches/benchmarks/regex_effbot.py | 158 ++++++ benches/benchmarks/richards.py | 415 ++++++++++++++++ benches/benchmarks/unpickle.py | 33 ++ tests/benchmark_scripts.rs | 84 ++++ 17 files changed, 1821 insertions(+) create mode 100644 benches/benchmarks/base64.py create mode 100644 benches/benchmarks/decimal_factorial.py create mode 100644 benches/benchmarks/deepcopy.py create mode 100644 benches/benchmarks/deltablue.py create mode 100644 benches/benchmarks/float.py create mode 100644 benches/benchmarks/gc_collect.py create mode 100644 benches/benchmarks/gc_traversal.py create mode 100644 benches/benchmarks/generators.py create mode 100644 benches/benchmarks/json_dumps.py create mode 100644 benches/benchmarks/nqueens.py create mode 100644 benches/benchmarks/pickle.py create mode 100644 benches/benchmarks/pidigits.py create mode 100644 benches/benchmarks/regex_effbot.py create mode 100644 benches/benchmarks/richards.py create mode 100644 benches/benchmarks/unpickle.py create mode 100644 tests/benchmark_scripts.rs diff --git a/benches/README.md b/benches/README.md index 990ea2139..d31e1a0f2 100644 --- a/benches/README.md +++ b/benches/README.md @@ -97,3 +97,7 @@ rustflags = [ ## Benchmark source - +- The workloads marked as adapted from pyperformance come from + [python/pyperformance 1.14.0](https://github.com/python/pyperformance/tree/1.14.0/pyperformance/data-files/benchmarks). + Their `pyperf` runners and internal timers are removed so Criterion is the only measurement + harness, and some input sizes are reduced for CodSpeed simulation. diff --git a/benches/benchmarks/base64.py b/benches/benchmarks/base64.py new file mode 100644 index 000000000..97fde642e --- /dev/null +++ b/benches/benchmarks/base64.py @@ -0,0 +1,34 @@ +# Adapted from python/pyperformance 1.14.0 (base64). +"""Exercise the base64 module's primary public APIs.""" + +import base64 +import random + + +random_source = random.Random(12345) +data_tiny = random_source.randbytes(20) +data_small = random_source.randbytes(127) +data_medium = random_source.randbytes(3072) +data_large = random_source.randbytes(9000) +cases = ( + (data_tiny, 45), + (data_small, 7), + (data_medium, 1), + (data_large, 1), +) + + +def exercise(encode, decode): + for data, count in cases: + encoded = encode(data) + for _ in range(count): + assert decode(encode(data)) == data + assert decode(encoded) == data + + +exercise(base64.b64encode, base64.b64decode) +exercise(base64.urlsafe_b64encode, base64.urlsafe_b64decode) +exercise(base64.b32encode, base64.b32decode) +exercise(base64.b16encode, base64.b16decode) +exercise(base64.a85encode, base64.a85decode) +exercise(base64.b85encode, base64.b85decode) diff --git a/benches/benchmarks/decimal_factorial.py b/benches/benchmarks/decimal_factorial.py new file mode 100644 index 000000000..09cfeaba8 --- /dev/null +++ b/benches/benchmarks/decimal_factorial.py @@ -0,0 +1,42 @@ +# Adapted from python/pyperformance 1.14.0 (decimal_factorial). +""" +Calculate `factorial` using the decimal module. + +- 2024-06-14: Michael Droettboom copied this from + Modules/_decimal/tests/bench.py in the CPython source and adapted to use + pyperf. +""" + +# Original copyright notice in CPython source: + +# +# Copyright (C) 2001-2012 Python Software Foundation. All Rights Reserved. +# Modified and extended by Stefan Krah. +# + + +import decimal + + +def factorial(n, m): + if n > m: + return factorial(m, n) + elif m == 0: + return 1 + elif n == m: + return n + else: + return factorial(n, (n + m) // 2) * factorial((n + m) // 2 + 1, m) + + +def bench_decimal_factorial(): + # The upstream 10,000! and 100,000! inputs are reduced for simulation, + # while retaining enough precision to exercise large Decimal arithmetic. + with decimal.localcontext() as context: + context.prec = 3000 + result = factorial(decimal.Decimal(1000), 0) + + assert len(result.as_tuple().digits) == 2568 + + +bench_decimal_factorial() diff --git a/benches/benchmarks/deepcopy.py b/benches/benchmarks/deepcopy.py new file mode 100644 index 000000000..bcae0d156 --- /dev/null +++ b/benches/benchmarks/deepcopy.py @@ -0,0 +1,71 @@ +# Adapted from python/pyperformance 1.14.0 (deepcopy). +""" +Benchmark to measure performance of the python builtin method copy.deepcopy + +Performance is tested on a nested dictionary and a dataclass + +Author: Pieter Eendebak + +""" +import copy +from dataclasses import dataclass + + +@dataclass +class A: + string: str + lst: list + boolean: bool + + +def benchmark_reduce(n): + """ Benchmark where the __reduce__ functionality is used """ + class C(object): + def __init__(self): + self.a = 1 + self.b = 2 + + def __reduce__(self): + return (C, (), self.__dict__) + + def __setstate__(self, state): + self.__dict__.update(state) + c = C() + + for _ in range(n): + _ = copy.deepcopy(c) + + +def benchmark_memo(n): + """ Benchmark where the memo functionality is used """ + A = [1] * 100 + data = {'a': (A, A, A), 'b': [A] * 100} + + for _ in range(n): + _ = copy.deepcopy(data) + + +def benchmark(n): + """ Benchmark on some standard data types """ + a = { + 'list': [1, 2, 3, 43], + 't': (1 ,2, 3), + 'str': 'hello', + 'subdict': {'a': True} + } + dc = A('hello', [1, 2, 3], True) + + for _ in range(n): + for _ in range(30): + _ = copy.deepcopy(a) + for s in ['red', 'blue', 'green']: + dc.string = s + for value in range(5): + dc.lst[0] = value + for b in [True, False]: + dc.boolean = b + _ = copy.deepcopy(dc) + +benchmark(1) +benchmark_reduce(5) +benchmark_memo(1) diff --git a/benches/benchmarks/deltablue.py b/benches/benchmarks/deltablue.py new file mode 100644 index 000000000..bfd4a076a --- /dev/null +++ b/benches/benchmarks/deltablue.py @@ -0,0 +1,628 @@ +# Adapted from python/pyperformance 1.14.0 (deltablue). +""" +deltablue.py +============ + +Ported for the PyPy project. +Contributed by Daniel Lindsley + +This implementation of the DeltaBlue benchmark was directly ported +from the `V8's source code`_, which was in turn derived +from the Smalltalk implementation by John Maloney and Mario +Wolczko. The original Javascript implementation was licensed under the GPL. + +It's been updated in places to be more idiomatic to Python (for loops over +collections, a couple magic methods, ``OrderedCollection`` being a list & things +altering those collections changed to the builtin methods) but largely retains +the layout & logic from the original. (Ugh.) + +.. _`V8's source code`: (https://github.com/v8/v8/blob/master/benchmarks/deltablue.js) +""" + +# The JS variant implements "OrderedCollection", which basically completely +# overlaps with ``list``. So we'll cheat. :D +class OrderedCollection(list): + pass + + +class Strength(object): + REQUIRED = None + STRONG_PREFERRED = None + PREFERRED = None + STRONG_DEFAULT = None + NORMAL = None + WEAK_DEFAULT = None + WEAKEST = None + + def __init__(self, strength, name): + super(Strength, self).__init__() + self.strength = strength + self.name = name + + @classmethod + def stronger(cls, s1, s2): + return s1.strength < s2.strength + + @classmethod + def weaker(cls, s1, s2): + return s1.strength > s2.strength + + @classmethod + def weakest_of(cls, s1, s2): + if cls.weaker(s1, s2): + return s1 + + return s2 + + @classmethod + def strongest(cls, s1, s2): + if cls.stronger(s1, s2): + return s1 + + return s2 + + def next_weaker(self): + strengths = { + 0: self.__class__.WEAKEST, + 1: self.__class__.WEAK_DEFAULT, + 2: self.__class__.NORMAL, + 3: self.__class__.STRONG_DEFAULT, + 4: self.__class__.PREFERRED, + # TODO: This looks like a bug in the original code. Shouldn't this be + # ``STRONG_PREFERRED? Keeping for porting sake... + 5: self.__class__.REQUIRED, + } + return strengths[self.strength] + + +# This is a terrible pattern IMO, but true to the original JS implementation. +Strength.REQUIRED = Strength(0, "required") +Strength.STRONG_PREFERRED = Strength(1, "strongPreferred") +Strength.PREFERRED = Strength(2, "preferred") +Strength.STRONG_DEFAULT = Strength(3, "strongDefault") +Strength.NORMAL = Strength(4, "normal") +Strength.WEAK_DEFAULT = Strength(5, "weakDefault") +Strength.WEAKEST = Strength(6, "weakest") + + +class Constraint(object): + + def __init__(self, strength): + super(Constraint, self).__init__() + self.strength = strength + + def add_constraint(self): + global planner + self.add_to_graph() + planner.incremental_add(self) + + def satisfy(self, mark): + global planner + self.choose_method(mark) + + if not self.is_satisfied(): + if self.strength == Strength.REQUIRED: + print('Could not satisfy a required constraint!') + + return None + + self.mark_inputs(mark) + out = self.output() + overridden = out.determined_by + + if overridden is not None: + overridden.mark_unsatisfied() + + out.determined_by = self + + if not planner.add_propagate(self, mark): + print('Cycle encountered') + + out.mark = mark + return overridden + + def destroy_constraint(self): + global planner + if self.is_satisfied(): + planner.incremental_remove(self) + else: + self.remove_from_graph() + + def is_input(self): + return False + + +class UrnaryConstraint(Constraint): + + def __init__(self, v, strength): + super(UrnaryConstraint, self).__init__(strength) + self.my_output = v + self.satisfied = False + self.add_constraint() + + def add_to_graph(self): + self.my_output.add_constraint(self) + self.satisfied = False + + def choose_method(self, mark): + if self.my_output.mark != mark and \ + Strength.stronger(self.strength, self.my_output.walk_strength): + self.satisfied = True + else: + self.satisfied = False + + def is_satisfied(self): + return self.satisfied + + def mark_inputs(self, mark): + # No-ops. + pass + + def output(self): + # Ugh. Keeping it for consistency with the original. So much for + # "we're all adults here"... + return self.my_output + + def recalculate(self): + self.my_output.walk_strength = self.strength + self.my_output.stay = not self.is_input() + + if self.my_output.stay: + self.execute() + + def mark_unsatisfied(self): + self.satisfied = False + + def inputs_known(self, mark): + return True + + def remove_from_graph(self): + if self.my_output is not None: + self.my_output.remove_constraint(self) + self.satisfied = False + + +class StayConstraint(UrnaryConstraint): + + def __init__(self, v, string): + super(StayConstraint, self).__init__(v, string) + + def execute(self): + # The methods, THEY DO NOTHING. + pass + + +class EditConstraint(UrnaryConstraint): + + def __init__(self, v, string): + super(EditConstraint, self).__init__(v, string) + + def is_input(self): + return True + + def execute(self): + # This constraint also does nothing. + pass + + +class Direction(object): + # Hooray for things that ought to be structs! + NONE = 0 + FORWARD = 1 + BACKWARD = -1 + + +class BinaryConstraint(Constraint): + + def __init__(self, v1, v2, strength): + super(BinaryConstraint, self).__init__(strength) + self.v1 = v1 + self.v2 = v2 + self.direction = Direction.NONE + self.add_constraint() + + def choose_method(self, mark): + if self.v1.mark == mark: + if self.v2.mark != mark and Strength.stronger(self.strength, self.v2.walk_strength): + self.direction = Direction.FORWARD + else: + self.direction = Direction.BACKWARD + + if self.v2.mark == mark: + if self.v1.mark != mark and Strength.stronger(self.strength, self.v1.walk_strength): + self.direction = Direction.BACKWARD + else: + self.direction = Direction.NONE + + if Strength.weaker(self.v1.walk_strength, self.v2.walk_strength): + if Strength.stronger(self.strength, self.v1.walk_strength): + self.direction = Direction.BACKWARD + else: + self.direction = Direction.NONE + else: + if Strength.stronger(self.strength, self.v2.walk_strength): + self.direction = Direction.FORWARD + else: + self.direction = Direction.BACKWARD + + def add_to_graph(self): + self.v1.add_constraint(self) + self.v2.add_constraint(self) + self.direction = Direction.NONE + + def is_satisfied(self): + return self.direction != Direction.NONE + + def mark_inputs(self, mark): + self.input().mark = mark + + def input(self): + if self.direction == Direction.FORWARD: + return self.v1 + + return self.v2 + + def output(self): + if self.direction == Direction.FORWARD: + return self.v2 + + return self.v1 + + def recalculate(self): + ihn = self.input() + out = self.output() + out.walk_strength = Strength.weakest_of( + self.strength, ihn.walk_strength) + out.stay = ihn.stay + + if out.stay: + self.execute() + + def mark_unsatisfied(self): + self.direction = Direction.NONE + + def inputs_known(self, mark): + i = self.input() + return i.mark == mark or i.stay or i.determined_by is None + + def remove_from_graph(self): + if self.v1 is not None: + self.v1.remove_constraint(self) + + if self.v2 is not None: + self.v2.remove_constraint(self) + + self.direction = Direction.NONE + + +class ScaleConstraint(BinaryConstraint): + + def __init__(self, src, scale, offset, dest, strength): + self.direction = Direction.NONE + self.scale = scale + self.offset = offset + super(ScaleConstraint, self).__init__(src, dest, strength) + + def add_to_graph(self): + super(ScaleConstraint, self).add_to_graph() + self.scale.add_constraint(self) + self.offset.add_constraint(self) + + def remove_from_graph(self): + super(ScaleConstraint, self).remove_from_graph() + + if self.scale is not None: + self.scale.remove_constraint(self) + + if self.offset is not None: + self.offset.remove_constraint(self) + + def mark_inputs(self, mark): + super(ScaleConstraint, self).mark_inputs(mark) + self.scale.mark = mark + self.offset.mark = mark + + def execute(self): + if self.direction == Direction.FORWARD: + self.v2.value = self.v1.value * self.scale.value + self.offset.value + else: + self.v1.value = ( + self.v2.value - self.offset.value) / self.scale.value + + def recalculate(self): + ihn = self.input() + out = self.output() + out.walk_strength = Strength.weakest_of( + self.strength, ihn.walk_strength) + out.stay = ihn.stay and self.scale.stay and self.offset.stay + + if out.stay: + self.execute() + + +class EqualityConstraint(BinaryConstraint): + + def execute(self): + self.output().value = self.input().value + + +class Variable(object): + + def __init__(self, name, initial_value=0): + super(Variable, self).__init__() + self.name = name + self.value = initial_value + self.constraints = OrderedCollection() + self.determined_by = None + self.mark = 0 + self.walk_strength = Strength.WEAKEST + self.stay = True + + def __repr__(self): + # To make debugging this beast from pdb easier... + return '' % ( + self.name, + self.value + ) + + def add_constraint(self, constraint): + self.constraints.append(constraint) + + def remove_constraint(self, constraint): + self.constraints.remove(constraint) + + if self.determined_by == constraint: + self.determined_by = None + + +class Planner(object): + + def __init__(self): + super(Planner, self).__init__() + self.current_mark = 0 + + def incremental_add(self, constraint): + mark = self.new_mark() + overridden = constraint.satisfy(mark) + + while overridden is not None: + overridden = overridden.satisfy(mark) + + def incremental_remove(self, constraint): + out = constraint.output() + constraint.mark_unsatisfied() + constraint.remove_from_graph() + unsatisfied = self.remove_propagate_from(out) + strength = Strength.REQUIRED + # Do-while, the Python way. + repeat = True + + while repeat: + for u in unsatisfied: + if u.strength == strength: + self.incremental_add(u) + + strength = strength.next_weaker() + + repeat = strength != Strength.WEAKEST + + def new_mark(self): + self.current_mark += 1 + return self.current_mark + + def make_plan(self, sources): + mark = self.new_mark() + plan = Plan() + todo = sources + + while len(todo): + c = todo.pop(0) + + if c.output().mark != mark and c.inputs_known(mark): + plan.add_constraint(c) + c.output().mark = mark + self.add_constraints_consuming_to(c.output(), todo) + + return plan + + def extract_plan_from_constraints(self, constraints): + sources = OrderedCollection() + + for c in constraints: + if c.is_input() and c.is_satisfied(): + sources.append(c) + + return self.make_plan(sources) + + def add_propagate(self, c, mark): + todo = OrderedCollection() + todo.append(c) + + while len(todo): + d = todo.pop(0) + + if d.output().mark == mark: + self.incremental_remove(c) + return False + + d.recalculate() + self.add_constraints_consuming_to(d.output(), todo) + + return True + + def remove_propagate_from(self, out): + out.determined_by = None + out.walk_strength = Strength.WEAKEST + out.stay = True + unsatisfied = OrderedCollection() + todo = OrderedCollection() + todo.append(out) + + while len(todo): + v = todo.pop(0) + + for c in v.constraints: + if not c.is_satisfied(): + unsatisfied.append(c) + + determining = v.determined_by + + for c in v.constraints: + if c != determining and c.is_satisfied(): + c.recalculate() + todo.append(c.output()) + + return unsatisfied + + def add_constraints_consuming_to(self, v, coll): + determining = v.determined_by + cc = v.constraints + + for c in cc: + if c != determining and c.is_satisfied(): + # I guess we're just updating a reference (``coll``)? Seems + # inconsistent with the rest of the implementation, where they + # return the lists... + coll.append(c) + + +class Plan(object): + + def __init__(self): + super(Plan, self).__init__() + self.v = OrderedCollection() + + def add_constraint(self, c): + self.v.append(c) + + def __len__(self): + return len(self.v) + + def __getitem__(self, index): + return self.v[index] + + def execute(self): + for c in self.v: + c.execute() + + +# Main + +def chain_test(n): + """ + This is the standard DeltaBlue benchmark. A long chain of equality + constraints is constructed with a stay constraint on one end. An + edit constraint is then added to the opposite end and the time is + measured for adding and removing this constraint, and extracting + and executing a constraint satisfaction plan. There are two cases. + In case 1, the added constraint is stronger than the stay + constraint and values must propagate down the entire length of the + chain. In case 2, the added constraint is weaker than the stay + constraint so it cannot be accomodated. The cost in this case is, + of course, very low. Typical situations lie somewhere between these + two extremes. + """ + global planner + planner = Planner() + prev, first, last = None, None, None + + # We need to go up to n inclusively. + for i in range(n + 1): + name = "v%s" % i + v = Variable(name) + + if prev is not None: + EqualityConstraint(prev, v, Strength.REQUIRED) + + if i == 0: + first = v + + if i == n: + last = v + + prev = v + + StayConstraint(last, Strength.STRONG_DEFAULT) + edit = EditConstraint(first, Strength.PREFERRED) + edits = OrderedCollection() + edits.append(edit) + plan = planner.extract_plan_from_constraints(edits) + + for i in range(100): + first.value = i + plan.execute() + + if last.value != i: + print("Chain test failed.") + + +def projection_test(n): + """ + This test constructs a two sets of variables related to each + other by a simple linear transformation (scale and offset). The + time is measured to change a variable on either side of the + mapping and to change the scale and offset factors. + """ + global planner + planner = Planner() + scale = Variable("scale", 10) + offset = Variable("offset", 1000) + src = None + + dests = OrderedCollection() + + for i in range(n): + src = Variable("src%s" % i, i) + dst = Variable("dst%s" % i, i) + dests.append(dst) + StayConstraint(src, Strength.NORMAL) + ScaleConstraint(src, scale, offset, dst, Strength.REQUIRED) + + change(src, 17) + + if dst.value != 1170: + print("Projection 1 failed") + + change(dst, 1050) + + if src.value != 5: + print("Projection 2 failed") + + change(scale, 5) + + for i in range(n - 1): + if dests[i].value != (i * 5 + 1000): + print("Projection 3 failed") + + change(offset, 2000) + + for i in range(n - 1): + if dests[i].value != (i * 5 + 2000): + print("Projection 4 failed") + + +def change(v, new_value): + global planner + edit = EditConstraint(v, Strength.PREFERRED) + edits = OrderedCollection() + edits.append(edit) + + plan = planner.extract_plan_from_constraints(edits) + + for i in range(10): + v.value = new_value + plan.execute() + + edit.destroy_constraint() + + +# HOORAY FOR GLOBALS... Oh wait. +# In spirit of the original, we'll keep it, but ugh. +planner = None + + +def delta_blue(n): + chain_test(n) + projection_test(n) + +delta_blue(100) diff --git a/benches/benchmarks/float.py b/benches/benchmarks/float.py new file mode 100644 index 000000000..568794615 --- /dev/null +++ b/benches/benchmarks/float.py @@ -0,0 +1,54 @@ +# Adapted from python/pyperformance 1.14.0 (float). +""" +Artificial, floating point-heavy benchmark originally used by Factor. +""" +from math import sin, cos, sqrt + + +POINTS = 100000 + + +class Point(object): + __slots__ = ('x', 'y', 'z') + + def __init__(self, i): + self.x = x = sin(i) + self.y = cos(i) * 3 + self.z = (x * x) / 2 + + def __repr__(self): + return "" % (self.x, self.y, self.z) + + def normalize(self): + x = self.x + y = self.y + z = self.z + norm = sqrt(x * x + y * y + z * z) + self.x /= norm + self.y /= norm + self.z /= norm + + def maximize(self, other): + self.x = self.x if self.x > other.x else other.x + self.y = self.y if self.y > other.y else other.y + self.z = self.z if self.z > other.z else other.z + return self + + +def maximize(points): + next = points[0] + for p in points[1:]: + next = next.maximize(p) + return next + + +def benchmark(n): + points = [None] * n + for i in range(n): + points[i] = Point(i) + for p in points: + p.normalize() + return maximize(points) + +result = benchmark(10000) +assert result.x <= 1.0 and result.y <= 1.0 and result.z <= 1.0 diff --git a/benches/benchmarks/gc_collect.py b/benches/benchmarks/gc_collect.py new file mode 100644 index 000000000..e43929540 --- /dev/null +++ b/benches/benchmarks/gc_collect.py @@ -0,0 +1,55 @@ +# Adapted from python/pyperformance 1.14.0 (gc_collect). +import gc + +CYCLES = 100 +LINKS = 20 + + +class Node: + def __init__(self): + self.next = None + self.prev = None + + def link_next(self, next): + self.next = next + self.next.prev = self + + +def create_cycle(node, n_links): + """Create a cycle of n_links nodes, starting with node.""" + + if n_links == 0: + return + + current = node + for i in range(n_links): + next_node = Node() + current.link_next(next_node) + current = next_node + + current.link_next(node) + + +def create_gc_cycles(n_cycles, n_links): + """Create n_cycles cycles n_links+1 nodes each.""" + + cycles = [] + for _ in range(n_cycles): + node = Node() + cycles.append(node) + create_cycle(node, n_links) + return cycles + + +def benchamark_collection(loops, cycles, links): + for _ in range(loops): + gc.collect() + all_cycles = create_gc_cycles(cycles, links) + + # Main loop to measure + del all_cycles + collected = gc.collect() + + assert collected is None or collected >= cycles * (links + 1) + +benchamark_collection(1, CYCLES, LINKS) diff --git a/benches/benchmarks/gc_traversal.py b/benches/benchmarks/gc_traversal.py new file mode 100644 index 000000000..f9e651796 --- /dev/null +++ b/benches/benchmarks/gc_traversal.py @@ -0,0 +1,28 @@ +# Adapted from python/pyperformance 1.14.0 (gc_traversal). +import gc + +N_LEVELS = 1000 + + +def create_recursive_containers(n_levels): + + current_list = [] + for n in range(n_levels): + new_list = [None] * n + for index in range(n): + new_list[index] = current_list + current_list = new_list + + return current_list + + +def benchamark_collection(loops, n_levels): + all_cycles = create_recursive_containers(n_levels) + for _ in range(loops): + gc.collect() + # Main loop to measure + collected = gc.collect() + + assert collected is None or collected == 0 + +benchamark_collection(1, N_LEVELS) diff --git a/benches/benchmarks/generators.py b/benches/benchmarks/generators.py new file mode 100644 index 000000000..ae5b582cd --- /dev/null +++ b/benches/benchmarks/generators.py @@ -0,0 +1,42 @@ +# Adapted from python/pyperformance 1.14.0 (generators). +""" +Benchmark recursive generators implemented in python +by traversing a binary tree. + +Author: Kumar Aditya +""" + +from __future__ import annotations + +from collections.abc import Iterator + +class Tree: + def __init__(self, left: Tree | None, value: int, right: Tree | None) -> None: + self.left = left + self.value = value + self.right = right + + def __iter__(self) -> Iterator[int]: + if self.left: + yield from self.left + yield self.value + if self.right: + yield from self.right + + +def tree(input: range) -> Tree | None: + n = len(input) + if n == 0: + return None + i = n // 2 + return Tree(tree(input[:i]), input[i], tree(input[i + 1:])) + +def bench_generators(loops: int) -> None: + assert list(tree(range(10))) == list(range(10)) + range_it = range(loops) + iterable = tree(range(100000)) + for _ in range_it: + for _ in iterable: + pass + +bench_generators(1) diff --git a/benches/benchmarks/json_dumps.py b/benches/benchmarks/json_dumps.py new file mode 100644 index 000000000..1b7737e09 --- /dev/null +++ b/benches/benchmarks/json_dumps.py @@ -0,0 +1,28 @@ +# Adapted from python/pyperformance 1.14.0 (json_dumps). + +import json + + +empty = ({}, 2000) +simple_data = { + "key1": 0, + "key2": True, + "key3": "value", + "key4": "foo", + "key5": "string", +} +simple = (simple_data, 1000) +nested_data = { + "key1": 0, + "key2": simple_data, + "key3": "value", + "key4": simple_data, + "key5": simple_data, + "key": "ąćż", +} +nested = (nested_data, 1000) +huge = ([nested_data] * 1000, 1) + +for obj, count in (empty, simple, nested, huge): + for _ in range(count): + json.dumps(obj) diff --git a/benches/benchmarks/nqueens.py b/benches/benchmarks/nqueens.py new file mode 100644 index 000000000..c714f9fe7 --- /dev/null +++ b/benches/benchmarks/nqueens.py @@ -0,0 +1,55 @@ +# Adapted from python/pyperformance 1.14.0 (nqueens). +"""Simple, brute-force N-Queens solver.""" + +__author__ = "collinwinter@google.com (Collin Winter)" + + +# Pure-Python implementation of itertools.permutations(). +def permutations(iterable, r=None): + """permutations(range(3), 2) --> (0,1) (0,2) (1,0) (1,2) (2,0) (2,1)""" + pool = tuple(iterable) + n = len(pool) + if r is None: + r = n + indices = list(range(n)) + cycles = list(range(n - r + 1, n + 1))[::-1] + yield tuple(pool[i] for i in indices[:r]) + while n: + for i in reversed(range(r)): + cycles[i] -= 1 + if cycles[i] == 0: + indices[i:] = indices[i + 1:] + indices[i:i + 1] + cycles[i] = n - i + else: + j = cycles[i] + indices[i], indices[-j] = indices[-j], indices[i] + yield tuple(pool[i] for i in indices[:r]) + break + else: + return + + +# From http://code.activestate.com/recipes/576647/ +def n_queens(queen_count): + """N-Queens solver. + + Args: + queen_count: the number of queens to solve for. This is also the + board size. + + Yields: + Solutions to the problem. Each yielded value is looks like + (3, 8, 2, 1, 4, ..., 6) where each number is the column position for the + queen, and the index into the tuple indicates the row. + """ + cols = range(queen_count) + for vec in permutations(cols): + if (queen_count == len(set(vec[i] + i for i in cols)) + == len(set(vec[i] - i for i in cols))): + yield vec + + +def bench_n_queens(queen_count): + list(n_queens(queen_count)) + +bench_n_queens(8) diff --git a/benches/benchmarks/pickle.py b/benches/benchmarks/pickle.py new file mode 100644 index 000000000..d603314b8 --- /dev/null +++ b/benches/benchmarks/pickle.py @@ -0,0 +1,33 @@ +# Adapted from python/pyperformance 1.14.0 (pickle_pure_python). + +import datetime +import pickle + + +profile = { + "age": 18, + "birthday": datetime.date(1980, 5, 7), + "country": "BR", + "encrypted_id": "G9urXXAJwjE", + "flags": 412317970704, + "friend_count": 0, + "locale_preference": "pt_BR", + "tags": ["a", "b", "c", "d", "e", "f", "g"], + "username": "collinwinter", +} +record = ([*range(20)], 60) +group = [dict(profile, id=index) for index in range(3)] +list_data = [[list(range(10)), list(range(10))] for _ in range(10)] +dict_data = {key: dict.fromkeys(range(10)) for key in range(100)} +protocol = pickle.HIGHEST_PROTOCOL +dumps = pickle._dumps + +for obj, count in ( + (profile, 20), + (record, 20), + (group, 20), + (list_data, 10), + (dict_data, 5), +): + for _ in range(count): + dumps(obj, protocol) diff --git a/benches/benchmarks/pidigits.py b/benches/benchmarks/pidigits.py new file mode 100644 index 000000000..69b31c673 --- /dev/null +++ b/benches/benchmarks/pidigits.py @@ -0,0 +1,57 @@ +# Adapted from python/pyperformance 1.14.0 (pidigits). +# coding: utf-8 +""" +Calculating some of the digits of π. + +This benchmark stresses big integer arithmetic. + +Adapted from code on: +http://benchmarksgame.alioth.debian.org/ +""" + +import itertools + +DEFAULT_DIGITS = 2000 +icount = itertools.count +islice = itertools.islice + + +def gen_x(): + return map(lambda k: (k, 4 * k + 2, 0, 2 * k + 1), icount(1)) + + +def compose(a, b): + aq, ar, as_, at = a + bq, br, bs, bt = b + return (aq * bq, + aq * br + ar * bt, + as_ * bq + at * bs, + as_ * br + at * bt) + + +def extract(z, j): + q, r, s, t = z + return (q * j + r) // (s * j + t) + + +def gen_pi_digits(): + z = (1, 0, 0, 1) + x = gen_x() + while 1: + y = extract(z, 3) + while y != extract(z, 4): + z = compose(z, next(x)) + y = extract(z, 3) + z = compose((10, -10 * y, 0, 1), z) + yield y + + +def calc_ndigits(n): + return list(islice(gen_pi_digits(), n)) + + +def add_cmdline_args(cmd, args): + cmd.extend(("--digits", str(args.digits))) + +digits = calc_ndigits(1000) +assert digits[:10] == [3, 1, 4, 1, 5, 9, 2, 6, 5, 3] diff --git a/benches/benchmarks/regex_effbot.py b/benches/benchmarks/regex_effbot.py new file mode 100644 index 000000000..f846fae64 --- /dev/null +++ b/benches/benchmarks/regex_effbot.py @@ -0,0 +1,158 @@ +# Adapted from python/pyperformance 1.14.0 (regex_effbot). + +"""Benchmarks for Python's regex engine. + +These are some of the original benchmarks used to tune Python's regex engine +in 2000 written by Fredrik Lundh. Retreived from +http://mail.python.org/pipermail/python-dev/2000-August/007797.html and +integrated into Unladen Swallow's pyperf.py in 2009 by David Laing. + +These benchmarks are of interest since they helped to guide the original +optimization of the sre engine, and we shouldn't necessarily ignore them just +because they're "old". +""" + +# Python imports +import re + +# Local imports +USE_BYTES = False + + +def re_compile(s): + if USE_BYTES: + return re.compile(s.encode('latin1')) + else: + return re.compile(s) + +# These are the regular expressions to be tested. These sync up, +# index-for-index with the list of strings generated by gen_string_table() +# below. + + +def gen_regex_table(): + return [ + re_compile('Python|Perl'), + re_compile('Python|Perl'), + re_compile('(Python|Perl)'), + re_compile('(?:Python|Perl)'), + re_compile('Python'), + re_compile('Python'), + re_compile('.*Python'), + re_compile('.*Python.*'), + re_compile('.*(Python)'), + re_compile('.*(?:Python)'), + re_compile('Python|Perl|Tcl'), + re_compile('Python|Perl|Tcl'), + re_compile('(Python|Perl|Tcl)'), + re_compile('(?:Python|Perl|Tcl)'), + re_compile('(Python)\\1'), + re_compile('(Python)\\1'), + re_compile('([0a-z][a-z0-9]*,)+'), + re_compile('(?:[0a-z][a-z0-9]*,)+'), + re_compile('([a-z][a-z0-9]*,)+'), + re_compile('(?:[a-z][a-z0-9]*,)+'), + re_compile('.*P.*y.*t.*h.*o.*n.*')] + + +def gen_string_table(n): + """Generates the list of strings that will be used in the benchmarks. + + All strings have repeated prefixes and suffices, and n specifies the + number of repetitions. + """ + strings = [] + + def append(s): + if USE_BYTES: + strings.append(s.encode('latin1')) + else: + strings.append(s) + append('-' * n + 'Perl' + '-' * n) + append('P' * n + 'Perl' + 'P' * n) + append('-' * n + 'Perl' + '-' * n) + append('-' * n + 'Perl' + '-' * n) + append('-' * n + 'Python' + '-' * n) + append('P' * n + 'Python' + 'P' * n) + append('-' * n + 'Python' + '-' * n) + append('-' * n + 'Python' + '-' * n) + append('-' * n + 'Python' + '-' * n) + append('-' * n + 'Python' + '-' * n) + append('-' * n + 'Perl' + '-' * n) + append('P' * n + 'Perl' + 'P' * n) + append('-' * n + 'Perl' + '-' * n) + append('-' * n + 'Perl' + '-' * n) + append('-' * n + 'PythonPython' + '-' * n) + append('P' * n + 'PythonPython' + 'P' * n) + append('-' * n + 'a5,b7,c9,' + '-' * n) + append('-' * n + 'a5,b7,c9,' + '-' * n) + append('-' * n + 'a5,b7,c9,' + '-' * n) + append('-' * n + 'a5,b7,c9,' + '-' * n) + append('-' * n + 'Python' + '-' * n) + return strings + + +def init_benchmarks(n_values=None): + """Initialize the strings we'll run the regexes against. + + The strings used in the benchmark are prefixed and suffixed by + strings that are repeated n times. + + The sequence n_values contains the values for n. + If n_values is None the values of n from the original benchmark + are used. + + The generated list of strings is cached in the string_tables + variable, which is indexed by n. + + Returns: + A list of string prefix/suffix lengths. + """ + + if n_values is None: + n_values = (0, 5, 50, 250, 1000, 5000, 10000) + + string_tables = {n: gen_string_table(n) for n in n_values} + regexs = gen_regex_table() + + data = [] + for n in n_values: + for id in range(len(regexs)): + regex = regexs[id] + string = string_tables[n][id] + data.append((regex, string)) + return data + + +def bench_regex_effbot(loops): + if bench_regex_effbot.data is None: + bench_regex_effbot.data = init_benchmarks() + data = bench_regex_effbot.data + + range_it = range(loops) + search = re.search + + for _ in range_it: + # Runs all of the benchmarks for a given value of n. + for regex, string in data: + # search 10 times + search(regex, string) + search(regex, string) + search(regex, string) + search(regex, string) + search(regex, string) + search(regex, string) + search(regex, string) + search(regex, string) + search(regex, string) + search(regex, string) + +# cached data, generated at the first call +bench_regex_effbot.data = None + + +def add_cmdline_args(cmd, args): + if args.force_bytes: + cmd.append("--force_bytes") + +bench_regex_effbot(1) diff --git a/benches/benchmarks/richards.py b/benches/benchmarks/richards.py new file mode 100644 index 000000000..c8d147f36 --- /dev/null +++ b/benches/benchmarks/richards.py @@ -0,0 +1,415 @@ +# Adapted from python/pyperformance 1.14.0 (richards). +""" +based on a Java version: + Based on original version written in BCPL by Dr Martin Richards + in 1981 at Cambridge University Computer Laboratory, England + and a C++ version derived from a Smalltalk version written by + L Peter Deutsch. + Java version: Copyright (C) 1995 Sun Microsystems, Inc. + Translation from C++, Mario Wolczko + Outer loop added by Alex Jacoby +""" + +# Task IDs +I_IDLE = 1 +I_WORK = 2 +I_HANDLERA = 3 +I_HANDLERB = 4 +I_DEVA = 5 +I_DEVB = 6 + +# Packet types +K_DEV = 1000 +K_WORK = 1001 + +# Packet + +BUFSIZE = 4 + +BUFSIZE_RANGE = range(BUFSIZE) + + +class Packet(object): + + def __init__(self, l, i, k): + self.link = l + self.ident = i + self.kind = k + self.datum = 0 + self.data = [0] * BUFSIZE + + def append_to(self, lst): + self.link = None + if lst is None: + return self + else: + p = lst + next = p.link + while next is not None: + p = next + next = p.link + p.link = self + return lst + +# Task Records + + +class TaskRec(object): + pass + + +class DeviceTaskRec(TaskRec): + + def __init__(self): + self.pending = None + + +class IdleTaskRec(TaskRec): + + def __init__(self): + self.control = 1 + self.count = 10000 + + +class HandlerTaskRec(TaskRec): + + def __init__(self): + self.work_in = None + self.device_in = None + + def workInAdd(self, p): + self.work_in = p.append_to(self.work_in) + return self.work_in + + def deviceInAdd(self, p): + self.device_in = p.append_to(self.device_in) + return self.device_in + + +class WorkerTaskRec(TaskRec): + + def __init__(self): + self.destination = I_HANDLERA + self.count = 0 +# Task + + +class TaskState(object): + + def __init__(self): + self.packet_pending = True + self.task_waiting = False + self.task_holding = False + + def packetPending(self): + self.packet_pending = True + self.task_waiting = False + self.task_holding = False + return self + + def waiting(self): + self.packet_pending = False + self.task_waiting = True + self.task_holding = False + return self + + def running(self): + self.packet_pending = False + self.task_waiting = False + self.task_holding = False + return self + + def waitingWithPacket(self): + self.packet_pending = True + self.task_waiting = True + self.task_holding = False + return self + + def isPacketPending(self): + return self.packet_pending + + def isTaskWaiting(self): + return self.task_waiting + + def isTaskHolding(self): + return self.task_holding + + def isTaskHoldingOrWaiting(self): + return self.task_holding or (not self.packet_pending and self.task_waiting) + + def isWaitingWithPacket(self): + return self.packet_pending and self.task_waiting and not self.task_holding + + +tracing = False +layout = 0 + + +def trace(a): + global layout + layout -= 1 + if layout <= 0: + print() + layout = 50 + print(a, end='') + + +TASKTABSIZE = 10 + + +class TaskWorkArea(object): + + def __init__(self): + self.taskTab = [None] * TASKTABSIZE + + self.taskList = None + + self.holdCount = 0 + self.qpktCount = 0 + + +taskWorkArea = TaskWorkArea() + + +class Task(TaskState): + + def __init__(self, i, p, w, initialState, r): + self.link = taskWorkArea.taskList + self.ident = i + self.priority = p + self.input = w + + self.packet_pending = initialState.isPacketPending() + self.task_waiting = initialState.isTaskWaiting() + self.task_holding = initialState.isTaskHolding() + + self.handle = r + + taskWorkArea.taskList = self + taskWorkArea.taskTab[i] = self + + def fn(self, pkt, r): + raise NotImplementedError + + def addPacket(self, p, old): + if self.input is None: + self.input = p + self.packet_pending = True + if self.priority > old.priority: + return self + else: + p.append_to(self.input) + return old + + def runTask(self): + if self.isWaitingWithPacket(): + msg = self.input + self.input = msg.link + if self.input is None: + self.running() + else: + self.packetPending() + else: + msg = None + + return self.fn(msg, self.handle) + + def waitTask(self): + self.task_waiting = True + return self + + def hold(self): + taskWorkArea.holdCount += 1 + self.task_holding = True + return self.link + + def release(self, i): + t = self.findtcb(i) + t.task_holding = False + if t.priority > self.priority: + return t + else: + return self + + def qpkt(self, pkt): + t = self.findtcb(pkt.ident) + taskWorkArea.qpktCount += 1 + pkt.link = None + pkt.ident = self.ident + return t.addPacket(pkt, self) + + def findtcb(self, id): + t = taskWorkArea.taskTab[id] + if t is None: + raise Exception("Bad task id %d" % id) + return t + + +# DeviceTask + + +class DeviceTask(Task): + + def __init__(self, i, p, w, s, r): + Task.__init__(self, i, p, w, s, r) + + def fn(self, pkt, r): + d = r + assert isinstance(d, DeviceTaskRec) + if pkt is None: + pkt = d.pending + if pkt is None: + return self.waitTask() + else: + d.pending = None + return self.qpkt(pkt) + else: + d.pending = pkt + if tracing: + trace(pkt.datum) + return self.hold() + + +class HandlerTask(Task): + + def __init__(self, i, p, w, s, r): + Task.__init__(self, i, p, w, s, r) + + def fn(self, pkt, r): + h = r + assert isinstance(h, HandlerTaskRec) + if pkt is not None: + if pkt.kind == K_WORK: + h.workInAdd(pkt) + else: + h.deviceInAdd(pkt) + work = h.work_in + if work is None: + return self.waitTask() + count = work.datum + if count >= BUFSIZE: + h.work_in = work.link + return self.qpkt(work) + + dev = h.device_in + if dev is None: + return self.waitTask() + + h.device_in = dev.link + dev.datum = work.data[count] + work.datum = count + 1 + return self.qpkt(dev) + +# IdleTask + + +class IdleTask(Task): + + def __init__(self, i, p, w, s, r): + Task.__init__(self, i, 0, None, s, r) + + def fn(self, pkt, r): + i = r + assert isinstance(i, IdleTaskRec) + i.count -= 1 + if i.count == 0: + return self.hold() + elif i.control & 1 == 0: + i.control //= 2 + return self.release(I_DEVA) + else: + i.control = i.control // 2 ^ 0xd008 + return self.release(I_DEVB) + + +# WorkTask + + +A = ord('A') + + +class WorkTask(Task): + + def __init__(self, i, p, w, s, r): + Task.__init__(self, i, p, w, s, r) + + def fn(self, pkt, r): + w = r + assert isinstance(w, WorkerTaskRec) + if pkt is None: + return self.waitTask() + + if w.destination == I_HANDLERA: + dest = I_HANDLERB + else: + dest = I_HANDLERA + + w.destination = dest + pkt.ident = dest + pkt.datum = 0 + + for i in BUFSIZE_RANGE: # range(BUFSIZE) + w.count += 1 + if w.count > 26: + w.count = 1 + pkt.data[i] = A + w.count - 1 + + return self.qpkt(pkt) + + +def schedule(): + t = taskWorkArea.taskList + while t is not None: + if tracing: + print("tcb =", t.ident) + + if t.isTaskHoldingOrWaiting(): + t = t.link + else: + if tracing: + trace(chr(ord("0") + t.ident)) + t = t.runTask() + + +class Richards(object): + + def run(self, iterations): + for i in range(iterations): + taskWorkArea.holdCount = 0 + taskWorkArea.qpktCount = 0 + + IdleTask(I_IDLE, 1, 10000, TaskState().running(), IdleTaskRec()) + + wkq = Packet(None, 0, K_WORK) + wkq = Packet(wkq, 0, K_WORK) + WorkTask(I_WORK, 1000, wkq, TaskState( + ).waitingWithPacket(), WorkerTaskRec()) + + wkq = Packet(None, I_DEVA, K_DEV) + wkq = Packet(wkq, I_DEVA, K_DEV) + wkq = Packet(wkq, I_DEVA, K_DEV) + HandlerTask(I_HANDLERA, 2000, wkq, TaskState( + ).waitingWithPacket(), HandlerTaskRec()) + + wkq = Packet(None, I_DEVB, K_DEV) + wkq = Packet(wkq, I_DEVB, K_DEV) + wkq = Packet(wkq, I_DEVB, K_DEV) + HandlerTask(I_HANDLERB, 3000, wkq, TaskState( + ).waitingWithPacket(), HandlerTaskRec()) + + wkq = None + DeviceTask(I_DEVA, 4000, wkq, + TaskState().waiting(), DeviceTaskRec()) + DeviceTask(I_DEVB, 5000, wkq, + TaskState().waiting(), DeviceTaskRec()) + + schedule() + + if taskWorkArea.holdCount == 9297 and taskWorkArea.qpktCount == 23246: + pass + else: + return False + + return True + +assert Richards().run(1) diff --git a/benches/benchmarks/unpickle.py b/benches/benchmarks/unpickle.py new file mode 100644 index 000000000..03297c4c2 --- /dev/null +++ b/benches/benchmarks/unpickle.py @@ -0,0 +1,33 @@ +# Adapted from python/pyperformance 1.14.0 (unpickle_pure_python). + +import datetime +import pickle + + +profile = { + "age": 18, + "birthday": datetime.date(1980, 5, 7), + "country": "BR", + "encrypted_id": "G9urXXAJwjE", + "flags": 412317970704, + "friend_count": 0, + "locale_preference": "pt_BR", + "tags": ["a", "b", "c", "d", "e", "f", "g"], + "username": "collinwinter", +} +record = ([*range(20)], 60) +group = [dict(profile, id=index) for index in range(3)] +list_data = [[list(range(10)), list(range(10))] for _ in range(10)] +protocol = pickle.HIGHEST_PROTOCOL +dumps = pickle._dumps +loads = pickle._loads + +for obj, count in ( + (profile, 20), + (record, 20), + (group, 20), + (list_data, 10), +): + encoded = dumps(obj, protocol) + for _ in range(count): + assert loads(encoded) == obj diff --git a/tests/benchmark_scripts.rs b/tests/benchmark_scripts.rs new file mode 100644 index 000000000..4a2f02006 --- /dev/null +++ b/tests/benchmark_scripts.rs @@ -0,0 +1,84 @@ +// cspell:ignore pyperformance nqueens pidigits +use rustpython::{InterpreterBuilder, InterpreterBuilderExt}; +use std::{fs, path::Path}; + +const PYPERFORMANCE_BENCHMARKS: &[&str] = &[ + "base64.py", + "decimal_factorial.py", + "deepcopy.py", + "deltablue.py", + "float.py", + "gc_collect.py", + "gc_traversal.py", + "generators.py", + "json_dumps.py", + "nqueens.py", + "pickle.py", + "pidigits.py", + "regex_effbot.py", + "richards.py", + "unpickle.py", +]; + +#[test] +fn pyperformance_benchmarks_run_in_rustpython() { + let benchmark_dir = Path::new("benches/benchmarks"); + + for name in PYPERFORMANCE_BENCHMARKS { + let path = benchmark_dir.join(name); + let source = fs::read_to_string(&path) + .unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display())); + let source_path = path.to_string_lossy(); + + InterpreterBuilder::new() + .init_stdlib() + .interpreter() + .enter(|vm| { + let code = vm + .compile(&source, rustpython::vm::compiler::Mode::Exec, &*source_path) + .unwrap_or_else(|err| panic!("failed to compile {}: {err}", path.display())); + let scope = vm.new_scope_with_builtins(); + if let Err(err) = vm.run_code_obj(code, scope) { + vm.print_exception(err); + panic!("failed to execute {}", path.display()); + } + }); + } +} + +#[test] +fn pyperformance_benchmarks_restore_interpreter_state() { + let source = r#" +import decimal +import pickle +import sys + +missing = object() +pickle_module = sys.modules.get("pickle", missing) +pickle_accelerator = sys.modules.get("_pickle", missing) +decimal_precision = decimal.getcontext().prec + +for path in ( + "benches/benchmarks/pickle.py", + "benches/benchmarks/unpickle.py", + "benches/benchmarks/decimal_factorial.py", +): + with open(path) as benchmark_file: + benchmark_source = benchmark_file.read() + exec(compile(benchmark_source, path, "exec"), {}) + +assert sys.modules.get("pickle", missing) is pickle_module +assert sys.modules.get("_pickle", missing) is pickle_accelerator +assert decimal.getcontext().prec == decimal_precision +"#; + + InterpreterBuilder::new() + .init_stdlib() + .interpreter() + .enter(|vm| { + if let Err(err) = vm.run_simple_string(source) { + vm.print_exception(err); + panic!("pyperformance benchmarks leaked interpreter state"); + } + }); +}