[pypy-commit] benchmarks deltablue: Added the deltablue benchmark.
toastdriven
noreply at buildbot.pypy.org
Tue Sep 3 10:49:12 CEST 2013
Author: Daniel Lindsley <daniel at toastdriven.com>
Branch: deltablue
Changeset: r230:7c266c787055
Date: 2013-09-01 18:17 -0700
http://bitbucket.org/pypy/benchmarks/changeset/7c266c787055/
Log: Added the deltablue benchmark.
diff --git a/benchmarks.py b/benchmarks.py
--- a/benchmarks.py
+++ b/benchmarks.py
@@ -80,7 +80,7 @@
for name in ['float', 'nbody_modified', 'meteor-contest', 'fannkuch',
'spectral-norm', 'chaos', 'telco', 'go', 'pyflate-fast',
'raytrace-simple', 'crypto_pyaes', 'bm_mako', 'bm_chameleon',
- 'json_bench', 'pidigits', 'hexiom2', 'eparse']:
+ 'json_bench', 'pidigits', 'hexiom2', 'eparse', 'deltablue']:
_register_new_bm(name, name, globals(), **opts.get(name, {}))
for name in ['names', 'iteration', 'tcp', 'pb', ]:#'web']:#, 'accepts']:
diff --git a/own/deltablue.py b/own/deltablue.py
new file mode 100644
--- /dev/null
+++ b/own/deltablue.py
@@ -0,0 +1,642 @@
+"""
+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`: (http://code.google.com/p/v8/source/browse/branches/bleeding_edge/benchmarks/deltablue.js)
+
+"""
+from __future__ import print_function
+
+
+# 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.STONG_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 == 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 '<Variable: %s - %s>' % (
+ 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, dest = None, 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():
+ chain_test(100)
+ projection_test(100)
+
+
+# Specific to the PyPy implementation, to run within the main harnass.
+def main(n):
+ import time
+ times = []
+
+ for i in range(n):
+ t1 = time.time()
+ delta_blue()
+ t2 = time.time()
+ times.append(t2 - t1)
+
+ return times
+
+
+if __name__ == '__main__':
+ import util
+ import optparse
+
+ parser = optparse.OptionParser(
+ usage="%prog [options]",
+ description="Test the performance of the DeltaBlue benchmark")
+ util.add_standard_options_to(parser)
+ options, args = parser.parse_args()
+
+ util.run_benchmark(options, options.num_runs, main)
More information about the pypy-commit
mailing list