Skip to content

Commit 378024b

Browse files
authored
Merge pull request #22 from EngineHub/ot/feature/optimize-lin-bus-dfu
Optimize DFU / tree implementations
2 parents 5bd83e5 + 09fb09b commit 378024b

16 files changed

Lines changed: 1254 additions & 22 deletions

File tree

dfu/src/main/java/org/enginehub/linbus/dfu/LinOps.java

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
import java.nio.IntBuffer;
4545
import java.nio.LongBuffer;
4646
import java.util.ArrayList;
47-
import java.util.Arrays;
47+
import java.util.Iterator;
4848
import java.util.List;
4949
import java.util.Map.Entry;
5050
import java.util.function.BiConsumer;
@@ -86,7 +86,7 @@ public LinTag<?> emptyList() {
8686

8787
@Override
8888
public LinTag<?> emptyMap() {
89-
return LinCompoundTag.builder().build();
89+
return LinCompoundTag.empty();
9090
}
9191

9292
@Override
@@ -99,12 +99,18 @@ public <U> U convertTo(DynamicOps<U> outOps, LinTag<?> input) {
9999
case LinLongTag tag -> outOps.createLong(tag.valueAsLong());
100100
case LinFloatTag tag -> outOps.createFloat(tag.valueAsFloat());
101101
case LinDoubleTag tag -> outOps.createDouble(tag.valueAsDouble());
102-
case LinByteArrayTag tag -> outOps.createByteList(tag.view());
103102
case LinStringTag tag -> outOps.createString(tag.value());
104103
case LinListTag<?> tag -> convertList(outOps, tag);
105104
case LinCompoundTag tag -> convertMap(outOps, tag);
106-
case LinIntArrayTag tag -> outOps.createIntList(Arrays.stream(tag.value()));
107-
case LinLongArrayTag tag -> outOps.createLongList(Arrays.stream(tag.value()));
105+
case LinByteArrayTag tag -> outOps.createByteList(tag.view());
106+
case LinIntArrayTag tag -> {
107+
IntBuffer view = tag.view();
108+
yield outOps.createIntList(IntStream.range(0, view.limit()).map(view::get));
109+
}
110+
case LinLongArrayTag tag -> {
111+
LongBuffer view = tag.view();
112+
yield outOps.createLongList(IntStream.range(0, view.limit()).mapToLong(view::get));
113+
}
108114
};
109115
}
110116

@@ -293,21 +299,27 @@ public DataResult<LinTag<?>> mergeToMap(LinTag<?> map, MapLike<LinTag<?>> values
293299
return DataResult.error(() -> "mergeToMap called with non-map: " + map, map);
294300
}
295301
LinCompoundTag.Builder output = builderFrom(map);
296-
List<LinTag<?>> missed = new ArrayList<>();
302+
List<LinTag<?>> missed = null;
303+
Iterator<Pair<LinTag<?>, LinTag<?>>> entries = values.entries().iterator();
297304
try {
298-
values.entries().forEach(entry -> {
305+
while (entries.hasNext()) {
306+
Pair<LinTag<?>, LinTag<?>> entry = entries.next();
299307
LinTag<?> key = entry.getFirst();
300308
if (key instanceof LinStringTag stringKey) {
301309
output.put(stringKey.value(), entry.getSecond());
302310
} else {
311+
if (missed == null) {
312+
missed = new ArrayList<>();
313+
}
303314
missed.add(key);
304315
}
305-
});
316+
}
306317
} catch (IllegalArgumentException e) {
307318
return DataResult.error(e::getMessage);
308319
}
309-
if (!missed.isEmpty()) {
310-
return DataResult.error(() -> "some keys are not strings: " + missed, output.build());
320+
if (missed != null) {
321+
List<LinTag<?>> missedKeys = missed;
322+
return DataResult.error(() -> "some keys are not strings: " + missedKeys, output.build());
311323
}
312324
return DataResult.success(output.build());
313325
}
@@ -368,10 +380,18 @@ public DataResult<Stream<LinTag<?>>> getStream(LinTag<?> input) {
368380
IntStream.range(0, values.limit()).mapToObj(i -> LinByteTag.of(values.get(i)))
369381
);
370382
}
371-
case LinIntArrayTag tag ->
372-
DataResult.success(Arrays.stream(tag.value()).mapToObj(value -> (LinTag<?>) LinIntTag.of(value)));
373-
case LinLongArrayTag tag ->
374-
DataResult.success(Arrays.stream(tag.value()).mapToObj(value -> (LinTag<?>) LinLongTag.of(value)));
383+
case LinIntArrayTag tag -> {
384+
IntBuffer values = tag.view();
385+
yield DataResult.success(
386+
IntStream.range(0, values.limit()).mapToObj(i -> (LinTag<?>) LinIntTag.of(values.get(i)))
387+
);
388+
}
389+
case LinLongArrayTag tag -> {
390+
LongBuffer values = tag.view();
391+
yield DataResult.success(
392+
IntStream.range(0, values.limit()).mapToObj(i -> (LinTag<?>) LinLongTag.of(values.get(i)))
393+
);
394+
}
375395
default -> DataResult.error(() -> "Not a list");
376396
};
377397
}
@@ -395,7 +415,8 @@ public LinTag<?> createByteList(ByteBuffer input) {
395415
@Override
396416
public DataResult<IntStream> getIntStream(LinTag<?> input) {
397417
if (input instanceof LinIntArrayTag tag) {
398-
return DataResult.success(Arrays.stream(tag.value()));
418+
IntBuffer view = tag.view();
419+
return DataResult.success(IntStream.range(0, view.limit()).map(view::get));
399420
}
400421
return DynamicOps.super.getIntStream(input);
401422
}
@@ -408,7 +429,8 @@ public LinTag<?> createIntList(IntStream input) {
408429
@Override
409430
public DataResult<LongStream> getLongStream(LinTag<?> input) {
410431
if (input instanceof LinLongArrayTag tag) {
411-
return DataResult.success(Arrays.stream(tag.value()));
432+
LongBuffer view = tag.view();
433+
return DataResult.success(IntStream.range(0, view.limit()).mapToLong(view::get));
412434
}
413435
return DynamicOps.super.getLongStream(input);
414436
}

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
javafx = "22.0.2"
33
tinylog = "2.7.0"
44
crankcase = "0.1.1"
5+
jmh = "1.37"
56

67
[plugins]
78
crankcase-java = { id = "org.enginehub.crankcase.java", version.ref = "crankcase" }
@@ -10,6 +11,7 @@ crankcase-licensing = { id = "org.enginehub.crankcase.licensing", version.ref =
1011
crankcase-publishing = { id = "org.enginehub.crankcase.publishing", version.ref = "crankcase" }
1112
crankcase-release = { id = "org.enginehub.crankcase.release", version.ref = "crankcase" }
1213
osdetector = { id = "com.google.osdetector", version = "1.7.3" }
14+
jmh = { id = "me.champeau.jmh", version = "0.7.3" }
1315

1416
[libraries]
1517
crankcase-checkstyle = { module = "org.enginehub.crankcase:checkstyle", version.ref = "crankcase" }

tree/build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ plugins {
22
id("org.enginehub.lin-bus.java-library-conventions")
33
alias(libs.plugins.crankcase.licensing)
44
alias(libs.plugins.crankcase.publishing)
5+
alias(libs.plugins.jmh)
56
}
67

78
dependencies {
@@ -15,6 +16,15 @@ dependencies {
1516
}
1617
}
1718

19+
jmh {
20+
jmhVersion = libs.versions.jmh
21+
fork = 2
22+
warmupIterations = 3
23+
warmup = "1s"
24+
iterations = 5
25+
timeOnIteration = "1s"
26+
}
27+
1828
publishing {
1929
publications {
2030
create<MavenPublication>("maven") {

tree/src/jmh/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# CompoundValueMap benchmarks
2+
3+
JMH benchmarks for the production `CompoundValueMap`, the open-addressed map backing
4+
`LinCompoundTag`. The benchmarks live in the `org.enginehub.linbus.tree` package
5+
so they can construct the package-private map directly.
6+
7+
`CompoundValueMapBenchmark` covers the map's hot paths across several sizes:
8+
9+
- `getHit` tests a successful get with a reused key (its `String.hashCode` is cached after the first lookup)
10+
- `getMiss` tests an unsuccessful get with a reused key
11+
- `getHitFreshKey` / `getMissFreshKey` build a new `String` per lookup so the hash is uncached, forcing the map to
12+
rehash the key every time
13+
- `construct` tests the time to build the copy of another map (the source's keys have cached hashes)
14+
- `constructFreshKey` builds from a source whose keys have uncached hashes, so both maps hash every key during the
15+
copy rather than the `LinkedHashMap` path reusing cached hashes
16+
- `iterate` tests the time to iterate over the map's entries, which is a common operation when serializing
17+
18+
## Running
19+
20+
```sh
21+
# CPU: all JMH benchmarks (fork/warmup/iteration counts come from the jmh { } block in build.gradle.kts)
22+
./gradlew :tree:jmh
23+
24+
# CPU + allocation: add `profilers.add("gc")` to the jmh { } block so the benchmarks report
25+
# `gc.alloc.rate.norm` (bytes allocated per op), or run the jar directly:
26+
# ./gradlew :tree:jmhJar && java -jar tree/build/libs/tree-*-jmh.jar -prof gc
27+
```

0 commit comments

Comments
 (0)