From 5aebd8899246d6e19508c6b651ea80db9a0dcf70 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 17 Jan 2024 20:07:17 +0000 Subject: [PATCH] Replace LuaTable.Node with flat arrays Rather than having a Node[] array, we now have separate arrays for keys, values and the next slot. This allows for slightly less pointer chasing and arguably better cache locality - never sure how much we can do that in Java! Valhalla can't come soon enough :(. --- build.gradle.kts | 7 + .../java/org/squiddev/cobalt/LuaTable.java | 309 +++++++++--------- .../cobalt/benchmark/BenchmarkFast.java | 2 - .../cobalt/benchmark/BenchmarkFull.java | 1 + .../cc/tweaked/cobalt/benchmark/FibLoop.java | 6 - .../cobalt/benchmark/FibRecursive.java | 6 - .../cobalt/benchmark/LuaBenchmark.java | 2 + .../cc/tweaked/cobalt/benchmark/SumPairs.java | 6 - .../cc/tweaked/cobalt/benchmark/TableOps.java | 149 +++++++++ .../cobalt/table/TableOperations.java | 2 +- 10 files changed, 310 insertions(+), 180 deletions(-) create mode 100644 src/test/java/cc/tweaked/cobalt/benchmark/TableOps.java diff --git a/build.gradle.kts b/build.gradle.kts index 8b724b54..21eb6536 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -175,3 +175,10 @@ tasks.test { events("skipped", "failed") } } + +val benchmark by tasks.registering(JavaExec::class) { + description = "Run our benchmarking suite" + + classpath(sourceSets.test.map { it.runtimeClasspath }) + mainClass = "cc.tweaked.cobalt.benchmark.BenchmarkFull" +} diff --git a/src/main/java/org/squiddev/cobalt/LuaTable.java b/src/main/java/org/squiddev/cobalt/LuaTable.java index 0bfdf091..6ec82539 100644 --- a/src/main/java/org/squiddev/cobalt/LuaTable.java +++ b/src/main/java/org/squiddev/cobalt/LuaTable.java @@ -27,6 +27,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import java.lang.ref.WeakReference; +import java.util.Arrays; import java.util.Map; import static org.squiddev.cobalt.Constants.*; @@ -72,10 +73,14 @@ */ public final class LuaTable extends LuaValue { private static final Object[] EMPTY_ARRAY = new Object[0]; - private static final Node[] EMPTY_NODES = new Node[0]; + private static final int[] EMPTY_NEXT = new int[0]; private Object[] array = EMPTY_ARRAY; - private Node[] nodes = EMPTY_NODES; + + private Object[] keys = EMPTY_ARRAY; + private Object[] values = EMPTY_ARRAY; + private int[] next = EMPTY_NEXT; + private int lastFree = 0; private boolean weakKeys; @@ -116,7 +121,7 @@ public LuaTable checkTable() { */ public void presize(int nArray) { if (nArray > array.length) { - resize(nArray, nodes.length, false); + resize(nArray, keys.length, false); } } @@ -202,7 +207,7 @@ public int length() { } } return m; - } else if (nodes.length == 0) { + } else if (keys.length == 0) { // When no nodes are present and the last item is not nil, // the size of the table is the exact same size its capacity, // so we can directly return the array.length @@ -242,12 +247,12 @@ public int length() { * @see #length() */ public int size() { - int i = 0; - for (var k : array) if (!strengthen(k).isNil()) i++; - for (var e : nodes) { - if (!e.key().isNil() && !e.value().isNil()) i++; + int n = 0; + for (var k : array) if (!strengthen(k).isNil()) n++; + for (int i = 0; i < keys.length; i++) { + if (!key(i).isNil() && !value(i).isNil()) n++; } - return i; + return n; } /** @@ -286,10 +291,10 @@ public Varargs next(LuaValue key) throws LuaError { } i -= array.length; - for (; i < nodes.length; i++) { - Node node = nodes[i]; - LuaValue value = node.value(); - if (!node.key().isNil() && !value.isNil()) return varargsOf(node.key(), value); + for (; i < keys.length; i++) { + LuaValue thisKey = key(i); + LuaValue thisValue = value(i); + if (!thisKey.isNil() && !thisValue.isNil()) return varargsOf(thisKey, thisValue); } return NIL; @@ -308,22 +313,17 @@ private int findIndex(LuaValue key) { // Its in the array part so just return that int arrayIndex = arraySlot(key); if (arrayIndex > 0 && arrayIndex <= array.length) return arrayIndex; - if (nodes.length == 0) return -1; + if (keys.length == 0) return -1; // Must be in the main part so try to find it in the chain. int idx = hashSlot(key); - Node node = nodes[idx]; while (true) { - if (node.key().equals(key)) { + if (key(idx).equals(key)) { return idx + array.length + 1; } - if (node.next >= 0) { - idx = node.next; - node = nodes[node.next]; - } else { - return -1; - } + idx = next[idx]; + if (idx < 0) return -1; } } @@ -371,7 +371,7 @@ private static int arraySlot(LuaValue value) { * @return slot to use */ private int hashSlot(LuaValue key) { - return hashSlot(key, nodes.length - 1); + return hashSlot(key, keys.length - 1); } private void dropWeakArrayValues() { @@ -451,13 +451,21 @@ private int numUseArray(int[] nums) { private void setNodeVector(int size) { if (size == 0) { - nodes = EMPTY_NODES; + keys = values = EMPTY_ARRAY; + next = EMPTY_NEXT; lastFree = 0; } else { int lsize = log2(size); size = 1 << lsize; - Node[] nodes = this.nodes = new Node[size]; - for (int i = 0; i < size; i++) nodes[i] = new Node(weakKeys, weakValues); + + keys = new Object[size]; + values = new Object[size]; + next = new int[size]; + + // TODO: It would be nice if we didn't need to fill here, as this can be quite slow. + Arrays.fill(keys, NIL); + Arrays.fill(values, NIL); + Arrays.fill(next, -1); // All positions are free lastFree = size - 1; @@ -466,14 +474,15 @@ private void setNodeVector(int size) { private void resize(int newArraySize, int newHashSize, boolean modeChange) { int oldArraySize = array.length; - int oldHashSize = nodes.length; + int oldHashSize = keys.length; // Array part must grow if (newArraySize > oldArraySize) { array = setArrayVector(array, newArraySize, modeChange, weakValues); } - Node[] oldNode = nodes; + Object[] oldKeys = keys; + Object[] oldValues = values; setNodeVector(newHashSize); if (newArraySize < oldArraySize) { @@ -496,9 +505,8 @@ private void resize(int newArraySize, int newHashSize, boolean modeChange) { // Re-insert elements from hash part for (int i = oldHashSize - 1; i >= 0; i--) { - Node old = oldNode[i]; - LuaValue key = old.key(); - LuaValue value = old.value(); + LuaValue key = key(oldKeys, oldValues, i, true); + LuaValue value = value(oldValues, i, true); if (!key.isNil() && !value.isNil()) rawsetImpl(key, value); } } @@ -514,11 +522,12 @@ private void rehash(LuaValue extraKey, boolean mode) { // Count the number of hash values that can be moved to the array, as well as the total count. // See numusehash in ltable.c { - int i = nodes.length; + int i = keys.length; while (--i >= 0) { - Node node = nodes[i]; - if (!node.value().isNil()) { - arrayCount += countInt(node.key(), nums); + LuaValue key = key(keys, values, i, true); + LuaValue value = value(values, i, true); + if (!value.isNil()) { + arrayCount += countInt(key, nums); totalCount++; } } @@ -565,10 +574,10 @@ private void rehash(LuaValue extraKey, boolean mode) { * @return The first slot in the map */ private int getFreePos() { - if (nodes.length == 0) return -1; + if (keys.length == 0) return -1; while (lastFree >= 0) { - Node last = nodes[lastFree--]; - if (last.key == NIL) { + LuaValue last = key(lastFree--); + if (last == NIL) { return lastFree + 1; } } @@ -576,6 +585,45 @@ private int getFreePos() { return -1; } + /** + * Get the current key, converting it to a strong reference if required. If it is nil then it clears the key and + * value (marking it as "dead"). + * + * @return The entry's key. + */ + private LuaValue key(int slot) { + return key(keys, values, slot, weakKeys); + } + + private static LuaValue key(Object[] keys, Object[] values, int slot, boolean weak) { + assert keys.length == values.length; + Object key = keys[slot]; + if (key == NIL || !weak) return (LuaValue) key; + + LuaValue strengthened = strengthen(key); + if (strengthened.isNil()) values[slot] = NIL; // We preserve the key so we can check it is nil + + return strengthened; + } + + private LuaValue value(int slot) { + return value(values, slot, weakValues); + } + + /** + * Get the current value, converting it to a strong reference if required. + * + * @return The entry's value. + */ + private static LuaValue value(Object[] values, int slot, boolean weak) { + Object value = values[slot]; + if (value == NIL || !weak) return (LuaValue) value; + + LuaValue strengthened = strengthen(value); + if (strengthened.isNil()) values[slot] = NIL; + return strengthened; + } + /** * Insert a new key into a hash table. *

@@ -586,103 +634,95 @@ private int getFreePos() { * @param key The key to set * @throws IllegalArgumentException If this key cannot be used. */ - private Node newKey(LuaValue key) { + private int newKey(LuaValue key) { if (key.isNil()) throw new IllegalArgumentException("table index is nil"); // Rehash and let the rawgetter handle it - if (nodes.length == 0) { + if (keys.length == 0) { rehash(key, false); - return null; + return -1; } - Node mainNode = nodes[hashSlot(key)]; - LuaValue mainKey = mainNode.key(); - if (!mainKey.isNil() && !mainNode.value().isNil()) { + int mainNode = hashSlot(key); + LuaValue mainKey = key(mainNode); + if (!mainKey.isNil() && !value(mainNode).isNil()) { // If we've got a collision then - final int freePos = getFreePos(); + final int freeNode = getFreePos(); - if (freePos < 0) { + if (freeNode < 0) { rehash(key, false); - return null; + return -1; } - final Node freeNode = nodes[freePos]; - - int otherPos = hashSlot(mainKey); - Node otherNode = nodes[otherPos]; + int otherNode = hashSlot(mainKey); if (otherNode != mainNode) { // If the colliding position isn't at its main position then we move it to a free position // Walk the chain to find the node just before the desired one - while (nodes[otherNode.next] != mainNode) { - otherNode = nodes[otherNode.next]; - } + while (next[otherNode] != mainNode) otherNode = next[otherNode]; // Rechain other to point to the free position - otherNode.next = freePos; + next[otherNode] = freeNode; // Copy colliding node into free position - freeNode.key = mainNode.key; - freeNode.value = mainNode.value; - freeNode.next = mainNode.next; + keys[freeNode] = keys[mainNode]; + values[freeNode] = values[mainNode]; + next[freeNode] = next[mainNode]; // Clear main node - mainNode.next = -1; - mainNode.key = NIL; - mainNode.value = NIL; + next[mainNode] = -1; + keys[mainNode] = NIL; + values[mainNode] = NIL; } else { // Colliding node is in the main position so we will assign to a free position. - if (mainNode.next != -1) { + if (next[mainNode] != -1) { // We're inserting "after" the first node in the linked list so change the // next node. - freeNode.next = mainNode.next; + next[freeNode] = next[mainNode]; } else { - assert freeNode.next == -1; + assert next[freeNode] == -1; } // Insert after the main node - mainNode.next = freePos; + next[mainNode] = freeNode; mainNode = freeNode; } } - mainNode.key = weakKeys ? weaken(key) : key; + keys[mainNode] = weakKeys ? weaken(key) : key; return mainNode; } - private Node getNode(int search) { - if (nodes.length == 0) return null; + private int getNode(int search) { + if (keys.length == 0) return -1; - Node node = nodes[hashmod(search, nodes.length - 1)]; + int node = hashmod(search, keys.length - 1); while (true) { - LuaValue key = node.key(); + LuaValue key = key(node); if (key instanceof LuaInteger keyI && keyI.intValue() == search) { return node; } else { - int next = node.next; - if (next == -1) return null; - node = nodes[next]; + node = next[node]; + if (node == -1) return -1; } } } - private Node getNode(LuaValue search) { - if (nodes.length == 0 || search == NIL) return null; + private int getNode(LuaValue search) { + if (keys.length == 0 || search == NIL) return -1; - int slot = hashSlot(search); - Node node = nodes[slot]; + int node = hashSlot(search); while (true) { - LuaValue key = node.key(); + LuaValue key = key(node); if (key.equals(search)) { return node; } else { - int next = node.next; - if (next == -1) return null; - node = nodes[next]; + node = next[node]; + if (node == -1) return -1; } } } @@ -690,28 +730,28 @@ private Node getNode(LuaValue search) { public LuaValue rawget(int search) { if (search > 0 && search <= array.length) { return strengthen(array[search - 1]); - } else if (nodes.length == 0) { + } else if (keys.length == 0) { return NIL; } else { - Node node = getNode(search); - return node == null ? NIL : node.value(); + int node = getNode(search); + return node == -1 ? NIL : value(node); } } public LuaValue rawget(LuaValue search) { if (search instanceof LuaInteger i) return rawget(i.intValue()); - Node node = getNode(search); - return node == null ? NIL : node.value(); + int node = getNode(search); + return node == -1 ? NIL : value(node); } public LuaValue rawget(CachedMetamethod search) { int flag = 1 << search.ordinal(); if ((metatableFlags & flag) != 0) return NIL; - Node node = getNode(search.getKey()); - if (node != null) { - LuaValue value = node.value(); + int node = getNode(search.getKey()); + if (node != -1) { + LuaValue value = value(node); if (!value.isNil()) return value; } @@ -719,6 +759,11 @@ public LuaValue rawget(CachedMetamethod search) { return NIL; } + /** + * Check if this table has a {@code __newindex} metamethod. + * + * @return Whether this method has a {@code __newindex} metamethod. + */ private boolean hasNewIndex() { LuaTable metatable = this.metatable; return metatable != null && metatable.rawget(CachedMetamethod.NEWINDEX) != NIL; @@ -744,12 +789,12 @@ private boolean trySet(int key, LuaValue value, LuaValue keyValue) { return true; } - Node node = getNode(key); - if (node == null) { + int node = getNode(key); + if (node == -1) { if (hasNewIndex()) return false; } else { - if (node.value() == NIL && hasNewIndex()) return false; - node.value = weakValues ? weaken(value) : value; + if (value(node) == NIL && hasNewIndex()) return false; + values[node] = weakValues ? weaken(value) : value; return true; } @@ -769,12 +814,12 @@ private boolean trySet(int key, LuaValue value, LuaValue keyValue) { boolean trySet(LuaValue key, LuaValue value) throws LuaError { if (key instanceof LuaInteger keyI) return trySet(keyI.intValue(), value, key); - Node node = getNode(key); - if (node == null) { + int node = getNode(key); + if (node == -1) { if (hasNewIndex()) return false; } else { - if (node.value() == NIL && hasNewIndex()) return false; - node.value = weakValues ? weaken(value) : value; + if (value(node) == NIL && hasNewIndex()) return false; + values[node] = weakValues ? weaken(value) : value; return true; } @@ -794,15 +839,15 @@ private void rawset(int key, LuaValue value, LuaValue valueOf) { return; } - Node node = getNode(key); - if (node == null) { + int node = getNode(key); + if (node == -1) { if (valueOf == null) valueOf = valueOf(key); node = newKey(valueOf); } // newKey will have handled this otherwise - if (node != null) { - node.value = weakValues ? weaken(value) : value; + if (node != -1) { + values[node] = weakValues ? weaken(value) : value; return; } } while (true); @@ -823,13 +868,13 @@ public void rawsetImpl(LuaValue key, LuaValue value) { // TODO: Check valid key here instead of at the call site! do { - Node node = getNode(key); - if (node == null) node = newKey(key); + int node = getNode(key); + if (node == -1) node = newKey(key); // newKey will have handled this otherwise - if (node != null) { + if (node != -1) { // if (value.isNil() && !weakKeys) node.key = weaken((LuaValue) node.key); - node.value = weakValues ? weaken(value) : value; + values[node] = weakValues ? weaken(value) : value; metatableFlags = 0; return; } @@ -901,58 +946,4 @@ LuaValue strongValue() { } } //endregion - - /** - * Represents a node in the hash element of the table. - */ - private static final class Node { - private final boolean weakKey; - private final boolean weakValue; - Object value = NIL; - Object key = NIL; - int next = -1; - - Node(boolean weakKey, boolean weakValue) { - this.weakKey = weakKey; - this.weakValue = weakValue; - } - - @Override - public String toString() { - String main = key + "=" + value; - if (next >= 0) main += "->" + next; - return main; - } - - /** - * Get the current key, converting it to a strong reference if - * required. If it is nil then it clears the key and value (marking it - * as "dead"). - * - * @return The entry's key. - */ - LuaValue key() { - Object key = this.key; - if (key == NIL || !weakKey) return (LuaValue) key; - - LuaValue strengthened = strengthen(key); - if (strengthened.isNil()) this.value = NIL; // We preserve the key so we can check it is nil - - return strengthened; - } - - /** - * Get the current value, converting it to a strong reference if required. - * - * @return The entry's value. - */ - LuaValue value() { - Object value = this.value; - if (value == NIL || !weakValue) return (LuaValue) value; - - LuaValue strengthened = strengthen(value); - if (strengthened.isNil()) this.value = NIL; - return strengthened; - } - } } diff --git a/src/test/java/cc/tweaked/cobalt/benchmark/BenchmarkFast.java b/src/test/java/cc/tweaked/cobalt/benchmark/BenchmarkFast.java index 69463779..4a900587 100644 --- a/src/test/java/cc/tweaked/cobalt/benchmark/BenchmarkFast.java +++ b/src/test/java/cc/tweaked/cobalt/benchmark/BenchmarkFast.java @@ -5,7 +5,6 @@ import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; -import org.openjdk.jmh.runner.options.TimeValue; /** * Runs all benchmarks with a minimal number of warmup runs and forks, allowing for quick testing. @@ -19,7 +18,6 @@ public static void main(String... args) throws RunnerException { .include("cc.tweaked.cobalt.benchmark.*") .warmupIterations(1) .measurementIterations(2) - .measurementTime(TimeValue.milliseconds(3000)) .jvmArgsPrepend("-server") .resultFormat(ResultFormatType.JSON) .forks(1) diff --git a/src/test/java/cc/tweaked/cobalt/benchmark/BenchmarkFull.java b/src/test/java/cc/tweaked/cobalt/benchmark/BenchmarkFull.java index 5c403f4a..9e38a422 100644 --- a/src/test/java/cc/tweaked/cobalt/benchmark/BenchmarkFull.java +++ b/src/test/java/cc/tweaked/cobalt/benchmark/BenchmarkFull.java @@ -18,6 +18,7 @@ public static void main(String... args) throws RunnerException { Options opts = new OptionsBuilder() .include("cc.tweaked.cobalt.benchmark.*") .warmupIterations(3) + .warmupTime(TimeValue.milliseconds(1000)) .measurementIterations(5) .measurementTime(TimeValue.milliseconds(3000)) .jvmArgsPrepend("-server") diff --git a/src/test/java/cc/tweaked/cobalt/benchmark/FibLoop.java b/src/test/java/cc/tweaked/cobalt/benchmark/FibLoop.java index a67752a3..4dc13813 100644 --- a/src/test/java/cc/tweaked/cobalt/benchmark/FibLoop.java +++ b/src/test/java/cc/tweaked/cobalt/benchmark/FibLoop.java @@ -1,16 +1,10 @@ package cc.tweaked.cobalt.benchmark; -import org.openjdk.jmh.annotations.*; import org.squiddev.cobalt.ValueFactory; -import java.util.concurrent.TimeUnit; - /** * Evaluates the nth Fibonacci number using a loop. */ -@State(Scope.Thread) -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.MICROSECONDS) public class FibLoop extends LuaBenchmark { public FibLoop() { super(""" diff --git a/src/test/java/cc/tweaked/cobalt/benchmark/FibRecursive.java b/src/test/java/cc/tweaked/cobalt/benchmark/FibRecursive.java index f504f620..937cd13b 100644 --- a/src/test/java/cc/tweaked/cobalt/benchmark/FibRecursive.java +++ b/src/test/java/cc/tweaked/cobalt/benchmark/FibRecursive.java @@ -1,16 +1,10 @@ package cc.tweaked.cobalt.benchmark; -import org.openjdk.jmh.annotations.*; import org.squiddev.cobalt.ValueFactory; -import java.util.concurrent.TimeUnit; - /** * Evaluates the nth Fibonacci number using a recursive function. */ -@State(Scope.Thread) -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.MICROSECONDS) public class FibRecursive extends LuaBenchmark { public FibRecursive() { super(""" diff --git a/src/test/java/cc/tweaked/cobalt/benchmark/LuaBenchmark.java b/src/test/java/cc/tweaked/cobalt/benchmark/LuaBenchmark.java index df39a316..6693cf4e 100644 --- a/src/test/java/cc/tweaked/cobalt/benchmark/LuaBenchmark.java +++ b/src/test/java/cc/tweaked/cobalt/benchmark/LuaBenchmark.java @@ -18,6 +18,8 @@ @State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(time = 3, timeUnit = TimeUnit.SECONDS) +@Measurement(time = 3, timeUnit = TimeUnit.SECONDS) public abstract class LuaBenchmark { private final String program; private final Function argsFactory; diff --git a/src/test/java/cc/tweaked/cobalt/benchmark/SumPairs.java b/src/test/java/cc/tweaked/cobalt/benchmark/SumPairs.java index 99702eb9..2d13adaa 100644 --- a/src/test/java/cc/tweaked/cobalt/benchmark/SumPairs.java +++ b/src/test/java/cc/tweaked/cobalt/benchmark/SumPairs.java @@ -1,17 +1,11 @@ package cc.tweaked.cobalt.benchmark; -import org.openjdk.jmh.annotations.*; import org.squiddev.cobalt.LuaTable; import org.squiddev.cobalt.ValueFactory; -import java.util.concurrent.TimeUnit; - /** * Creates a table mapping strings to numbers, then loops over adding them all up. */ -@State(Scope.Thread) -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.MICROSECONDS) public class SumPairs extends LuaBenchmark { public SumPairs() { super(""" diff --git a/src/test/java/cc/tweaked/cobalt/benchmark/TableOps.java b/src/test/java/cc/tweaked/cobalt/benchmark/TableOps.java new file mode 100644 index 00000000..f204c13b --- /dev/null +++ b/src/test/java/cc/tweaked/cobalt/benchmark/TableOps.java @@ -0,0 +1,149 @@ +package cc.tweaked.cobalt.benchmark; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.squiddev.cobalt.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import static org.squiddev.cobalt.Constants.NIL; + +@State(Scope.Thread) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(time = 1, timeUnit = TimeUnit.SECONDS) +public class TableOps { + /** + * The number of keys we'll attempt to access. + */ + private static final int KEY_COUNT = 100; + + @Param({"5", "20", "100"}) + private int tableSize; + + /** + * A dense integer-indexed table. + */ + private LuaTable denseIntTable; + + /** + * A sparse integer-indexed table. + */ + private LuaTable sparseIntTable; + + /** + * A string-string table. + */ + private LuaTable stringTable; + + /** + * A table indexed by a mixture of strings and tables. + */ + private LuaTable mixedHashTable; + + /** + * A random collection of keys from {@link #mixedHashTable}. + */ + private final LuaValue[] mixedKeysHit = new LuaValue[KEY_COUNT]; + + /** + * A random collection of keys not in {@link #mixedHashTable} + */ + private final LuaValue[] mixedKeysMiss = new LuaValue[KEY_COUNT]; + + @Setup + public void setup() throws LuaError { + denseIntTable = new LuaTable(); + for (int i = 0; i < tableSize; i++) denseIntTable.rawset(i + 1, ValueFactory.valueOf("v" + i)); + + var random = new Random(); + + sparseIntTable = new LuaTable(); + for (int i = 0; i < tableSize; i++) { + sparseIntTable.rawset(random.nextInt(1, 10_000), ValueFactory.valueOf("v" + i)); + } + + stringTable = new LuaTable(); + for (int i = 0; i < tableSize; i++) stringTable.rawset("k" + i, ValueFactory.valueOf("v" + i)); + + List keys = new ArrayList<>(tableSize); + mixedHashTable = new LuaTable(); + for (int i = 0; i < Math.max(1, tableSize / 2); i++) { + keys.add(new LuaTable()); + keys.add(ValueFactory.valueOf("k" + i)); + } + for (int i = 0; i < keys.size(); i++) mixedHashTable.rawset(keys.get(i), ValueFactory.valueOf("v" + i)); + + for (int i = 0; i < KEY_COUNT; i++) { + mixedKeysHit[i] = keys.get(random.nextInt(keys.size())); + mixedKeysMiss[i] = random.nextBoolean() ? new LuaTable() : ValueFactory.valueOf("u" + i); + } + } + + private int consumeTableWithNext(Blackhole bh, LuaTable table) throws LuaError { + int count = 0; + LuaValue k = NIL; + while (true) { + Varargs n = table.next(k); + if ((k = n.first()).isNil()) break; + bh.consume(k); + count++; + } + + if (count != tableSize) throw new IllegalStateException(count + " != expected size " + tableSize); + return count; + } + + private int consumeTableWithLength(Blackhole bh, LuaTable table) { + int count = table.length(); + for (int i = 1; i <= count; i++) bh.consume(table.rawget(i)); + if (count != tableSize) throw new IllegalStateException(count + " != expected size " + tableSize); + return count; + } + + @Benchmark + public int denseIntTableLength() { + return denseIntTable.length(); + } + + @Benchmark + public int denseIntTableConsumeWithNext(Blackhole bh) throws LuaError { + return consumeTableWithNext(bh, denseIntTable); + } + + @Benchmark + public int denseIntTableConsumeWithLength(Blackhole bh) throws LuaError { + return consumeTableWithLength(bh, denseIntTable); + } + + @Benchmark + public int sparseIntTableConsumeWithNext(Blackhole bh) throws LuaError { + return consumeTableWithNext(bh, sparseIntTable); + } + + @Benchmark + public int stringTableConsumeWithNext(Blackhole bh) throws LuaError { + return consumeTableWithNext(bh, stringTable); + } + + @Benchmark + public int mixedTableConsumeWithNext(Blackhole bh) throws LuaError { + return consumeTableWithNext(bh, mixedHashTable); + } + + @Benchmark + @OperationsPerInvocation(KEY_COUNT) + public void mixedTableFetchKeysHit(Blackhole bh) { + for (LuaValue key : mixedKeysHit) bh.consume(mixedHashTable.rawget(key)); + } + + @Benchmark + @OperationsPerInvocation(KEY_COUNT) + public void mixedTableFetchKeysMiss(Blackhole bh) { + for (LuaValue key : mixedKeysMiss) bh.consume(mixedHashTable.rawget(key)); + } +} diff --git a/src/test/java/org/squiddev/cobalt/table/TableOperations.java b/src/test/java/org/squiddev/cobalt/table/TableOperations.java index 86f5161f..143f36ff 100644 --- a/src/test/java/org/squiddev/cobalt/table/TableOperations.java +++ b/src/test/java/org/squiddev/cobalt/table/TableOperations.java @@ -24,7 +24,7 @@ public final class TableOperations { static { Field nodesField, arrayField, lastFreeField; try { - nodesField = LuaTable.class.getDeclaredField("nodes"); + nodesField = LuaTable.class.getDeclaredField("keys"); nodesField.setAccessible(true); arrayField = LuaTable.class.getDeclaredField("array");