[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.
This commit is contained in:
Mads Ager
2022-05-25 14:10:58 +02:00
committed by Space
parent 3b5179686e
commit ddb140cc51
4 changed files with 320 additions and 2 deletions
@@ -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() }),
@@ -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<K: Comparable<K>, V> {
// Nodes of the splay tree.
class Node<K: Comparable<K>, V>(val key: K, val value: V) {
var left: Node<K, V>? = null
var right: Node<K, V>? = null
// Performs an ordered traversal of the subtree starting at this SplayTree.Node.
fun traverse(f: (Node<K, V>) -> Unit) {
var current: Node<K, V>? = this
while (current != null) {
current.left?.traverse(f)
f(current)
current = current.right
}
}
}
// Root of the splay tree.
private var root: Node<K, V>? = 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<K, V> {
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<K, V>? {
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<K, V>? = null): Node<K, V>? {
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<K, V>? {
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<K> {
val result = mutableListOf<K>()
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<Any, Any> {
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<Int, Pair<Any, Any>>, 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<Int, Pair<Any, Any>> {
val result = SplayTree<Int, Pair<Any, Any>>()
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)
}
}
}
}
@@ -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 <T: Any> 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 <T: Any> 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
}
@@ -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 {