[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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -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
|
||||
}
|
||||
|
||||
+5
-1
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user