From 4775855fd7ae65f4f3ec797179990339790f2777 Mon Sep 17 00:00:00 2001 From: Troels Bjerre Lund Date: Wed, 8 Jun 2022 14:47:35 +0200 Subject: [PATCH] [K/N] Add two more benchmarks from the V8 suite --- .../performance/ring/src/main/kotlin/main.kt | 2 + .../org/jetbrains/ring/DeltaBlueBenchmark.kt | 797 ++++++++++++++++++ .../org/jetbrains/ring/RichardsBenchmark.kt | 527 ++++++++++++ 3 files changed, 1326 insertions(+) create mode 100644 kotlin-native/performance/ring/src/main/kotlin/org/jetbrains/ring/DeltaBlueBenchmark.kt create mode 100644 kotlin-native/performance/ring/src/main/kotlin/org/jetbrains/ring/RichardsBenchmark.kt diff --git a/kotlin-native/performance/ring/src/main/kotlin/main.kt b/kotlin-native/performance/ring/src/main/kotlin/main.kt index 5b95e19e53b..f1395f4b2d2 100644 --- a/kotlin-native/performance/ring/src/main/kotlin/main.kt +++ b/kotlin-native/performance/ring/src/main/kotlin/main.kt @@ -37,6 +37,7 @@ class RingLauncher : Launcher() { "ClassStream.copy" to BenchmarkEntryWithInit.create(::ClassStreamBenchmark, { copy() }), "ClassStream.filter" to BenchmarkEntryWithInit.create(::ClassStreamBenchmark, { filter() }), "ClassStream.reduce" to BenchmarkEntryWithInit.create(::ClassStreamBenchmark, { reduce() }), + "DeltaBlue" to BenchmarkEntryWithInit.create(::DeltaBlueBenchmark, { deltaBlue() }), "Elvis.testElvis" to BenchmarkEntryWithInit.create(::ElvisBenchmark, { testElvis() }), "Euler.problem1bySequence" to BenchmarkEntryWithInit.create(::EulerBenchmark, { problem1bySequence() }), "Euler.problem9" to BenchmarkEntryWithInit.create(::EulerBenchmark, { problem9() }), @@ -57,6 +58,7 @@ class RingLauncher : Launcher() { "MatrixMap.add" to BenchmarkEntryWithInit.create(::MatrixMapBenchmark, { add() }), "PrimeList.calcDirect" to BenchmarkEntryWithInit.create(::PrimeListBenchmark, { calcDirect() }), "PrimeList.calcEratosthenes" to BenchmarkEntryWithInit.create(::PrimeListBenchmark, { calcEratosthenes() }), + "Richards" to BenchmarkEntryWithInit.create(::RichardsBenchmark, { runRichards() }), "Singleton.access" to BenchmarkEntryWithInit.create(::SingletonBenchmark, { access() }), "Splay" to BenchmarkEntryWithInitAndValidation.create(::SplayBenchmark, { runSplay() }, { splayTearDown() }), "String.stringConcat" to BenchmarkEntryWithInit.create(::StringBenchmark, { stringConcat() }), diff --git a/kotlin-native/performance/ring/src/main/kotlin/org/jetbrains/ring/DeltaBlueBenchmark.kt b/kotlin-native/performance/ring/src/main/kotlin/org/jetbrains/ring/DeltaBlueBenchmark.kt new file mode 100644 index 00000000000..38382899cf8 --- /dev/null +++ b/kotlin-native/performance/ring/src/main/kotlin/org/jetbrains/ring/DeltaBlueBenchmark.kt @@ -0,0 +1,797 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +// This benchmark is a port of the V8 JavaScript benchmark suite +// DeltaBlue benchmark: +// https://chromium.googlesource.com/external/v8/+/ba56077937e154aa0adbabd8abb9c24e53aae85d/benchmarks/deltablue.js + +// Copyright 2008 the V8 project authors. All rights reserved. +// Copyright 1996 John Maloney and Mario Wolczko. + +// This program is free software you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +// This implementation of the DeltaBlue benchmark is derived +// from the Smalltalk implementation by John Maloney and Mario +// Wolczko. Some parts have been translated directly, whereas +// others have been modified more aggresively to make it feel +// more like a JavaScript program. + +/** + * A JavaScript implementation of the DeltaBlue constraint-solving + * algorithm, as described in: + * + * "The DeltaBlue Algorithm: An Incremental Constraint Hierarchy Solver" + * Bjorn N. Freeman-Benson and John Maloney + * January 1990 Communications of the ACM, + * also available as University of Washington TR 89-08-06. + * + * Beware: this benchmark is written in a grotesque style where + * the constraint model is built by side-effects from constructors. + * I've kept it this way to avoid deviating too much from the original + * implementation. + */ + +fun alert(msg: String) { + throw Error(msg) +} + +/* --- O b j e c t M o d e l --- */ + +class OrderedCollection { + var elms = mutableListOf() + + fun add(elm: T) = elms.add(elm) + fun at(index: Int) = elms[index] + fun size() = elms.size + fun removeFirst() = elms.removeLast() + + fun remove(elm: T) { + var index = 0 + var skipped = 0 + for (i in 0 until elms.size) { + val value = elms[i] + if (value != elm) { + elms[index] = value + index++ + } else { + skipped++ + } + } + for (i in 0 until skipped) elms.removeLast() + } + + operator fun iterator() = elms.iterator() +} + +/* --- * + * S t r e n g t h + * --- */ + +/** + * Strengths are used to measure the relative importance of constraints. + * New strengths may be inserted in the strength hierarchy without + * disrupting current constraints. Strengths cannot be created outside + * this class, so pointer comparison can be used for value comparison. + */ +enum class Strength { + REQUIRED, + STRONG_PREFERRED, + PREFERRED, + STRONG_DEFAULT, + NORMAL, + WEAK_DEFAULT, + WEAKEST; + + val strengthValue get() = ordinal + + fun nextWeaker() = when (this) { + REQUIRED -> STRONG_PREFERRED + STRONG_PREFERRED -> PREFERRED + PREFERRED -> STRONG_DEFAULT + STRONG_DEFAULT -> NORMAL + NORMAL -> WEAK_DEFAULT + WEAK_DEFAULT -> WEAKEST + WEAKEST -> WEAKEST + } + + companion object { + fun stronger(s1: Strength, s2: Strength) = s1.strengthValue < s2.strengthValue + fun weaker(s1: Strength, s2: Strength) = s1.strengthValue > s2.strengthValue + fun weakestOf(s1: Strength, s2: Strength) = if (weaker(s1, s2)) s1 else s2 + } +} + +/* --- * + * C o n s t r a i n t + * --- */ + +/** + * An abstract class representing a system-maintainable relationship + * (or "constraint") between a set of variables. A constraint supplies + * a strength instance variable concrete subclasses provide a means + * of storing the constrained variables and other information required + * to represent a constraint. + */ +abstract class Constraint(val strength: Strength) { + abstract fun addToGraph() + abstract fun removeFromGraph() + abstract fun isSatisfied() : Boolean + abstract fun chooseMethod(mark: Int) + abstract fun markInputs(mark: Int) + abstract fun output(): Variable + abstract fun markUnsatisfied() + abstract fun recalculate() + abstract fun execute() + abstract fun inputsKnown(mark: Int): Boolean + + /** + * Attempt to find a way to enforce this constraint. If successful, + * record the solution, perhaps modifying the current dataflow + * graph. Answer the constraint that this constraint overrides, if + * there is one, or nil, if there isn't. + * Assume: I am not already satisfied. + */ + fun satisfy(mark: Int, planner: Planner): Constraint? { + chooseMethod(mark) + if (!isSatisfied()) { + if (strength == Strength.REQUIRED) alert("Could not satisfy a required constraint!") + return null + } + markInputs(mark) + val out = this.output() + val overridden = out.determinedBy + if (overridden != null) overridden.markUnsatisfied() + out.determinedBy = this + if (!planner.addPropagate(this, mark)) + alert("Cycle encountered") + out.mark = mark + return overridden + } + + fun destroyConstraint(planner: Planner) { + if (isSatisfied()) planner.incrementalRemove(this) + else removeFromGraph() + } + + /** + * Normal constraints are not input constraints. An input constraint + * is one that depends on external state, such as the mouse, the + * keybord, a clock, or some arbitraty piece of imperative code. + */ + open fun isInput() = false +} + +/* --- * + * U n a r y C o n s t r a i n t + * --- */ + +/** + * Abstract superclass for constraints having a single possible output + * variable. + */ +abstract class UnaryConstraint(val myOutput: Variable, strength: Strength) : Constraint(strength) { + var satisfied = false + + /** + * Adds this constraint to the constraint graph + */ + override fun addToGraph() { + myOutput.addConstraint(this) + satisfied = false + } + + /** + * Decides if this constraint can be satisfied and records that + * decision. + */ + override fun chooseMethod(mark: Int) { + satisfied = (myOutput.mark != mark) + && Strength.stronger(strength, myOutput.walkStrength) + } + + /** + * Returns true if this constraint is satisfied in the current solution. + */ + override fun isSatisfied() = satisfied + + override fun markInputs(mark: Int) { + // has no inputs + } + + /** + * Returns the current output variable. + */ + override fun output() = myOutput + + /** + * Calculate the walkabout strength, the stay flag, and, if it is + * 'stay', the value for the current output of this constraint. Assume + * this constraint is satisfied. + */ + override fun recalculate() { + myOutput.walkStrength = strength + myOutput.stay = !isInput() + if (myOutput.stay) execute() // Stay optimization + } + + /** + * Records that this constraint is unsatisfied + */ + override fun markUnsatisfied() { + this.satisfied = false + } + + override fun inputsKnown(mark: Int) = true + + override fun removeFromGraph() { +// if (myOutput != null) + myOutput.removeConstraint(this) + satisfied = false + } +} + +/* --- * + * S t a y C o n s t r a i n t + * --- */ + +/** + * Variables that should, with some level of preference, stay the same. + * Planners may exploit the fact that instances, if satisfied, will not + * change their output during plan execution. This is called "stay + * optimization". + */ +class StayConstraint(v: Variable, str: Strength) : UnaryConstraint(v, str) { + override fun execute() { + // Stay constraints do nothing + } +} + +/* --- * + * E d i t C o n s t r a i n t + * --- */ + +/** + * A unary input constraint used to mark a variable that the client + * wishes to change. + */ +class EditConstraint(v: Variable, str: Strength) : UnaryConstraint(v, str) { + /** + * Edits indicate that a variable is to be changed by imperative code. + */ + override fun isInput() = true + + override fun execute() { + // Edit constraints do nothing + } +} + +/* --- * + * B i n a r y C o n s t r a i n t + * --- */ + +enum class Direction { + BACKWARD, // = -1 + NONE, // = 0 + FORWARD // = 1 +} + +/** + * Abstract superclass for constraints having two possible output + * variables. + */ +abstract class BinaryConstraint(val v1: Variable, val v2: Variable, strength: Strength) : Constraint(strength) { + var direction = Direction.NONE + + /** + * Decides if this constraint can be satisfied and which way it + * should flow based on the relative strength of the variables related, + * and record that decision. + */ + override fun chooseMethod(mark: Int) { + if (v1.mark == mark) { + direction = if (v2.mark != mark && Strength.stronger(strength, v2.walkStrength)) + Direction.FORWARD else Direction.NONE + } + if (v2.mark == mark) { + direction = if (v1.mark != mark && Strength.stronger(strength, v1.walkStrength)) + Direction.BACKWARD else Direction.NONE + } + if (Strength.weaker(v1.walkStrength, v2.walkStrength)) { + direction = if (Strength.stronger(strength, v1.walkStrength)) + Direction.BACKWARD else Direction.NONE + } else { + direction = if (Strength.stronger(strength, v2.walkStrength)) + Direction.FORWARD else Direction.BACKWARD + } + } + + /** + * Add this constraint to the constraint graph + */ + override fun addToGraph() { + v1.addConstraint(this) + v2.addConstraint(this) + direction = Direction.NONE + } + + /** + * Answer true if this constraint is satisfied in the current solution. + */ + override fun isSatisfied() = direction != Direction.NONE + + /** + * Mark the input variable with the given mark. + */ + override fun markInputs(mark: Int) { + input().mark = mark + } + + /** + * Returns the current input variable + */ + fun input() = if (direction == Direction.FORWARD) v1 else v2 + + /** + * Returns the current output variable + */ + override fun output() = if (direction == Direction.FORWARD) v2 else v1 + + /** + * Calculate the walkabout strength, the stay flag, and, if it is + * 'stay', the value for the current output of this + * constraint. Assume this constraint is satisfied. + */ + override fun recalculate() { + val ihn = input() + val out = output() + out.walkStrength = Strength.weakestOf(this.strength, ihn.walkStrength) + out.stay = ihn.stay + if (out.stay) execute() + } + + /** + * Record the fact that this constraint is unsatisfied. + */ + override fun markUnsatisfied() { + direction = Direction.NONE + } + + override fun inputsKnown(mark: Int): Boolean { + val i = this.input() + return i.mark == mark || i.stay || i.determinedBy == null + } + + override fun removeFromGraph() { +// if (v1 != null) + v1.removeConstraint(this) +// if (v2 != null) + v2.removeConstraint(this) + this.direction = Direction.NONE + } +} + +/* --- * + * S c a l e C o n s t r a i n t + * --- */ + +/** + * Relates two variables by the linear scaling relationship: "v2 = + * (v1 * scale) + offset". Either v1 or v2 may be changed to maintain + * this relationship but the scale factor and offset are considered + * read-only. + */ +class ScaleConstraint(src: Variable, val scale: Variable, val offset: Variable, dest: Variable, strength: Strength): BinaryConstraint(src, dest, strength) { + /** + * Adds this constraint to the constraint graph. + */ + override fun addToGraph() { + super.addToGraph() + scale.addConstraint(this) + offset.addConstraint(this) + } + + override fun removeFromGraph() { + super.removeFromGraph() +// if (this.scale != null) + scale.removeConstraint(this) +// if (this.offset != null) + offset.removeConstraint(this) + } + + override fun markInputs(mark: Int) { + super.markInputs(mark) + scale.mark = mark + offset.mark = mark + } + + /** + * Enforce this constraint. Assume that it is satisfied. + */ + override fun execute() { + if (direction == Direction.FORWARD) { + v2.value = v1.value * scale.value + offset.value + } else { + v1.value = (v2.value - offset.value) / scale.value + } + } + + /** + * Calculate the walkabout strength, the stay flag, and, if it is + * 'stay', the value for the current output of this constraint. Assume + * this constraint is satisfied. + */ + override fun recalculate() { + val ihn = input() + val out = output() + out.walkStrength = Strength.weakestOf(strength, ihn.walkStrength) + out.stay = ihn.stay && scale.stay && offset.stay + if (out.stay) execute() + } +} + +/* --- * + * E q u a l i t y C o n s t r a i n t + * --- */ + +/** + * Constrains two variables to have the same value. + */ +class EqualityConstraint(var1: Variable, var2: Variable, strength: Strength): BinaryConstraint(var1, var2, strength) { + /** + * Enforce this constraint. Assume that it is satisfied. + */ + override fun execute() { + output().value = input().value + } +} + +/* --- * + * V a r i a b l e + * --- */ + +/** + * A constrained variable. In addition to its value, it maintain the + * structure of the constraint graph, the current dataflow graph, and + * various parameters of interest to the DeltaBlue incremental + * constraint solver. + **/ +class Variable(val name: String, var value : Int = 0) { + val constraints = OrderedCollection() + var determinedBy: Constraint? = null + var mark = 0 + var walkStrength = Strength.WEAKEST + var stay = true + + /** + * Add the given constraint to the set of all constraints that refer + * this variable. + */ + fun addConstraint(c: Constraint) = constraints.add(c) + + /** + * Removes all traces of c from this variable. + */ + fun removeConstraint(c: Constraint) { + constraints.remove(c) + if (determinedBy == c) determinedBy = null + } +} + +/* --- * + * P l a n n e r + * --- */ + +/** + * The DeltaBlue planner + */ +class Planner { + var currentMark = 0 + + /** + * Activate the constraint and attempt to satisfy it. + */ + fun add(c: Constraint) { + c.addToGraph() + incrementalAdd(c) + } + + /** + * Attempt to satisfy the given constraint and, if successful, + * incrementally update the dataflow graph. Details: If satifying + * the constraint is successful, it may override a weaker constraint + * on its output. The algorithm attempts to resatisfy that + * constraint using some other method. This process is repeated + * until either a) it reaches a variable that was not previously + * determined by any constraint or b) it reaches a constraint that + * is too weak to be satisfied using any of its methods. The + * variables of constraints that have been processed are marked with + * a unique mark value so that we know where we've been. This allows + * the algorithm to avoid getting into an infinite loop even if the + * constraint graph has an inadvertent cycle. + */ + fun incrementalAdd(c: Constraint) { + val mark = newMark() + var overridden = c.satisfy(mark, this) + while (overridden != null) + overridden = overridden.satisfy(mark, this) + } + + /** + * Entry point for retracting a constraint. Remove the given + * constraint and incrementally update the dataflow graph. + * Details: Retracting the given constraint may allow some currently + * unsatisfiable downstream constraint to be satisfied. We therefore collect + * a list of unsatisfied downstream constraints and attempt to + * satisfy each one in turn. This list is traversed by constraint + * strength, strongest first, as a heuristic for avoiding + * unnecessarily adding and then overriding weak constraints. + * Assume: c is satisfied. + */ + fun incrementalRemove(c: Constraint) { + val out = c.output() + c.markUnsatisfied() + c.removeFromGraph() + var unsatisfied = removePropagateFrom(out) + var strength = Strength.REQUIRED + do { + for (u in unsatisfied) { + if (u.strength == strength) + this.incrementalAdd(u) + } + strength = strength.nextWeaker() + } while (strength != Strength.WEAKEST) + } + + /** + * Select a previously unused mark value. + */ + fun newMark() = ++currentMark + + /** + * Extract a plan for resatisfaction starting from the given source + * constraints, usually a set of input constraints. This method + * assumes that stay optimization is desired the plan will contain + * only constraints whose output variables are not stay. Constraints + * that do no computation, such as stay and edit constraints, are + * not included in the plan. + * Details: The outputs of a constraint are marked when it is added + * to the plan under construction. A constraint may be appended to + * the plan when all its input variables are known. A variable is + * known if either a) the variable is marked (indicating that has + * been computed by a constraint appearing earlier in the plan), b) + * the variable is 'stay' (i.e. it is a constant at plan execution + * time), or c) the variable is not determined by any + * constraint. The last provision is for past states of history + * variables, which are not stay but which are also not computed by + * any constraint. + * Assume: sources are all satisfied. + */ + fun makePlan(sources: OrderedCollection): Plan { + var mark = this.newMark() + var plan = Plan() + var todo = sources + while (todo.size() > 0) { + var c = todo.removeFirst() + if (c.output().mark != mark && c.inputsKnown(mark)) { + plan.addConstraint(c) + c.output().mark = mark + addConstraintsConsumingTo(c.output(), todo) + } + } + return plan + } + + /** + * Extract a plan for resatisfying starting from the output of the + * given constraints, usually a set of input constraints. + */ + fun extractPlanFromConstraints(constraints: OrderedCollection): Plan { + val sources = OrderedCollection() + for (c in constraints) { + if (c.isInput() && c.isSatisfied()) + // not in plan already and eligible for inclusion + sources.add(c) + } + return makePlan(sources) + } + + /** + * Recompute the walkabout strengths and stay flags of all variables + * downstream of the given constraint and recompute the actual + * values of all variables whose stay flag is true. If a cycle is + * detected, remove the given constraint and answer + * false. Otherwise, answer true. + * Details: Cycles are detected when a marked variable is + * encountered downstream of the given constraint. The sender is + * assumed to have marked the inputs of the given constraint with + * the given mark. Thus, encountering a marked node downstream of + * the output constraint means that there is a path from the + * constraint's output to one of its inputs. + */ + fun addPropagate(c: Constraint, mark: Int): Boolean { + val todo = OrderedCollection() + todo.add(c) + while (todo.size() > 0) { + var d = todo.removeFirst() + if (d.output().mark == mark) { + incrementalRemove(c) + return false + } + d.recalculate() + addConstraintsConsumingTo(d.output(), todo) + } + return true + } + + /** + * Update the walkabout strengths and stay flags of all variables + * downstream of the given constraint. Answer a collection of + * unsatisfied constraints sorted in order of decreasing strength. + */ + fun removePropagateFrom(out: Variable): OrderedCollection { + out.determinedBy = null + out.walkStrength = Strength.WEAKEST + out.stay = true + val unsatisfied = OrderedCollection() + val todo = OrderedCollection() + todo.add(out) + while (todo.size() > 0) { + var v = todo.removeFirst() + for (c in v.constraints) { + if (!c.isSatisfied()) + unsatisfied.add(c) + } + var determining = v.determinedBy + for (next in v.constraints) { + if (next != determining && next.isSatisfied()) { + next.recalculate() + todo.add(next.output()) + } + } + } + return unsatisfied + } + + fun addConstraintsConsumingTo(v: Variable, coll: OrderedCollection) { + var determining = v.determinedBy + for (c in v.constraints) { + if (c != determining && c.isSatisfied()) + coll.add(c) + } + } + + fun change(v: Variable, newValue: Int) { + val edit = EditConstraint(v, Strength.PREFERRED) + add(edit) + val edits = OrderedCollection() + edits.add(edit) + val plan = extractPlanFromConstraints(edits) + for (i in 0 until 10) { + v.value = newValue + plan.execute() + } + edit.destroyConstraint(this) + } +} + +/* --- * + * P l a n + * --- */ + +/** + * A Plan is an ordered list of constraints to be executed in sequence + * to resatisfy all currently satisfiable constraints in the face of + * one or more changing inputs. + */ +class Plan { + val v = OrderedCollection() + + fun addConstraint(c: Constraint) = v.add(c) + fun size() = v.size() + fun constraintAt(index: Int) = v.at(index) + + fun execute() { + for (c in v) { + c.execute() + } + } +} + +/* --- * + * M a i n + * --- */ + +class DeltaBlueBenchmark { + fun deltaBlue() { + chainTest(100) + projectionTest(100) + } + /** + * 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. + */ + fun chainTest(n: Int) { + val planner = Planner() + val variables = (0..n).map{ Variable("v$it") }.toList() + var first = variables.first() + var last = variables.last() + // Build chain of n equality constraints + variables.windowed(2) { + (v1, v2) -> planner.add(EqualityConstraint(v1, v2, Strength.REQUIRED)) + } + + planner.add(StayConstraint(last, Strength.STRONG_DEFAULT)) + val edit = EditConstraint(first, Strength.PREFERRED) + planner.add(edit) + val edits = OrderedCollection() + edits.add(edit) + val plan = planner.extractPlanFromConstraints(edits) + for (i in 0 until 100) { + first.value = i + plan.execute() + if (last.value != i) + alert("Chain test failed.") + } + } + + /** + * 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. + */ + fun projectionTest(n: Int) { + val planner = Planner() + var scale = Variable("scale", 10) + var offset = Variable("offset", 1000) + var src: Variable? = null + var dst: Variable? = null + + var dests = OrderedCollection() + for (i in 0 until n) { + src = Variable("src$i", i) + dst = Variable("dst$i", i) + dests.add(dst) + planner.add(StayConstraint(src, Strength.NORMAL)) + planner.add(ScaleConstraint(src, scale, offset, dst, Strength.REQUIRED)) + } + + planner.change(src!!, 17) + if (dst!!.value != 1170) alert("Projection 1 failed") + planner.change(dst, 1050) + if (src.value != 5) alert("Projection 2 failed") + planner.change(scale, 5) + for (i in 0 until n - 1) { + if (dests.at(i).value != i * 5 + 1000) + alert("Projection 3 failed") + } + planner.change(offset, 2000) + for (i in 0 until n - 1) { + if (dests.at(i).value != i * 5 + 2000) + alert("Projection 4 failed") + } + } +} diff --git a/kotlin-native/performance/ring/src/main/kotlin/org/jetbrains/ring/RichardsBenchmark.kt b/kotlin-native/performance/ring/src/main/kotlin/org/jetbrains/ring/RichardsBenchmark.kt new file mode 100644 index 00000000000..f8e921393db --- /dev/null +++ b/kotlin-native/performance/ring/src/main/kotlin/org/jetbrains/ring/RichardsBenchmark.kt @@ -0,0 +1,527 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +// This benchmark is a port of the V8 JavaScript benchmark suite +// richards benchmark: +// https://chromium.googlesource.com/external/v8/+/ba56077937e154aa0adbabd8abb9c24e53aae85d/benchmarks/richards.js + +// Copyright 2006-2008 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES LOSS OF USE, +// DATA, OR PROFITS OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/** + * The Richards benchmark simulates the task dispatcher of an + * operating system. + **/ +class RichardsBenchmark { + fun runRichards() { + val scheduler = Scheduler() + scheduler.addIdleTask(ID_IDLE, 0, null, COUNT) + + var queue = Packet(null, ID_WORKER, KIND_WORK) + queue = Packet(queue, ID_WORKER, KIND_WORK) + scheduler.addWorkerTask(ID_WORKER, 1000, queue) + + queue = Packet(null, ID_DEVICE_A, KIND_DEVICE) + queue = Packet(queue, ID_DEVICE_A, KIND_DEVICE) + queue = Packet(queue, ID_DEVICE_A, KIND_DEVICE) + scheduler.addHandlerTask(ID_HANDLER_A, 2000, queue) + + queue = Packet(null, ID_DEVICE_B, KIND_DEVICE) + queue = Packet(queue, ID_DEVICE_B, KIND_DEVICE) + queue = Packet(queue, ID_DEVICE_B, KIND_DEVICE) + scheduler.addHandlerTask(ID_HANDLER_B, 3000, queue) + + scheduler.addDeviceTask(ID_DEVICE_A, 4000, null) + + scheduler.addDeviceTask(ID_DEVICE_B, 5000, null) + + scheduler.schedule() + + if (scheduler.queueCount != EXPECTED_QUEUE_COUNT || + scheduler.holdCount != EXPECTED_HOLD_COUNT) { + val msg = + "Error during execution: queueCount = " + scheduler.queueCount + + ", holdCount = " + scheduler.holdCount + "." + throw Error(msg) + } + } +} + +var COUNT = 1000 + +/** + * These two constants specify how many times a packet is queued and + * how many times a task is put on hold in a correct run of richards. + * They don't have any meaning a such but are characteristic of a + * correct run so if the actual queue or hold count is different from + * the expected there must be a bug in the implementation. + **/ +var EXPECTED_QUEUE_COUNT = 2322 +var EXPECTED_HOLD_COUNT = 928 + + +/** + * A scheduler can be used to schedule a set of tasks based on their relative + * priorities. Scheduling is done by maintaining a list of task control blocks + * which holds tasks and the data queue they are processing. + * @constructor + */ +class Scheduler { + var queueCount = 0 + var holdCount = 0 + var blocks = Array(NUMBER_OF_IDS) { null } + var list: TaskControlBlock? = null + var currentTcb: TaskControlBlock? = null + var currentId = 0 + + /** + * Add an idle task to this scheduler. + * @param {int} id the identity of the task + * @param {int} priority the task's priority + * @param {Packet} queue the queue of work to be processed by the task + * @param {int} count the number of times to schedule the task + */ + fun addIdleTask(id: Int, priority: Int, queue: Packet?, count: Int) { + this.addRunningTask(id, priority, queue, IdleTask(this, 1, count)) + } + + /** + * Add a work task to this scheduler. + * @param {int} id the identity of the task + * @param {int} priority the task's priority + * @param {Packet} queue the queue of work to be processed by the task + */ + fun addWorkerTask(id: Int, priority: Int, queue: Packet?) { + this.addTask(id, priority, queue, WorkerTask(this, ID_HANDLER_A, 0)) + } + + /** + * Add a handler task to this scheduler. + * @param {int} id the identity of the task + * @param {int} priority the task's priority + * @param {Packet} queue the queue of work to be processed by the task + */ + fun addHandlerTask(id: Int, priority: Int, queue: Packet?) { + this.addTask(id, priority, queue, HandlerTask(this)) + } + + /** + * Add a handler task to this scheduler. + * @param {int} id the identity of the task + * @param {int} priority the task's priority + * @param {Packet} queue the queue of work to be processed by the task + */ + fun addDeviceTask(id: Int, priority: Int, queue: Packet?) { + this.addTask(id, priority, queue, DeviceTask(this)) + } + + /** + * Add the specified task and mark it as running. + * @param {int} id the identity of the task + * @param {int} priority the task's priority + * @param {Packet} queue the queue of work to be processed by the task + * @param {Task} task the task to add + */ + fun addRunningTask(id: Int, priority: Int, queue: Packet?, task: Task) { + this.addTask(id, priority, queue, task) + this.currentTcb!!.setRunning() + } + + /** + * Add the specified task to this scheduler. + * @param {int} id the identity of the task + * @param {int} priority the task's priority + * @param {Packet} queue the queue of work to be processed by the task + * @param {Task} task the task to add + */ + fun addTask(id: Int, priority: Int, queue: Packet?, task: Task) { + this.currentTcb = TaskControlBlock(this.list, id, priority, queue, task) + this.list = this.currentTcb + this.blocks[id] = this.currentTcb + } + + /** + * Execute the tasks managed by this scheduler. + */ + fun schedule() { + this.currentTcb = this.list + while (this.currentTcb != null) { + if (this.currentTcb!!.isHeldOrSuspended()) { + this.currentTcb = this.currentTcb!!.link + } else { + this.currentId = this.currentTcb!!.id + this.currentTcb = this.currentTcb!!.run() + } + } + } + + /** + * Release a task that is currently blocked and return the next block to run. + * @param {int} id the id of the task to suspend + */ + fun release(id: Int): TaskControlBlock? { + val tcb = this.blocks[id] + if (tcb == null) return tcb + tcb.markAsNotHeld() + if (tcb.priority > this.currentTcb!!.priority) { + return tcb + } else { + return this.currentTcb + } + } + + /** + * Block the currently executing task and return the next task control block + * to run. The blocked task will not be made runnable until it is explicitly + * released, even if new work is added to it. + */ + fun holdCurrent(): TaskControlBlock? { + this.holdCount++ + this.currentTcb!!.markAsHeld() + return this.currentTcb!!.link + } + + /** + * Suspend the currently executing task and return the next task control block + * to run. If new work is added to the suspended task it will be made runnable. + */ + fun suspendCurrent(): TaskControlBlock? { + this.currentTcb!!.markAsSuspended() + return this.currentTcb + } + + /** + * Add the specified packet to the end of the work list used by the task + * associated with the packet and make the task runnable if it is currently + * suspended. + * @param {Packet} packet the packet to add + */ + fun queue(packet: Packet): TaskControlBlock? { + val t = this.blocks[packet.id] + if (t == null) return t + this.queueCount++ + packet.link = null + packet.id = this.currentId + return t.checkPriorityAdd(this.currentTcb!!, packet) + } +} + +var ID_IDLE = 0 +var ID_WORKER = 1 +var ID_HANDLER_A = 2 +var ID_HANDLER_B = 3 +var ID_DEVICE_A = 4 +var ID_DEVICE_B = 5 +var NUMBER_OF_IDS = 6 + +var KIND_DEVICE = 0 +var KIND_WORK = 1 + +/** + * A task control block manages a task and the queue of work packages associated + * with it. + * @param {TaskControlBlock} link the preceding block in the linked block list + * @param {int} id the id of this block + * @param {int} priority the priority of this block + * @param {Packet} queue the queue of packages to be processed by the task + * @param {Task} task the task + * @constructor + */ +class TaskControlBlock(var link: TaskControlBlock?, var id: Int, var priority: Int, var queue: Packet?, var task: Task) { + var state = 0 + init { + if (queue == null) { + this.state = STATE_SUSPENDED + } else { + this.state = STATE_SUSPENDED_RUNNABLE + } + } + + fun setRunning() { + this.state = STATE_RUNNING + } + + fun markAsNotHeld() { + this.state = this.state and STATE_NOT_HELD + } + + fun markAsHeld() { + this.state = this.state or STATE_HELD + } + + fun isHeldOrSuspended(): Boolean { + return (this.state and STATE_HELD) != 0 || (this.state == STATE_SUSPENDED) + } + + fun markAsSuspended() { + this.state = this.state or STATE_SUSPENDED + } + + fun markAsRunnable() { + this.state = this.state or STATE_RUNNABLE + } + + /** + * Runs this task, if it is ready to be run, and returns the next task to run. + */ + fun run(): TaskControlBlock? { + val packet: Packet? + if (this.state == STATE_SUSPENDED_RUNNABLE) { + packet = this.queue + this.queue = packet?.link + if (this.queue == null) { + this.state = STATE_RUNNING + } else { + this.state = STATE_RUNNABLE + } + } else { + packet = null + } + return this.task.run(packet) + } + + /** + * Adds a packet to the work list of this block's task, marks this as runnable if + * necessary, and returns the next runnable object to run (the one + * with the highest priority). + */ + fun checkPriorityAdd(task: TaskControlBlock, packet: Packet): TaskControlBlock { + if (this.queue == null) { + this.queue = packet + this.markAsRunnable() + if (this.priority > task.priority) return this + } else { + this.queue = packet.addTo(this.queue) + } + return task + } + + override fun toString(): String { + return "tcb { " + this.task + "@" + this.state + " }" + } + +} + +/** + * The task is running and is currently scheduled. + */ +var STATE_RUNNING = 0 + +/** + * The task has packets left to process. + */ +var STATE_RUNNABLE = 1 + +/** + * The task is not currently running. The task is not blocked as such and may + * be started by the scheduler. + */ +var STATE_SUSPENDED = 2 + +/** + * The task is blocked and cannot be run until it is explicitly released. + */ +var STATE_HELD = 4 + +var STATE_SUSPENDED_RUNNABLE = STATE_SUSPENDED or STATE_RUNNABLE +var STATE_NOT_HELD = STATE_HELD.inv() + +interface Task { + fun run(packet: Packet?): TaskControlBlock? +} + +/** + * An idle task doesn't do any work itself but cycles control between the two + * device tasks. + * @param {Scheduler} scheduler the scheduler that manages this task + * @param {int} v1 a seed value that controls how the device tasks are scheduled + * @param {int} count the number of times this task should be scheduled + * @constructor + */ +class IdleTask(var scheduler: Scheduler, var v1: Int, var count: Int): Task { + override fun run(packet: Packet?): TaskControlBlock? { + this.count-- + if (this.count == 0) return this.scheduler.holdCurrent() + if ((this.v1 and 1) == 0) { + this.v1 = this.v1 shr 1 + return this.scheduler.release(ID_DEVICE_A) + } else { + this.v1 = (this.v1 shr 1) xor 0xD008 + return this.scheduler.release(ID_DEVICE_B) + } + } + + override fun toString(): String { + return "IdleTask" + } +} + +/** + * A task that suspends itself after each time it has been run to simulate + * waiting for data from an external device. + * @param {Scheduler} scheduler the scheduler that manages this task + * @constructor + */ +class DeviceTask(var scheduler: Scheduler): Task { + var v1: Packet? = null + + override fun run(packet: Packet?): TaskControlBlock? { + if (packet == null) { + if (this.v1 == null) return this.scheduler.suspendCurrent() + val v = this.v1 + this.v1 = null + return this.scheduler.queue(v!!) + } else { + this.v1 = packet + return this.scheduler.holdCurrent() + } + } + + override fun toString(): String { + return "DeviceTask" + } +} + +/** + * A task that manipulates work packets. + * @param {Scheduler} scheduler the scheduler that manages this task + * @param {int} v1 a seed used to specify how work packets are manipulated + * @param {int} v2 another seed used to specify how work packets are manipulated + * @constructor + */ +class WorkerTask(var scheduler: Scheduler, var v1: Int, var v2: Int): Task { + override fun run(packet: Packet?): TaskControlBlock? { + if (packet == null) { + return this.scheduler.suspendCurrent() + } else { + if (this.v1 == ID_HANDLER_A) { + this.v1 = ID_HANDLER_B + } else { + this.v1 = ID_HANDLER_A + } + packet.id = this.v1 + packet.a1 = 0 + for (i in 0 until DATA_SIZE) { + this.v2++ + if (this.v2 > 26) this.v2 = 1 + packet.a2[i] = this.v2 + } + return this.scheduler.queue(packet) + } + } + + override fun toString(): String { + return "WorkerTask" + } +} + +/** + * A task that manipulates work packets and then suspends itself. + * @param {Scheduler} scheduler the scheduler that manages this task + * @constructor + */ +class HandlerTask(var scheduler: Scheduler): Task { + var v1: Packet? = null + var v2: Packet? = null + + override fun run(packet: Packet?): TaskControlBlock? { + if (packet != null) { + if (packet.kind == KIND_WORK) { + this.v1 = packet.addTo(this.v1) + } else { + this.v2 = packet.addTo(this.v2) + } + } + this.v1?.let { v1 -> + val count = this.v1!!.a1 + if (count < DATA_SIZE) { + this.v2?.let { v2 -> + val v = v2 + this.v2 = v2.link + v.a1 = v1.a2[count] + v1.a1 = count + 1 + return this.scheduler.queue(v) + } + } else { + val v = v1 + this.v1 = v1.link + return this.scheduler.queue(v) + } + } + return this.scheduler.suspendCurrent() + } + + override fun toString(): String { + return "HandlerTask" + } +} + +/* --- * + * P a c k e t + * --- */ + +var DATA_SIZE = 4 + +/** + * A simple package of data that is manipulated by the tasks. The exact layout + * of the payload data carried by a packet is not important, and neither is the + * nature of the work performed on packets by the tasks. + * + * Besides carrying data, packets form linked lists and are hence used both as + * data and work lists. + * @param {Packet} link the tail of the linked list of packets + * @param {int} id an ID for this packet + * @param {int} kind the type of this packet + * @constructor + */ +class Packet(var link: Packet?, var id: Int, var kind: Int) { + var a1: Int = 0 + var a2 = IntArray(DATA_SIZE) + + /** + * Add this packet to the end of a work list, and return the work list. + * @param {Packet} queue the work list to add this packet to + */ + fun addTo(queue: Packet?): Packet { + this.link = null + if (queue == null) return this + var next: Packet = queue + var peek = next.link + while (peek != null) { + next = peek + peek = next.link + } + next.link = this + return queue + } + + override fun toString(): String { + return "Packet" + } +}