diff --git a/benchmarks/run.sh b/benchmarks/run.sh index fd8ef6f..b5e0a39 100755 --- a/benchmarks/run.sh +++ b/benchmarks/run.sh @@ -8,7 +8,11 @@ threads="${THREADS:-$(getconf _NPROCESSORS_ONLN)}" heap="${HEAP:-4g}" history="${HISTORY:-64}" runs="${RUNS:-3}" +cache="${CACHE_MB:-0}" results="results/scale-$scale-$durability-history-$history" +if [ "$cache" != 0 ]; then + results="$results-cache-$cache" +fi ../mvnw -q -f ../pom.xml -Pbenchmarks -DskipTests install classpath="target/classes:$(cat target/classpath.txt)" @@ -22,7 +26,7 @@ rm -rf "$results" && mkdir -p "$results" for run in $(seq 1 "$runs"); do for store in hstore hypergraphdb; do java "${jvm[@]}" io.hstore.bench.Comparison run --store "$store" --scale "$scale" --durability "$durability" \ - --threads "$threads" --history "$history" --out "$results/$store-$run.json" + --threads "$threads" --history "$history" --cache-mb "$cache" --out "$results/$store-$run.json" done done java "${jvm[@]}" io.hstore.bench.Comparison report "$results"/hstore-*.json "$results"/hypergraphdb-*.json | tee "$results/report.md" diff --git a/benchmarks/src/main/java/io/hstore/bench/Comparison.java b/benchmarks/src/main/java/io/hstore/bench/Comparison.java index fa645a8..fb5656c 100644 --- a/benchmarks/src/main/java/io/hstore/bench/Comparison.java +++ b/benchmarks/src/main/java/io/hstore/bench/Comparison.java @@ -73,7 +73,7 @@ public static void main(String[] args) throws Exception { case "report" -> IO.println(Report.markdown(args)); default -> { IO.println(""" - usage: Comparison run --store hstore|hypergraphdb [--scale N] [--durability async|sync] [--threads N] [--history N] --out FILE + usage: Comparison run --store hstore|hypergraphdb [--scale N] [--durability async|sync] [--threads N] [--history N] [--cache-mb N] --out FILE Comparison report FILE... (runs of two stores; cells show the median and range)"""); System.exit(2); } @@ -97,13 +97,14 @@ private static void run(Map options) throws Exception { int threads = Integer.parseInt(options.getOrDefault("threads", String.valueOf(Runtime.getRuntime().availableProcessors()))); Path out = Path.of(options.getOrDefault("out", "results/" + storeName + ".json")); Path directory = Files.createTempDirectory("hstore-bench-" + storeName); + long cacheBytes = Long.parseLong(options.getOrDefault("cache-mb", "0")) << 20; Dataset dataset = Dataset.generate(scale, SEED); List results = new ArrayList<>(); Map totals = new LinkedHashMap<>(); long baselineHeap = JvmUsage.retainedHeap(); Store store = switch (storeName) { - case "hstore" -> new HStoreStore(directory, sync, Integer.parseInt(options.getOrDefault("history", "64"))); - case "hypergraphdb" -> new HyperGraphDbStore(directory, sync); + case "hstore" -> new HStoreStore(directory, sync, Integer.parseInt(options.getOrDefault("history", "64")), cacheBytes); + case "hypergraphdb" -> new HyperGraphDbStore(directory, sync, cacheBytes); default -> throw new IllegalArgumentException("unknown store " + storeName); }; IO.println("%s: %,d nodes, %,d hyperedges, %,d incidences, durability %s, %d threads" diff --git a/benchmarks/src/main/java/io/hstore/bench/HStoreStore.java b/benchmarks/src/main/java/io/hstore/bench/HStoreStore.java index 70d3bd6..63c50fa 100644 --- a/benchmarks/src/main/java/io/hstore/bench/HStoreStore.java +++ b/benchmarks/src/main/java/io/hstore/bench/HStoreStore.java @@ -34,11 +34,17 @@ final class HStoreStore implements Store { private final int history; - HStoreStore(Path directory, boolean sync, int history) { + private final int cachedNodes; + private final long cacheBytes; + + HStoreStore(Path directory, boolean sync, int history, long cacheBytes) { this.directory = directory; this.history = history; - this.options = DatabaseOptions.defaults().withEngine(EngineOptions.defaults() - .withDurability(sync ? Durability.SYNC : Durability.ASYNC).withCachedNodes(CACHED_NODES).withHistoryLimit(history)); + this.cacheBytes = cacheBytes; + EngineOptions engine = EngineOptions.defaults(); + this.cachedNodes = cacheBytes > 0 ? Math.toIntExact(cacheBytes / engine.pageSize()) : CACHED_NODES; + this.options = DatabaseOptions.defaults().withEngine(engine + .withDurability(sync ? Durability.SYNC : Durability.ASYNC).withCachedNodes(cachedNodes).withHistoryLimit(history)); this.database = HypergraphDatabase.open(directory, options); database.write(writer -> { writer.defineNode(NODE, List.of(new PropertyDef("name", TypeTag.STRING, false, false))); @@ -59,7 +65,8 @@ public String version() { @Override public String cache() { - return "node cache of %,d decoded nodes and retains %d generations of history".formatted(CACHED_NODES, history); + String budget = cacheBytes > 0 ? " (%,d MiB divided by the page size)".formatted(cacheBytes >> 20) : ""; + return "node cache of %,d decoded nodes%s and retains %d generations of history".formatted(cachedNodes, budget, history); } @Override diff --git a/benchmarks/src/main/java/io/hstore/bench/HyperGraphDbStore.java b/benchmarks/src/main/java/io/hstore/bench/HyperGraphDbStore.java index 1c2803d..29b675b 100644 --- a/benchmarks/src/main/java/io/hstore/bench/HyperGraphDbStore.java +++ b/benchmarks/src/main/java/io/hstore/bench/HyperGraphDbStore.java @@ -2,6 +2,7 @@ import com.sleepycat.je.Durability; import com.sleepycat.je.Environment; +import com.sleepycat.je.EnvironmentConfig; import com.sleepycat.je.EnvironmentStats; import com.sleepycat.je.StatsConfig; import org.hypergraphdb.HGConfiguration; @@ -29,12 +30,18 @@ final class HyperGraphDbStore implements Store { private HGPersistentHandle large; private long writtenBeforeReopen; - HyperGraphDbStore(Path directory, boolean sync) { + private final long cacheBytes; + + HyperGraphDbStore(Path directory, boolean sync, long cacheBytes) { this.directory = directory; + this.cacheBytes = cacheBytes; configuration.setTransactional(true); + EnvironmentConfig environment = ((BJEStorageImplementation) configuration.getStoreImplementation()).getConfiguration().getEnvironmentConfig(); if (sync) { - ((BJEStorageImplementation) configuration.getStoreImplementation()).getConfiguration() - .getEnvironmentConfig().setDurability(Durability.COMMIT_SYNC); + environment.setDurability(Durability.COMMIT_SYNC); + } + if (cacheBytes > 0) { + environment.setCacheSize(cacheBytes); } this.graph = HGEnvironment.get(directory.toString(), configuration); } @@ -51,7 +58,8 @@ public String version() { @Override public String cache() { - return "Berkeley DB JE cache at 30% of the heap (HyperGraphDB default) plus the HyperGraphDB atom cache"; + String je = cacheBytes > 0 ? "Berkeley DB JE cache of %,d MiB".formatted(cacheBytes >> 20) : "Berkeley DB JE cache at 30% of the heap (HyperGraphDB default)"; + return je + " plus the HyperGraphDB atom cache"; } @Override diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 49fa5b9..e109a7b 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -21,7 +21,7 @@ flowchart LR |---|---|---| | Version | 0.1.0 (this repository) | 1.4 at commit `99485a1`, storage `bdb-je` on Berkeley DB JE 5.0.73 | | Data model used | node type `Entity` (indexed by key, property `name`); `SET_EDGE` type `Relation` | `String` atoms; `HGPlainLink` links | -| Cache | node cache of 1,048,576 decoded nodes | JE cache at 30% of the heap (HyperGraphDB default) plus the HyperGraphDB atom cache | +| Cache | node cache of 1,048,576 decoded nodes, or `CACHE_MB` / page size | JE cache at 30% of the heap (HyperGraphDB default), or `CACHE_MB`, plus the HyperGraphDB atom cache | | `async` durability | `durability = async` | HyperGraphDB default: JE `WRITE_NO_SYNC` | | `sync` durability | `durability = sync`, `wal_mode = references`, group commit | JE `COMMIT_SYNC` | | History | `history_limit = 64` generations retained for time travel (default) | none | @@ -210,3 +210,23 @@ SCALE=4 DURABILITY=async HISTORY=1 benchmarks/run.sh run in brackets, and "Results agree" requires every run of both stores to return the same checksum. `THREADS` and `HEAP` override the thread count and heap size. Close other workloads first: the numbers are only meaningful on an otherwise idle machine. + +**Equal cache budgets.** By default the two caches are not the same size. HStore caches about a million decoded +nodes, which at 16 KiB pages is enough to hold the whole dataset. HyperGraphDB gets JE's default of 30% of the +heap. `CACHE_MB` gives both the same budget: + +| HStore | HyperGraphDB | +|---|---| +| node cache of `CACHE_MB` / page size nodes | JE cache of exactly `CACHE_MB` | + +Decoded nodes can take more or less heap than their page, and HyperGraphDB's own atom and incidence caches can't +be capped in bytes (they shrink under memory pressure). So check the retained-heap line in the report to see what +each store actually kept. Results go to a directory ending in `-cache-`. + +``` +SCALE=4 CACHE_MB=64 benchmarks/run.sh # working set several times larger than either cache +``` + +With a small budget, reads that miss the cache go to the files, but the operating system's page cache will +usually still serve them from memory. Reads only really hit the disk when the data is larger than the machine's +free RAM.