Skip to content

Instantly share code, notes, and snippets.

@subbuss
Created October 23, 2014 14:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save subbuss/ff8d9761077de66b1b87 to your computer and use it in GitHub Desktop.
Save subbuss/ff8d9761077de66b1b87 to your computer and use it in GitHub Desktop.
# 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 warmup
1_000.times do
run(100)
end
end
def sample
run(10_000)
end
def name
return "deltablue"
end
require 'benchmark'
TIMES = (ARGV[0] || 1).to_i
N = (ARGV[1] || 10000).to_i
TIMES.times {
puts Benchmark.measure { run N }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment