[benchmarks] jmh-benchmarks for map and array lookup

+ j.u.HashMap vs fastutils
+ array binary vs linear search

GitOrigin-RevId: e8e0b97b9d7484d65b8b9fd41af9b7805a75bd26
This commit is contained in:
Ruslan Cheremin
2024-09-24 20:44:11 +00:00
committed by intellij-monorepo-bot
parent 5b375ff7a2
commit 6d78bbb7b9
2 changed files with 246 additions and 0 deletions
@@ -0,0 +1,124 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* Compare linear search vs binary search
* On Apple M1 Max binary search starts outperforming linear search ~between array length 32 and 64
*/
@BenchmarkMode({Mode.AverageTime/*, Mode.SampleTime*/})
@OutputTimeUnit(NANOSECONDS)
@Warmup(iterations = 2, time = 5, timeUnit = SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = SECONDS)
@Fork(1)
@Threads(1)
public class ArrayLookupBenchmark {
@State(Scope.Benchmark)
public static class DataContext {
@Param({"8", "16", "32", "64"})
public int ARRAY_SIZE = 64;
public Integer[] objects;
public int[] ints;
@Setup
public void setup() throws Exception {
objects = new Integer[ARRAY_SIZE];
ints = new int[ARRAY_SIZE];
ThreadLocalRandom rnd = ThreadLocalRandom.current();
for (int i = 0; i < ARRAY_SIZE; i++) {
int value = rnd.nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE);
ints[i] = value;
objects[i] = value;
}
Arrays.sort(ints);
Arrays.sort(objects);
}
public Integer randomObjectKey() {
return randomIntKey();
}
public int randomIntKey() {
//should be ~1/2 of array values
return 0;
}
}
@Benchmark
public Object _baseline() {
ThreadLocalRandom rnd = ThreadLocalRandom.current();
return rnd.nextInt(0, Integer.MAX_VALUE);
}
@Benchmark
public Object lookupIntLinear(DataContext dataContext) {
int keyToLookup = dataContext.randomIntKey();
int[] ints = dataContext.ints;
for (int i = 0; i < ints.length; i++) {
int key = ints[i];
if (key > keyToLookup) {
return i;
}
}
return -1;
}
@Benchmark
public Object lookupObjectLinear(DataContext dataContext) {
Integer keyToLookup = dataContext.randomObjectKey();
Integer[] objects = dataContext.objects;
for (int i = 0; i < objects.length; i++) {
Integer key = objects[i];
if (key.compareTo(keyToLookup) > 0) {
return i;
}
}
return -1;
}
@Benchmark
public Object lookupIntBinary(DataContext dataContext) {
int keyToLookup = dataContext.randomIntKey();
return Arrays.binarySearch(dataContext.ints, keyToLookup);
}
@Benchmark
public Object lookupObjectBinary(DataContext dataContext) {
Integer keyToLookup = dataContext.randomObjectKey();
return Arrays.binarySearch(dataContext.objects, keyToLookup);
}
public static void main(final String[] args) throws RunnerException {
final Options opt = new OptionsBuilder()
.jvmArgs()
.forks(1)
.threads(1)
//.mode(Mode.SingleShotTime)
//.warmupIterations(1000)
//.warmupBatchSize(1000)
//.measurementIterations(1000)
.include("\\W" + ArrayLookupBenchmark.class.getSimpleName() + ".*")
.build();
new Runner(opt).run();
}
}
@@ -0,0 +1,122 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* Benchmarks of {@link java.util.HashMap} vs {@link it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap}
* ...it seems like HashMap consistently outperforms Object2ObjectOpenHashMap, even though I can't really get the reasons
* TODO RC: add some simple HMap impl into the benchmarks
*/
@BenchmarkMode({Mode.AverageTime/*, Mode.SampleTime*/})
@OutputTimeUnit(NANOSECONDS)
@Warmup(iterations = 2, time = 5, timeUnit = SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = SECONDS)
@Fork(1)
@Threads(1)
public class MapImplBenchmark {
@State(Scope.Benchmark)
public static class DataContext {
@Param({"1000", "10000", "500000"})
public int KEYS_COUNT = 500_000;
public Object[] existingKeys;
public Object[] nonExistingKeys;
@Setup
public void setup() throws Exception {
existingKeys = new Object[KEYS_COUNT];
nonExistingKeys = new Object[KEYS_COUNT];
ThreadLocalRandom rnd = ThreadLocalRandom.current();
for (int i = 0; i < KEYS_COUNT; i++) {
int value = rnd.nextInt(0, Integer.MAX_VALUE);
existingKeys[i] = value;
nonExistingKeys[i] = -1 - value;
}
}
public Object existingKey(ThreadLocalRandom rnd) {
return existingKeys[rnd.nextInt(0, KEYS_COUNT)];
}
public Object nonExistingKey(ThreadLocalRandom rnd) {
return nonExistingKeys[rnd.nextInt(0, KEYS_COUNT)];
}
}
@State(Scope.Benchmark)
public static class MapContext {
@Param({"HashMap", "OpenHashMap"})
public String mapKind = "HashMap";
public Map<Object, Object> mapImpl;
@Setup
public void setup(DataContext dataContext) throws Exception {
mapImpl = switch (mapKind) {
case "HashMap" -> new HashMap<>();
case "OpenHashMap" ->
//noinspection SSBasedInspection
new Object2ObjectOpenHashMap<>();
default -> throw new IllegalArgumentException("Unrecognized map kind: " + mapKind);
};
for (int i = 0; i < dataContext.KEYS_COUNT; i++) {
mapImpl.put(
dataContext.existingKeys[i],
dataContext.existingKeys[i]
);
}
}
}
@Benchmark
public Object _baseline(DataContext dataContext) {
ThreadLocalRandom rnd = ThreadLocalRandom.current();
return dataContext.existingKey(rnd);
}
@Benchmark
public Object lookupExistingKey(DataContext dataContext,
MapContext mapContext) {
ThreadLocalRandom rnd = ThreadLocalRandom.current();
return mapContext.mapImpl.get(dataContext.existingKey(rnd));
}
@Benchmark
public Object lookupNonExistingKey(DataContext dataContext,
MapContext mapContext) {
ThreadLocalRandom rnd = ThreadLocalRandom.current();
return mapContext.mapImpl.get(dataContext.nonExistingKey(rnd));
}
public static void main(final String[] args) throws RunnerException {
final Options opt = new OptionsBuilder()
.jvmArgs()
.forks(1)
.threads(1)
//.mode(Mode.SingleShotTime)
//.warmupIterations(1000)
//.warmupBatchSize(1000)
//.measurementIterations(1000)
.include("\\W" + MapImplBenchmark.class.getSimpleName() + ".*")
.build();
new Runner(opt).run();
}
}