From ddb140cc5142185acfc28b861b31a4422bf0e450 Mon Sep 17 00:00:00 2001 From: Mads Ager Date: Wed, 25 May 2022 14:10:58 +0200 Subject: [PATCH] [K/N] Add splay benchmark based on the V8 Benchmark Suite. The benchmark constructs a splay tree, measures the time it takes to performs a number of insertions and removals, and after measurements validates that the splay tree is valid. --- .../performance/ring/src/main/kotlin/main.kt | 1 + .../org/jetbrains/ring/SplayBenchmark.kt | 303 ++++++++++++++++++ .../BenchmarksCollection.kt | 12 +- .../jetbrains/benchmarksLauncher/launcher.kt | 6 +- 4 files changed, 320 insertions(+), 2 deletions(-) create mode 100644 kotlin-native/performance/ring/src/main/kotlin/org/jetbrains/ring/SplayBenchmark.kt diff --git a/kotlin-native/performance/ring/src/main/kotlin/main.kt b/kotlin-native/performance/ring/src/main/kotlin/main.kt index 74a0a1dd0ff..5b95e19e53b 100644 --- a/kotlin-native/performance/ring/src/main/kotlin/main.kt +++ b/kotlin-native/performance/ring/src/main/kotlin/main.kt @@ -58,6 +58,7 @@ class RingLauncher : Launcher() { "PrimeList.calcDirect" to BenchmarkEntryWithInit.create(::PrimeListBenchmark, { calcDirect() }), "PrimeList.calcEratosthenes" to BenchmarkEntryWithInit.create(::PrimeListBenchmark, { calcEratosthenes() }), "Singleton.access" to BenchmarkEntryWithInit.create(::SingletonBenchmark, { access() }), + "Splay" to BenchmarkEntryWithInitAndValidation.create(::SplayBenchmark, { runSplay() }, { splayTearDown() }), "String.stringConcat" to BenchmarkEntryWithInit.create(::StringBenchmark, { stringConcat() }), "String.stringBuilderConcat" to BenchmarkEntryWithInit.create(::StringBenchmark, { stringBuilderConcat() }), "String.stringBuilderConcatNullable" to BenchmarkEntryWithInit.create(::StringBenchmark, { stringBuilderConcatNullable() }), diff --git a/kotlin-native/performance/ring/src/main/kotlin/org/jetbrains/ring/SplayBenchmark.kt b/kotlin-native/performance/ring/src/main/kotlin/org/jetbrains/ring/SplayBenchmark.kt new file mode 100644 index 00000000000..8c3ba6d885f --- /dev/null +++ b/kotlin-native/performance/ring/src/main/kotlin/org/jetbrains/ring/SplayBenchmark.kt @@ -0,0 +1,303 @@ +/* + * 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 +// splay benchmark: +// https://chromium.googlesource.com/external/v8/+/ba56077937e154aa0adbabd8abb9c24e53aae85d/benchmarks/splay.js + +// Copyright 2009 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. +// This benchmark is based on a JavaScript log processing module used +// by the V8 profiler to generate execution time profiles for runs of +// JavaScript applications, and it effectively measures how fast the +// JavaScript engine is at allocating nodes and reclaiming the memory +// used for old nodes. Because of the way splay trees work, the engine +// also has to deal with a lot of changes to the large tree object +// graph. + +import kotlin.random.Random + +// A splay tree is a self-balancing binary search tree with the additional +// property that recently accessed elements are quick to access again. +// It performs basic operations such as insertion, look-up and removal in +// O(log(n)) amortized time. +class SplayTree, V> { + + // Nodes of the splay tree. + class Node, V>(val key: K, val value: V) { + var left: Node? = null + var right: Node? = null + + // Performs an ordered traversal of the subtree starting at this SplayTree.Node. + fun traverse(f: (Node) -> Unit) { + var current: Node? = this + while (current != null) { + current.left?.traverse(f) + f(current) + current = current.right + } + } + } + + // Root of the splay tree. + private var root: Node? = null + + // Return whether the splay tree is empty. + fun isEmpty() = root == null + + // Inserts a node into the tree with the specified key and value if + // the tree does not already contain a node with the specified key. If + // the value is inserted, it becomes the root of the tree. + fun insert(key: K, value: V) { + if (isEmpty()) { + root = Node(key, value) + return + } + // Splay on the key to move the last node on the search path for + // the key to the root of the tree. + splay(key) + val r = root!! + if (r.key == key) { + return + } + val node = Node(key, value) + if (key > r.key) { + node.left = r + node.right = r.right + r.right = null + } else { + node.right = r + node.left = r.left + r.left = null + } + root = node + } + + // Removes a node with the specified key from the tree if the tree + // contains a node with this key. The removed node is returned. If the + // key is not found, an exception is thrown. + fun remove(key: K): Node { + if (this.isEmpty()) { + throw Exception("Key not found: $key") + } + splay(key) + val r = root!! + if (r.key != key) { + throw Exception("Key not found: $key") + } + val removed = r + if (r.left == null) { + root = r.right + } else { + val right = r.right + root = r.left + // Splay to make sure that the new root has an empty right child. + splay(key) + // Insert the original right child as the right child of the new root. + root!!.right = right + } + return removed + } + + // Returns the node having the specified key or null if the tree doesn't contain + // a node with the specified key. + fun find(key: K): Node? { + if (isEmpty()) return null + splay(key) + return if (root!!.key == key) root else null + } + + // Returns node having the maximum key value. + fun findMax(startNode: Node? = null): Node? { + if (isEmpty()) return null + var current = startNode ?: root!! + while (current.right != null) { + current = current.right!! + } + return current + } + + // Returns node having the maximum key value that is less than the + // specified key value. + fun findGreatestLessThan(key: K): Node? { + if (isEmpty()) return null + // Splay on the key to move the node with the given key or the last + // node on the search path to the top of the tree. + splay(key) + // Now the result is either the root node or the greatest node in the + // left subtree. + val r = root!! + return if (r.key < key) { + root + } else if (r.left != null) { + findMax(r.left) + } else { + null + } + } + + // Returns a list containing all the keys in the tree's nodes. + fun exportKeys(): List { + val result = mutableListOf() + if (!isEmpty()) { + root?.traverse { result.add(it.key) } + } + return result + } + + // Perform the splay operation for the given key. Moves the node with + // the given key to the top of the tree. If no node has the given + // key, the last node on the search path is moved to the top of the + // tree. This is the simplified top-down splaying algorithm from: + // "Self-adjusting Binary Search Trees" by Sleator and Tarjan + private fun splay(key: K) { + if (isEmpty()) return + // Create a dummy node. The use of the dummy node is a bit + // counter-intuitive: The right child of the dummy node will hold + // the L tree of the algorithm. The left child of the dummy node + // will hold the R tree of the algorithm. Using a dummy node, left + // and right will always be nodes and we avoid special cases. + // The key and value for the dummy node will not be used, so we just + // use the key and value of the root node for the dummy. + val dummy = Node(root!!.key, root!!.value) + var left = dummy + var right = dummy + + var current = root!! + while (true) { + if (key < current.key) { + if (current.left == null) { + break + } + if (key < current.left!!.key) { + // Rotate right + val tmp = current.left!! + current.left = tmp.right + tmp.right = current + current = tmp + if (current.left == null) { + break + } + } + // Link right. + right.left = current + right = current + current = current.left!! + } else if (key > current.key) { + if (current.right == null) { + break + } + if (key > current.right!!.key) { + // Rotate left. + val tmp = current.right!! + current.right = tmp.left + tmp.left = current + current = tmp + if (current.right == null) { + break + } + } + // Link left. + left.right = current + left = current + current = current.right!! + } else { + break + } + } + // Assemble. + left.right = current.left + right.left = current.right + current.left = dummy.right + current.right = dummy.left + root = current + } +} + +class SplayBenchmark { + // Seed random number generator for deterministic "random" number generation. + val random = Random(20) + val splayTreeSize = 8000; + // Different from the original as the surrounding runner is different. We want + // enough modifications that GCs will take place and will matter. + val splayTreeModifications = 8000; + val splayTreePayloadDepth = 5; + val splayTree = splaySetup() + + fun generateKey(): Int = random.nextInt() + + fun generatePayloadTree(depth: Int, tag: String): Pair { + return if (depth == 0) { + Pair(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), "String for key $tag in leaf node") + } else { + Pair(generatePayloadTree(depth - 1, tag), generatePayloadTree(depth - 1, tag)) + } + } + + fun insertNewNode(tree: SplayTree>, payloadDepth: Int): Int { + var key = generateKey() + while (tree.find(key) != null) { + key = generateKey() + } + var payload = generatePayloadTree(payloadDepth, "$key") + tree.insert(key, payload) + return key + } + + fun splaySetup(): SplayTree> { + val result = SplayTree>() + for (i in 0 until splayTreeSize) insertNewNode(result, splayTreePayloadDepth) + return result + } + + fun splayTearDown() { + val keys = splayTree.exportKeys() + val length = keys.size + if (length != splayTreeSize) { + throw Exception("Splay tree has wrong size") + } + for (i in 0 until length - 1) { + if (keys[i] >= keys[i + 1]) { + throw Exception("Splay tree not sorted") + } + } + } + + fun runSplay() { + for (i in 0 until splayTreeModifications) { + val key = insertNewNode(splayTree, splayTreePayloadDepth) + val greatest = splayTree.findGreatestLessThan(key) + if (greatest == null) { + splayTree.remove(key) + } else { + splayTree.remove(greatest.key) + } + } + } +} diff --git a/kotlin-native/performance/shared/src/main/kotlin/org/jetbrains/benchmarksLauncher/BenchmarksCollection.kt b/kotlin-native/performance/shared/src/main/kotlin/org/jetbrains/benchmarksLauncher/BenchmarksCollection.kt index 71045bccdc3..2368b7e2401 100644 --- a/kotlin-native/performance/shared/src/main/kotlin/org/jetbrains/benchmarksLauncher/BenchmarksCollection.kt +++ b/kotlin-native/performance/shared/src/main/kotlin/org/jetbrains/benchmarksLauncher/BenchmarksCollection.kt @@ -22,7 +22,7 @@ interface AbstractBenchmarkEntry { open val useAutoEvaluatedNumberOfMeasure: Boolean } -class BenchmarkEntryWithInit(val ctor: ()->Any, val lambda: (Any) -> Any?): AbstractBenchmarkEntry { +open class BenchmarkEntryWithInit(val ctor: ()->Any, val lambda: (Any) -> Any?): AbstractBenchmarkEntry { companion object { inline fun create(noinline ctor: ()->T, crossinline lambda: T.() -> Any?) = BenchmarkEntryWithInit(ctor) { (it as T).lambda() } } @@ -30,6 +30,16 @@ class BenchmarkEntryWithInit(val ctor: ()->Any, val lambda: (Any) -> Any?): Abst override val useAutoEvaluatedNumberOfMeasure: Boolean = true } +class BenchmarkEntryWithInitAndValidation(ctor: () -> Any, benchmark: (Any) -> Any?, val validation: (Any) -> Any?) + : BenchmarkEntryWithInit(ctor, benchmark) { + companion object { + inline fun create(noinline ctor: ()->T, crossinline benchmark: T.() -> Any?, crossinline validation: T.() -> Any?) + = BenchmarkEntryWithInitAndValidation(ctor, { (it as T).benchmark() }, { (it as T).validation() }) + } + + override val useAutoEvaluatedNumberOfMeasure: Boolean = true +} + open class BenchmarkEntry(val lambda: () -> Any?) : AbstractBenchmarkEntry { override val useAutoEvaluatedNumberOfMeasure: Boolean = true } diff --git a/kotlin-native/performance/shared/src/main/kotlin/org/jetbrains/benchmarksLauncher/launcher.kt b/kotlin-native/performance/shared/src/main/kotlin/org/jetbrains/benchmarksLauncher/launcher.kt index e538970f1db..12ec4087333 100644 --- a/kotlin-native/performance/shared/src/main/kotlin/org/jetbrains/benchmarksLauncher/launcher.kt +++ b/kotlin-native/performance/shared/src/main/kotlin/org/jetbrains/benchmarksLauncher/launcher.kt @@ -46,10 +46,14 @@ abstract class Launcher { var i = repeatNumber return if (benchmark is BenchmarkEntryWithInit) { cleanup() - measureNanoTime { + val result = measureNanoTime { while (i-- > 0) benchmark.lambda(benchmarkInstance!!) cleanup() } + if (benchmark is BenchmarkEntryWithInitAndValidation) { + benchmark.validation(benchmarkInstance!!) + } + result } else if (benchmark is BenchmarkEntry) { cleanup() measureNanoTime {