Created
October 23, 2014 01:01
-
-
Save chrisseaton/06fb70b61cd87fb43eb5 to your computer and use it in GitHub Desktop.
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
# Port of deltablue.py, as documented below, to Ruby. | |
# Stefan Marr, 2014-04-28 | |
# | |
# Was: 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) | |
# HOORAY FOR GLOBALS... Oh wait. | |
# In spirit of the original, we'll keep it, but ugh. | |
$planner = nil | |
class Strength | |
attr_accessor :strength | |
def initialize(strength, name) | |
@strength = strength | |
@name = name | |
end | |
REQUIRED = self.new(0, "required") | |
STRONG_PREFERRED = self.new(1, "strongPreferred") | |
PREFERRED = self.new(2, "preferred") | |
STRONG_DEFAULT = self.new(3, "strongDefault") | |
NORMAL = self.new(4, "normal") | |
WEAK_DEFAULT = self.new(5, "weakDefault") | |
WEAKEST = self.new(6, "weakest") | |
def self.stronger(s1, s2) | |
s1.strength < s2.strength | |
end | |
def self.weaker(s1, s2) | |
s1.strength > s2.strength | |
end | |
def self.weakest_of(s1, s2) | |
if self.weaker(s1, s2) | |
return s1 | |
end | |
return s2 | |
end | |
def self.strongest(s1, s2) | |
if self.stronger(s1, s2) | |
return s1 | |
end | |
return s2 | |
end | |
def next_weaker | |
case @strength | |
when 0 | |
WEAKEST | |
when 1 | |
WEAK_DEFAULT | |
when 2 | |
NORMAL | |
when 3 | |
STRONG_DEFAULT | |
when 4 | |
PREFERRED | |
when 5 | |
# TODO: This looks like a bug in the original code. Shouldn't this be | |
# ``STRONG_PREFERRED? Keeping for porting sake... | |
REQUIRED | |
end | |
end | |
end | |
class Constraint | |
attr_accessor :strength | |
def initialize(strength) | |
@strength = strength | |
end | |
def add_constraint | |
add_to_graph | |
$planner.incremental_add(self) | |
end | |
def satisfy(mark) | |
choose_method(mark) | |
if not is_satisfied | |
if @strength == Strength::REQUIRED | |
puts 'Could not satisfy a required constraint!' | |
end | |
return nil | |
end | |
mark_inputs(mark) | |
out = output | |
overridden = out.determined_by | |
unless overridden.nil? | |
overridden.mark_unsatisfied | |
end | |
out.determined_by = self | |
unless $planner.add_propagate(self, mark) | |
puts 'Cycle encountered' | |
end | |
out.mark = mark | |
overridden | |
end | |
def destroy_constraint | |
if is_satisfied | |
$planner.incremental_remove(self) | |
else | |
remove_from_graph | |
end | |
end | |
def is_input | |
false | |
end | |
end | |
class UnaryConstraint < Constraint | |
def initialize(v, strength) | |
super(strength) | |
@my_output = v | |
@satisfied = false | |
add_constraint | |
end | |
def add_to_graph | |
@my_output.add_constraint(self) | |
@satisfied = false | |
end | |
def choose_method(mark) | |
if @my_output.mark != mark and Strength.stronger(@strength, @my_output.walk_strength) | |
@satisfied = true | |
else | |
@satisfied = false | |
end | |
end | |
def is_satisfied | |
@satisfied | |
end | |
def mark_inputs(mark) | |
# No-ops. | |
end | |
def output | |
# Ugh. Keeping it for consistency with the original. So much for | |
# "we're all adults here"... | |
@my_output | |
end | |
def recalculate | |
@my_output.walk_strength = @strength | |
@my_output.stay = !is_input | |
if @my_output.stay | |
execute | |
end | |
end | |
def mark_unsatisfied | |
@satisfied = false | |
end | |
def inputs_known(mark) | |
true | |
end | |
def remove_from_graph | |
unless @my_output.nil? | |
@my_output.remove_constraint(self) | |
@satisfied = false | |
end | |
end | |
end | |
class StayConstraint < UnaryConstraint | |
def execute | |
# The methods, THEY DO NOTHING. | |
end | |
end | |
class EditConstraint < UnaryConstraint | |
def is_input | |
true | |
end | |
def execute | |
# This constraint also does nothing. | |
end | |
end | |
class Direction | |
# Hooray for things that ought to be structs! | |
NONE = 0 | |
FORWARD = 1 | |
BACKWARD = -1 | |
end | |
class BinaryConstraint < Constraint | |
def initialize(v1, v2, strength) | |
super(strength) | |
@v1 = v1 | |
@v2 = v2 | |
@direction = Direction::NONE | |
add_constraint | |
end | |
def choose_method(mark) | |
if @v1.mark == mark | |
if @v2.mark != mark and Strength::stronger(@strength, @v2.walk_strength) | |
@direction = Direction::FORWARD | |
else | |
@direction = Direction::BACKWARD | |
end | |
end | |
if @v2.mark == mark | |
if @v1.mark != mark and Strength::stronger(@strength, @v1.walk_strength) | |
@direction = Direction::BACKWARD | |
else | |
@direction = Direction::NONE | |
end | |
end | |
if Strength.weaker(@v1.walk_strength, @v2.walk_strength) | |
if Strength.stronger(@strength, @v1.walk_strength) | |
@direction = Direction::BACKWARD | |
else | |
@direction = Direction::NONE | |
end | |
else | |
if Strength.stronger(@strength, @v2.walk_strength) | |
@direction = Direction::FORWARD | |
else | |
@direction = Direction::BACKWARD | |
end | |
end | |
end | |
def add_to_graph | |
@v1.add_constraint(self) | |
@v2.add_constraint(self) | |
@direction = Direction::NONE | |
end | |
def is_satisfied | |
@direction != Direction::NONE | |
end | |
def mark_inputs(mark) | |
input.mark = mark | |
end | |
def input | |
if @direction == Direction::FORWARD | |
return @v1 | |
end | |
@v2 | |
end | |
def output | |
if @direction == Direction::FORWARD | |
return @v2 | |
end | |
@v1 | |
end | |
def recalculate | |
ihn = input | |
out = output | |
out.walk_strength = Strength.weakest_of(@strength, ihn.walk_strength) | |
out.stay = ihn.stay | |
if out.stay | |
execute | |
end | |
end | |
def mark_unsatisfied | |
@direction = Direction::NONE | |
end | |
def inputs_known(mark) | |
i = input | |
i.mark == mark or i.stay or i.determined_by.nil? | |
end | |
def remove_from_graph | |
unless @v1.nil? | |
@v1.remove_constraint(self) | |
end | |
unless @v2.nil? | |
@v2.remove_constraint(self) | |
end | |
@direction = Direction::NONE | |
end | |
end | |
class ScaleConstraint < BinaryConstraint | |
def initialize(src, scale, offset, dest, strength) | |
@direction = Direction::NONE | |
@scale = scale | |
@offset = offset | |
super(src, dest, strength) | |
end | |
def add_to_graph | |
super | |
@scale.add_constraint(self) | |
@offset.add_constraint(self) | |
end | |
def remove_from_graph | |
super | |
unless @scale.nil? | |
@scale.remove_constraint(self) | |
end | |
unless @offset.nil? | |
@offset.remove_constraint(self) | |
end | |
end | |
def mark_inputs(mark) | |
super | |
@scale.mark = mark | |
@offset.mark = mark | |
end | |
def execute | |
if @direction == Direction::FORWARD | |
@v2.value = @v1.value * @scale.value + @offset.value | |
else | |
@v1.value = (@v2.value - @offset.value) / @scale.value | |
end | |
end | |
def recalculate | |
ihn = input | |
out = output | |
out.walk_strength = Strength.weakest_of(@strength, ihn.walk_strength) | |
out.stay = (ihn.stay and @scale.stay and @offset.stay) | |
if out.stay | |
execute | |
end | |
end | |
end | |
class EqualityConstraint < BinaryConstraint | |
def execute | |
output.value = input.value | |
end | |
end | |
class Variable | |
attr_accessor :mark | |
attr_accessor :walk_strength | |
attr_accessor :determined_by | |
attr_accessor :stay | |
attr_accessor :value | |
attr_accessor :constraints | |
def initialize(name, initial_value = 0) | |
@name = name | |
@value = initial_value | |
@constraints = [] | |
@determined_by = nil | |
@mark = 0 | |
@walk_strength = Strength::WEAKEST | |
@stay = true | |
end | |
def inspect | |
# To make debugging this beast from pdb easier... | |
return "<Variable: #{@name} - #{value}>" | |
end | |
def add_constraint(constraint) | |
@constraints << constraint | |
end | |
def remove_constraint(constraint) | |
@constraints.delete(constraint) | |
if @determined_by == constraint | |
@determined_by = nil | |
end | |
end | |
end | |
class Planner | |
def initialize | |
@current_mark = 0 | |
end | |
def incremental_add(constraint) | |
mark = new_mark | |
overridden = constraint.satisfy(mark) | |
until overridden.nil? | |
overridden = overridden.satisfy(mark) | |
end | |
end | |
def incremental_remove(constraint) | |
out = constraint.output | |
constraint.mark_unsatisfied | |
constraint.remove_from_graph | |
unsatisfied = remove_propagate_from(out) | |
strength = Strength::REQUIRED | |
loop do | |
for u in unsatisfied | |
if u.strength == strength | |
incremental_add(u) | |
end | |
strength = strength.next_weaker | |
end | |
break if strength != Strength::WEAKEST | |
end | |
end | |
def new_mark | |
@current_mark += 1 | |
@current_mark | |
end | |
def make_plan(sources) | |
mark = new_mark | |
plan = Plan.new() | |
todo = sources | |
until todo.empty? | |
c = todo.pop | |
if c.output.mark != mark and c.inputs_known(mark) | |
plan.add_constraint(c) | |
c.output.mark = mark | |
add_constraints_consuming_to(c.output, todo) | |
end | |
end | |
plan | |
end | |
def extract_plan_from_constraints(constraints) | |
sources = [] | |
for c in constraints | |
if c.is_input and c.is_satisfied | |
sources << c | |
end | |
end | |
make_plan(sources) | |
end | |
def add_propagate(c, mark) | |
todo = [] | |
todo << c | |
until todo.empty? | |
d = todo.pop | |
if d.output.mark == mark | |
incremental_remove(c) | |
return false | |
end | |
d.recalculate | |
add_constraints_consuming_to(d.output, todo) | |
end | |
true | |
end | |
def remove_propagate_from(out) | |
out.determined_by = nil | |
out.walk_strength = Strength::WEAKEST | |
out.stay = true | |
unsatisfied = [] | |
todo = [] | |
todo << out | |
until todo.empty? | |
v = todo.pop | |
for c in v.constraints | |
unless c.is_satisfied | |
unsatisfied << c | |
end | |
end | |
determining = v.determined_by | |
for c in v.constraints | |
if c != determining and c.is_satisfied | |
c.recalculate | |
todo << c.output | |
end | |
end | |
end | |
unsatisfied | |
end | |
def add_constraints_consuming_to(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 << c | |
end | |
end | |
end | |
end | |
class Plan | |
def initialize | |
@v = [] | |
end | |
def add_constraint(c) | |
@v << c | |
end | |
def size | |
@v.size | |
end | |
def [](index) | |
@v[index] | |
end | |
def execute | |
for c in @v | |
c.execute | |
end | |
end | |
end | |
# 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. | |
$planner = Planner.new | |
prev, first, last = nil, nil, nil | |
# We need to go up to n inclusively. | |
for i in 0..n | |
name = "v%s" % i | |
v = Variable.new(name) | |
unless prev.nil? | |
EqualityConstraint.new(prev, v, Strength::REQUIRED) | |
end | |
if i == 0 | |
first = v | |
end | |
if i == n | |
last = v | |
end | |
prev = v | |
end | |
StayConstraint.new(last, Strength::STRONG_DEFAULT) | |
edit = EditConstraint.new(first, Strength::PREFERRED) | |
edits = [] | |
edits << edit | |
plan = $planner.extract_plan_from_constraints(edits) | |
for i in 0..99 | |
first.value = i | |
plan.execute | |
if last.value != i | |
puts 'Chain test failed.' | |
exit | |
end | |
end | |
end | |
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. | |
$planner = Planner.new | |
scale = Variable.new("scale", 10) | |
offset = Variable.new("offset", 1000) | |
src, dst = nil, nil | |
dests = [] | |
for i in 0..(n - 1) | |
src = Variable.new("src%s" % i, i) | |
dst = Variable.new("dst%s" % i, i) | |
dests << dst | |
StayConstraint.new(src, Strength::NORMAL) | |
ScaleConstraint.new(src, scale, offset, dst, Strength::REQUIRED) | |
end | |
change(src, 17) | |
if dst.value != 1170 | |
puts 'Projection 1 failed' | |
exit | |
end | |
change(dst, 1050) | |
if src.value != 5 | |
puts 'Projection 2 failed' | |
exit | |
end | |
change(scale, 5) | |
for i in 0..(n - 2) | |
if dests[i].value != (i * 5 + 1000) | |
puts 'Projection 3 failed' | |
exit | |
end | |
end | |
change(offset, 2000) | |
for i in 0..(n - 2) | |
if dests[i].value != (i * 5 + 2000) | |
puts 'Projection 4 failed' | |
exit | |
end | |
end | |
end | |
def change(v, new_value) | |
edit = EditConstraint.new(v, Strength::PREFERRED) | |
edits = [] | |
edits << edit | |
plan = $planner.extract_plan_from_constraints(edits) | |
for i in 0..9 | |
v.value = new_value | |
plan.execute | |
end | |
edit.destroy_constraint | |
end | |
def run(iterations) | |
chain_test(iterations) | |
projection_test(iterations) | |
end | |
def harness_input | |
10_000 | |
end | |
def harness_sample(input) | |
run(input) | |
end | |
def harness_verify(output) | |
# Self-verifies | |
true | |
end | |
require 'bench/harness' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment