diff --git a/.gitignore b/.gitignore
index eb5a316..aa2d97a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
target
+dependency-reduced-pom.xml
diff --git a/README.md b/README.md
index 23038f8..3f763a5 100644
--- a/README.md
+++ b/README.md
@@ -2,5 +2,8 @@
```bash
mvn package -q
-java --enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow -jar target/benchmarks.jar
+java -jar target/benchmarks.jar
+
+# optional: suppress JDK warning from the launcher JVM
+java --sun-misc-unsafe-memory-access=allow -jar target/benchmarks.jar
```
diff --git a/pom.xml b/pom.xml
index 20146cd..47f709c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,21 +35,21 @@
com.google.code.gson
gson
- 2.10.1
+ 2.13.2
com.esotericsoftware
kryo
- 5.6.0
+ 5.6.2
org.xerial
sqlite-jdbc
- 3.45.1.0
+ 3.51.3.0
@@ -66,11 +66,32 @@
2026.1
+
+
+ org.lmdbjava
+ lmdbjava
+ 0.9.3
+
+
+
+
+ org.mapdb
+ mapdb
+ 3.1.0
+
+
+
+
+ org.duckdb
+ duckdb_jdbc
+ 1.5.1.0
+
+
org.slf4j
slf4j-nop
- 2.0.13
+ 2.0.17
runtime
diff --git a/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java b/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java
index d8a8299..f740dca 100644
--- a/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java
+++ b/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java
@@ -20,6 +20,9 @@ public class DataAccessBenchmark {
@Param({
"in-memory",
"memory-mapped-file",
+ "lmdb",
+ "mapdb",
+ "duckdb-jdbc",
"gson",
"kryo",
"sqlite-jdbc-memory",
diff --git a/src/main/java/com/benchmark/fixtures/DuckDbJdbcFixture.java b/src/main/java/com/benchmark/fixtures/DuckDbJdbcFixture.java
new file mode 100644
index 0000000..bc8c1d4
--- /dev/null
+++ b/src/main/java/com/benchmark/fixtures/DuckDbJdbcFixture.java
@@ -0,0 +1,80 @@
+package com.benchmark.fixtures;
+
+import com.benchmark.model.User;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+
+public class DuckDbJdbcFixture implements DataFixtures {
+
+ private Connection connection;
+ private PreparedStatement selectStmt;
+
+ @Override
+ public void setup(List users) throws Exception {
+ connection = DriverManager.getConnection("jdbc:duckdb:");
+
+ try (Statement stmt = connection.createStatement()) {
+ stmt.execute("""
+ CREATE TABLE IF NOT EXISTS users (
+ id BIGINT PRIMARY KEY,
+ name VARCHAR NOT NULL,
+ email VARCHAR NOT NULL,
+ age INTEGER NOT NULL,
+ created_at BIGINT NOT NULL
+ )
+ """);
+ }
+
+ connection.setAutoCommit(false);
+ try (PreparedStatement ps = connection.prepareStatement(
+ "INSERT INTO users(id, name, email, age, created_at) VALUES(?,?,?,?,?)")) {
+ for (User user : users) {
+ ps.setLong(1, user.getId());
+ ps.setString(2, user.getName());
+ ps.setString(3, user.getEmail());
+ ps.setInt(4, user.getAge());
+ ps.setLong(5, user.getCreatedAt());
+ ps.addBatch();
+ }
+ ps.executeBatch();
+ }
+ connection.commit();
+ connection.setAutoCommit(true);
+
+ selectStmt = connection.prepareStatement(
+ "SELECT id, name, email, age, created_at FROM users WHERE id = ?");
+ }
+
+ @Override
+ public User findById(long id) throws SQLException {
+ selectStmt.setLong(1, id);
+ try (ResultSet rs = selectStmt.executeQuery()) {
+ if (!rs.next()) {
+ return null;
+ }
+ return new User(
+ rs.getLong("id"),
+ rs.getString("name"),
+ rs.getString("email"),
+ rs.getInt("age"),
+ rs.getLong("created_at")
+ );
+ }
+ }
+
+ @Override
+ public void teardown() throws Exception {
+ if (selectStmt != null) {
+ selectStmt.close();
+ }
+ if (connection != null) {
+ connection.close();
+ }
+ }
+}
diff --git a/src/main/java/com/benchmark/fixtures/FixtureFactory.java b/src/main/java/com/benchmark/fixtures/FixtureFactory.java
index 8e1c681..ec20585 100644
--- a/src/main/java/com/benchmark/fixtures/FixtureFactory.java
+++ b/src/main/java/com/benchmark/fixtures/FixtureFactory.java
@@ -6,6 +6,9 @@ public class FixtureFactory {
return switch (type) {
case "in-memory" -> new InMemoryFixture();
case "memory-mapped-file" -> new MemoryMappedFileFixture();
+ case "lmdb" -> new LmdbFixture();
+ case "mapdb" -> new MapDbFixture();
+ case "duckdb-jdbc" -> new DuckDbJdbcFixture();
case "chronicle-map" -> new ChronicleMapFixture();
case "gson" -> new GsonFileFixture();
case "kryo" -> new KryoFileFixture();
diff --git a/src/main/java/com/benchmark/fixtures/LmdbFixture.java b/src/main/java/com/benchmark/fixtures/LmdbFixture.java
new file mode 100644
index 0000000..f544b84
--- /dev/null
+++ b/src/main/java/com/benchmark/fixtures/LmdbFixture.java
@@ -0,0 +1,108 @@
+package com.benchmark.fixtures;
+
+import com.benchmark.model.User;
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+import org.lmdbjava.Dbi;
+import org.lmdbjava.DbiFlags;
+import org.lmdbjava.Env;
+import org.lmdbjava.Txn;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.List;
+
+public class LmdbFixture implements DataFixtures {
+
+ private Env env;
+ private Dbi db;
+ private Kryo kryo;
+ private Path envDir;
+
+ @Override
+ public void setup(List users) throws IOException {
+ kryo = new Kryo();
+ kryo.setRegistrationRequired(false);
+ kryo.register(User.class);
+
+ envDir = Files.createTempDirectory("benchmark-lmdb-");
+ long mapSize = Math.max(16L * 1024 * 1024, users.size() * 2048L);
+ env = Env.create()
+ .setMapSize(mapSize)
+ .setMaxDbs(1)
+ .setMaxReaders(1)
+ .open(envDir.toFile());
+
+ db = env.openDbi("users", DbiFlags.MDB_CREATE);
+
+ try (Txn txn = env.txnWrite()) {
+ for (User user : users) {
+ byte[] bytes = serialize(user);
+ db.put(txn, keyBuffer(user.getId()), valueBuffer(bytes));
+ }
+ txn.commit();
+ }
+ }
+
+ @Override
+ public User findById(long id) {
+ try (Txn txn = env.txnRead()) {
+ ByteBuffer value = db.get(txn, keyBuffer(id));
+ if (value == null) {
+ return null;
+ }
+ ByteBuffer copy = value.duplicate();
+ byte[] bytes = new byte[copy.remaining()];
+ copy.get(bytes);
+ return deserialize(bytes);
+ }
+ }
+
+ @Override
+ public void teardown() throws IOException {
+ if (db != null) {
+ db.close();
+ }
+ if (env != null) {
+ env.close();
+ }
+ if (envDir != null && Files.exists(envDir)) {
+ Files.walk(envDir)
+ .sorted(Comparator.reverseOrder())
+ .forEach(path -> path.toFile().delete());
+ }
+ }
+
+ private ByteBuffer keyBuffer(long id) {
+ ByteBuffer key = ByteBuffer.allocateDirect(Long.BYTES).order(ByteOrder.BIG_ENDIAN);
+ key.putLong(id);
+ key.flip();
+ return key;
+ }
+
+ private ByteBuffer valueBuffer(byte[] bytes) {
+ ByteBuffer value = ByteBuffer.allocateDirect(bytes.length);
+ value.put(bytes);
+ value.flip();
+ return value;
+ }
+
+ private byte[] serialize(User user) {
+ Output output = new Output(256, -1);
+ kryo.writeObject(output, user);
+ byte[] bytes = output.toBytes();
+ output.close();
+ return bytes;
+ }
+
+ private User deserialize(byte[] bytes) {
+ try (Input input = new Input(bytes)) {
+ return kryo.readObject(input, User.class);
+ }
+ }
+}
diff --git a/src/main/java/com/benchmark/fixtures/MapDbFixture.java b/src/main/java/com/benchmark/fixtures/MapDbFixture.java
new file mode 100644
index 0000000..a570b43
--- /dev/null
+++ b/src/main/java/com/benchmark/fixtures/MapDbFixture.java
@@ -0,0 +1,83 @@
+package com.benchmark.fixtures;
+
+import com.benchmark.model.User;
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+import org.mapdb.DB;
+import org.mapdb.DBMaker;
+import org.mapdb.HTreeMap;
+import org.mapdb.Serializer;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.List;
+
+public class MapDbFixture implements DataFixtures {
+
+ private DB db;
+ private HTreeMap usersById;
+ private Kryo kryo;
+ private Path dbDir;
+ private Path dbFile;
+
+ @Override
+ public void setup(List users) throws IOException {
+ kryo = new Kryo();
+ kryo.setRegistrationRequired(false);
+ kryo.register(User.class);
+
+ dbDir = Files.createTempDirectory("benchmark-mapdb-");
+ dbFile = dbDir.resolve("data.db");
+ db = DBMaker
+ .fileDB(dbFile.toFile())
+ .fileMmapEnableIfSupported()
+ .closeOnJvmShutdown()
+ .make();
+
+ usersById = db
+ .hashMap("users", Serializer.LONG, Serializer.BYTE_ARRAY)
+ .createOrOpen();
+
+ for (User user : users) {
+ usersById.put(user.getId(), serialize(user));
+ }
+ }
+
+ @Override
+ public User findById(long id) {
+ byte[] bytes = usersById.get(id);
+ return bytes == null ? null : deserialize(bytes);
+ }
+
+ @Override
+ public void teardown() throws IOException {
+ if (db != null) {
+ db.close();
+ }
+ if (dbFile != null) {
+ Files.deleteIfExists(dbFile);
+ }
+ if (dbDir != null && Files.exists(dbDir)) {
+ Files.walk(dbDir)
+ .sorted(Comparator.reverseOrder())
+ .forEach(path -> path.toFile().delete());
+ }
+ }
+
+ private byte[] serialize(User user) {
+ Output output = new Output(256, -1);
+ kryo.writeObject(output, user);
+ byte[] bytes = output.toBytes();
+ output.close();
+ return bytes;
+ }
+
+ private User deserialize(byte[] bytes) {
+ try (Input input = new Input(bytes)) {
+ return kryo.readObject(input, User.class);
+ }
+ }
+}
diff --git a/src/main/java/com/benchmark/runner/BenchmarkMain.java b/src/main/java/com/benchmark/runner/BenchmarkMain.java
index 5ee4a64..41c881c 100644
--- a/src/main/java/com/benchmark/runner/BenchmarkMain.java
+++ b/src/main/java/com/benchmark/runner/BenchmarkMain.java
@@ -1,12 +1,21 @@
package com.benchmark.runner;
import com.benchmark.benchmarks.DataAccessBenchmark;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.results.Result;
+import org.openjdk.jmh.results.RunResult;
import org.openjdk.jmh.runner.options.CommandLineOptions;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.ChainedOptionsBuilder;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
public class BenchmarkMain {
public static void main(String[] args) throws Exception {
@@ -23,6 +32,9 @@ public class BenchmarkMain {
"--enable-native-access=ALL-UNNAMED",
"--sun-misc-unsafe-memory-access=allow",
"--add-opens=java.base/java.lang=ALL-UNNAMED",
+ "--add-opens=java.base/java.nio=ALL-UNNAMED",
+ "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED",
+ "--add-exports=java.base/sun.nio.ch=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
@@ -50,7 +62,46 @@ public class BenchmarkMain {
}
Options opt = builder.build();
- new Runner(opt).run();
+ Collection runResults = new Runner(opt).run();
+ printSortedSummary(runResults);
+ }
+
+ private static void printSortedSummary(Collection runResults) {
+ if (runResults == null || runResults.isEmpty()) {
+ return;
+ }
+
+ Map> byMode = new LinkedHashMap<>();
+ for (RunResult runResult : runResults) {
+ byMode.computeIfAbsent(runResult.getParams().getMode(), m -> new ArrayList<>()).add(runResult);
+ }
+
+ System.out.println("\n=== Sorted summary by performance ===");
+ for (Map.Entry> entry : byMode.entrySet()) {
+ Mode mode = entry.getKey();
+ List sorted = new ArrayList<>(entry.getValue());
+ boolean lowerIsBetter = mode == Mode.AverageTime || mode == Mode.SampleTime || mode == Mode.SingleShotTime;
+
+ sorted.sort((a, b) -> {
+ double scoreA = a.getPrimaryResult().getScore();
+ double scoreB = b.getPrimaryResult().getScore();
+ return lowerIsBetter ? Double.compare(scoreA, scoreB) : Double.compare(scoreB, scoreA);
+ });
+
+ System.out.printf("%n[%s]%n", mode);
+ System.out.printf("%-24s %14s %14s %s%n", "fixtureType", "score", "error", "unit");
+ for (RunResult runResult : sorted) {
+ Result> result = runResult.getPrimaryResult();
+ String fixtureType = runResult.getParams().getParam("fixtureType");
+ System.out.printf(
+ "%-24s %14.3f %14.3f %s%n",
+ fixtureType,
+ result.getScore(),
+ result.getScoreError(),
+ result.getScoreUnit()
+ );
+ }
+ }
}
private static boolean isJmhListingOrHelpCommand(String[] args) {