Skip to content

Commit 24f140f

Browse files
committed
Add experimental key-based RCB alternative for less nested types
1 parent 20b3a8f commit 24f140f

File tree

6 files changed

+378
-0
lines changed

6 files changed

+378
-0
lines changed

build.gradle

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ println "Building: $version"
8181
sourceSets {
8282
stream {}
8383
streamIntermediary {}
84+
jmh {}
8485
}
8586

8687
java {
@@ -111,6 +112,11 @@ dependencies {
111112
api 'com.mojang:datafixerupper:7.0.14'
112113
api 'org.slf4j:slf4j-api:2.0.1'
113114

115+
jmhCompileOnly cLibs.bundles.compileonly
116+
jmhImplementation project(':')
117+
jmhImplementation 'org.openjdk.jmh:jmh-core:1.37'
118+
jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
119+
114120
testCompileOnly cLibs.bundles.compileonly
115121

116122
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
@@ -148,6 +154,15 @@ dependencies {
148154
}
149155
}
150156

157+
tasks.register('jmh', JavaExec) {
158+
group = 'benchmark'
159+
dependsOn sourceSets.jmh.output
160+
mainClass = 'org.openjdk.jmh.Main'
161+
systemProperty 'jmh.executor', 'VIRTUAL'
162+
systemProperty 'jmh.blackhole.mode', 'COMPILER'
163+
classpath = sourceSets.jmh.runtimeClasspath
164+
}
165+
151166
streamJar {
152167
manifest.attributes('FMLModType': 'GAMELIBRARY')
153168
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package dev.lukebemish.codecextras.jmh;
2+
3+
import com.google.gson.JsonElement;
4+
import com.mojang.serialization.JsonOps;
5+
import java.util.concurrent.TimeUnit;
6+
import java.util.concurrent.atomic.AtomicInteger;
7+
import org.openjdk.jmh.annotations.Benchmark;
8+
import org.openjdk.jmh.annotations.BenchmarkMode;
9+
import org.openjdk.jmh.annotations.Measurement;
10+
import org.openjdk.jmh.annotations.Mode;
11+
import org.openjdk.jmh.annotations.OutputTimeUnit;
12+
import org.openjdk.jmh.annotations.Scope;
13+
import org.openjdk.jmh.annotations.State;
14+
import org.openjdk.jmh.annotations.Warmup;
15+
import org.openjdk.jmh.infra.Blackhole;
16+
17+
@Measurement(time = 5)
18+
@Warmup(time = 5)
19+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
20+
@BenchmarkMode(Mode.Throughput)
21+
@State(value = Scope.Benchmark)
22+
public class LargeRecordsDecode {
23+
private final AtomicInteger counter = new AtomicInteger(0);
24+
25+
@Benchmark
26+
public void recordCodecBuilder(Blackhole blackhole) {
27+
JsonElement json = TestRecord.makeData(counter.getAndIncrement());
28+
var result = TestRecord.RCB.decode(JsonOps.INSTANCE, json);
29+
blackhole.consume(result.result().orElseThrow());
30+
}
31+
32+
@Benchmark
33+
public void keyedRecordCodecBuilder(Blackhole blackhole) {
34+
JsonElement json = TestRecord.makeData(counter.getAndIncrement());
35+
var result = TestRecord.KRCB.decode(JsonOps.INSTANCE, json);
36+
blackhole.consume(result.result().orElseThrow());
37+
}
38+
39+
@Benchmark
40+
public void extendedRecordCodecBuilder(Blackhole blackhole) {
41+
JsonElement json = TestRecord.makeData(counter.getAndIncrement());
42+
var result = TestRecord.ERCB.decode(JsonOps.INSTANCE, json);
43+
blackhole.consume(result.result().orElseThrow());
44+
}
45+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package dev.lukebemish.codecextras.jmh;
2+
3+
import com.mojang.serialization.JsonOps;
4+
import java.util.concurrent.TimeUnit;
5+
import java.util.concurrent.atomic.AtomicInteger;
6+
import org.openjdk.jmh.annotations.Benchmark;
7+
import org.openjdk.jmh.annotations.BenchmarkMode;
8+
import org.openjdk.jmh.annotations.Measurement;
9+
import org.openjdk.jmh.annotations.Mode;
10+
import org.openjdk.jmh.annotations.OutputTimeUnit;
11+
import org.openjdk.jmh.annotations.Scope;
12+
import org.openjdk.jmh.annotations.State;
13+
import org.openjdk.jmh.annotations.Warmup;
14+
import org.openjdk.jmh.infra.Blackhole;
15+
16+
@Measurement(time = 5)
17+
@Warmup(time = 5)
18+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
19+
@BenchmarkMode(Mode.Throughput)
20+
@State(value = Scope.Benchmark)
21+
public class LargeRecordsEncode {
22+
private final AtomicInteger counter = new AtomicInteger(0);
23+
24+
@Benchmark
25+
public void recordCodecBuilder(Blackhole blackhole) {
26+
TestRecord object = TestRecord.makeRecord(counter.getAndIncrement());
27+
var result = TestRecord.RCB.encodeStart(JsonOps.INSTANCE, object);
28+
blackhole.consume(result.result().orElseThrow());
29+
}
30+
31+
@Benchmark
32+
public void keyedRecordCodecBuilder(Blackhole blackhole) {
33+
TestRecord object = TestRecord.makeRecord(counter.getAndIncrement());
34+
var result = TestRecord.KRCB.encodeStart(JsonOps.INSTANCE, object);
35+
blackhole.consume(result.result().orElseThrow());
36+
}
37+
38+
@Benchmark
39+
public void extendedRecordCodecBuilder(Blackhole blackhole) {
40+
TestRecord object = TestRecord.makeRecord(counter.getAndIncrement());
41+
var result = TestRecord.ERCB.encodeStart(JsonOps.INSTANCE, object);
42+
blackhole.consume(result.result().orElseThrow());
43+
}
44+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package dev.lukebemish.codecextras.jmh;
2+
3+
import com.google.gson.JsonObject;
4+
import com.mojang.serialization.Codec;
5+
import com.mojang.serialization.codecs.RecordCodecBuilder;
6+
import dev.lukebemish.codecextras.ExtendedRecordCodecBuilder;
7+
import dev.lukebemish.codecextras.KeyedRecordCodecBuilder;
8+
9+
record TestRecord(
10+
int a, int b, int c, int d,
11+
int e, int f, int g, int h,
12+
int i, int j, int k, int l,
13+
int m, int n, int o, int p
14+
) {
15+
public static final Codec<TestRecord> RCB = RecordCodecBuilder.create(i -> i.group(
16+
Codec.INT.fieldOf("a").forGetter(TestRecord::a),
17+
Codec.INT.fieldOf("b").forGetter(TestRecord::b),
18+
Codec.INT.fieldOf("c").forGetter(TestRecord::c),
19+
Codec.INT.fieldOf("d").forGetter(TestRecord::d),
20+
Codec.INT.fieldOf("e").forGetter(TestRecord::e),
21+
Codec.INT.fieldOf("f").forGetter(TestRecord::f),
22+
Codec.INT.fieldOf("g").forGetter(TestRecord::g),
23+
Codec.INT.fieldOf("h").forGetter(TestRecord::h),
24+
Codec.INT.fieldOf("i").forGetter(TestRecord::i),
25+
Codec.INT.fieldOf("j").forGetter(TestRecord::j),
26+
Codec.INT.fieldOf("k").forGetter(TestRecord::k),
27+
Codec.INT.fieldOf("l").forGetter(TestRecord::l),
28+
Codec.INT.fieldOf("m").forGetter(TestRecord::m),
29+
Codec.INT.fieldOf("n").forGetter(TestRecord::n),
30+
Codec.INT.fieldOf("o").forGetter(TestRecord::o),
31+
Codec.INT.fieldOf("p").forGetter(TestRecord::p)
32+
).apply(i, TestRecord::new));
33+
34+
public static final Codec<TestRecord> ERCB = ExtendedRecordCodecBuilder
35+
.start(Codec.INT.fieldOf("a"), TestRecord::a)
36+
.field(Codec.INT.fieldOf("b"), TestRecord::b)
37+
.field(Codec.INT.fieldOf("c"), TestRecord::c)
38+
.field(Codec.INT.fieldOf("d"), TestRecord::d)
39+
.field(Codec.INT.fieldOf("e"), TestRecord::e)
40+
.field(Codec.INT.fieldOf("f"), TestRecord::f)
41+
.field(Codec.INT.fieldOf("g"), TestRecord::g)
42+
.field(Codec.INT.fieldOf("h"), TestRecord::h)
43+
.field(Codec.INT.fieldOf("i"), TestRecord::i)
44+
.field(Codec.INT.fieldOf("j"), TestRecord::j)
45+
.field(Codec.INT.fieldOf("k"), TestRecord::k)
46+
.field(Codec.INT.fieldOf("l"), TestRecord::l)
47+
.field(Codec.INT.fieldOf("m"), TestRecord::m)
48+
.field(Codec.INT.fieldOf("n"), TestRecord::n)
49+
.field(Codec.INT.fieldOf("o"), TestRecord::o)
50+
.field(Codec.INT.fieldOf("p"), TestRecord::p)
51+
.build(p -> o -> n -> m -> l -> k -> j -> i -> h -> g -> f -> e -> d -> c -> b -> a -> new TestRecord(
52+
a, b, c, d,
53+
e, f, g, h,
54+
i, j, k, l,
55+
m, n, o, p
56+
));
57+
58+
public static Codec<TestRecord> KRCB = KeyedRecordCodecBuilder.codec(i ->
59+
i.with(Codec.INT.fieldOf("a"), TestRecord::a, (aI, aK) ->
60+
aI.with(Codec.INT.fieldOf("b"), TestRecord::b, (bI, bK) ->
61+
bI.with(Codec.INT.fieldOf("c"), TestRecord::c, (cI, cK) ->
62+
cI.with(Codec.INT.fieldOf("d"), TestRecord::d, (dI, dK) ->
63+
dI.with(Codec.INT.fieldOf("e"), TestRecord::e, (eI, eK) ->
64+
eI.with(Codec.INT.fieldOf("f"), TestRecord::f, (fI, fK) ->
65+
fI.with(Codec.INT.fieldOf("g"), TestRecord::g, (gI, gK) ->
66+
gI.with(Codec.INT.fieldOf("h"), TestRecord::h, (hI, hK) ->
67+
hI.with(Codec.INT.fieldOf("i"), TestRecord::i, (iI, iK) ->
68+
iI.with(Codec.INT.fieldOf("j"), TestRecord::j, (jI, jK) ->
69+
jI.with(Codec.INT.fieldOf("k"), TestRecord::k, (kI, kK) ->
70+
kI.with(Codec.INT.fieldOf("l"), TestRecord::l, (lI, lK) ->
71+
lI.with(Codec.INT.fieldOf("m"), TestRecord::m, (mI, mK) ->
72+
mI.with(Codec.INT.fieldOf("n"), TestRecord::n, (nI, nK) ->
73+
nI.with(Codec.INT.fieldOf("o"), TestRecord::o, (oI, oK) ->
74+
oI.with(Codec.INT.fieldOf("p"), TestRecord::p, (pI, pK) ->
75+
pI.build(container ->
76+
new TestRecord(
77+
container.get(aK), container.get(bK), container.get(cK), container.get(dK),
78+
container.get(eK), container.get(fK), container.get(gK), container.get(hK),
79+
container.get(iK), container.get(jK), container.get(kK), container.get(lK),
80+
container.get(mK), container.get(nK), container.get(oK), container.get(pK)
81+
)
82+
)
83+
)
84+
)
85+
)
86+
)
87+
)
88+
)
89+
)
90+
)
91+
)
92+
)
93+
)
94+
)
95+
)
96+
)
97+
)
98+
)
99+
);
100+
101+
public static TestRecord makeRecord(int i) {
102+
return new TestRecord(
103+
i, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7, i + 8, i + 9, i + 10, i + 11, i + 12, i + 13, i + 14, i + 15
104+
);
105+
}
106+
107+
public static JsonObject makeData(int i) {
108+
JsonObject json = new JsonObject();
109+
for (int j = 0; j < 16; j++) {
110+
json.addProperty(Character.toString('a'+j), i+j);
111+
}
112+
return json;
113+
}
114+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package dev.lukebemish.codecextras;
2+
3+
import com.mojang.serialization.Codec;
4+
import com.mojang.serialization.DataResult;
5+
import com.mojang.serialization.DynamicOps;
6+
import com.mojang.serialization.MapCodec;
7+
import com.mojang.serialization.MapLike;
8+
import com.mojang.serialization.RecordBuilder;
9+
import com.mojang.serialization.codecs.RecordCodecBuilder;
10+
import org.jetbrains.annotations.ApiStatus;
11+
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import java.util.function.BiFunction;
15+
import java.util.function.Function;
16+
import java.util.stream.Stream;
17+
18+
/**
19+
* Similar to {@link ExtendedRecordCodecBuilder}, an alternative to {@link RecordCodecBuilder} that allows for any
20+
* number of fields. Unlike {@link ExtendedRecordCodecBuilder}, this does not require massively curried lambdas and so
21+
* is less likely to make IDEs cry, and may be slightly faster in some scenarios; the tradeoff is some particularly
22+
* nested lambdas to build it.
23+
*/
24+
@ApiStatus.Experimental
25+
public final class KeyedRecordCodecBuilder<A> {
26+
private final List<Field<A, ?>> fields;
27+
28+
private KeyedRecordCodecBuilder(List<Field<A, ?>> fields) {
29+
this.fields = fields;
30+
}
31+
32+
public static final class Key<T> {
33+
private final int count;
34+
35+
private Key(int i) {
36+
this.count = i;
37+
}
38+
}
39+
40+
public static final class Built<A> {
41+
private final KeyedRecordCodecBuilder<A> builder;
42+
private final Function<Container, DataResult<A>> function;
43+
44+
private Built(KeyedRecordCodecBuilder<A> builder, Function<Container, DataResult<A>> function) {
45+
this.builder = builder;
46+
this.function = function;
47+
}
48+
}
49+
50+
public static final class Container {
51+
private final Object[] array;
52+
53+
private Container(Object[] array) {
54+
this.array = array;
55+
}
56+
57+
@SuppressWarnings("unchecked")
58+
public <T> T get(Key<T> key) {
59+
return (T) array[key.count];
60+
}
61+
}
62+
63+
private record Field<A, T>(Key<T> key, Function<A, T> getter, MapCodec<T> partial) {}
64+
65+
public static <A> Codec<A> codec(Function<KeyedRecordCodecBuilder<A>, Built<A>> function) {
66+
return mapCodec(function).codec();
67+
}
68+
69+
public static <A> MapCodec<A> mapCodec(Function<KeyedRecordCodecBuilder<A>, Built<A>> function) {
70+
KeyedRecordCodecBuilder<A> builder = new KeyedRecordCodecBuilder<>(List.of());
71+
Built<A> built = function.apply(builder);
72+
return new MapCodec<>() {
73+
@Override
74+
public <T> RecordBuilder<T> encode(A input, DynamicOps<T> ops, RecordBuilder<T> prefix) {
75+
for (Field<A, ?> field : built.builder.fields) {
76+
prefix = encodePartial(input, ops, prefix, field);
77+
}
78+
return prefix;
79+
}
80+
81+
private <T, P> RecordBuilder<T> encodePartial(A input, DynamicOps<T> ops, RecordBuilder<T> prefix, Field<A, P> field) {
82+
P value = field.getter.apply(input);
83+
return field.partial.encode(value, ops, prefix);
84+
}
85+
86+
@Override
87+
public <T> DataResult<A> decode(DynamicOps<T> ops, MapLike<T> input) {
88+
Container container = new Container(new Object[built.builder.fields.size()]);
89+
for (Field<A, ?> field : built.builder.fields) {
90+
decodePartial(ops, input, container, field);
91+
}
92+
return built.function.apply(container);
93+
}
94+
95+
private <T, P> void decodePartial(DynamicOps<T> ops, MapLike<T> input, Container container, Field<A, P> field) {
96+
DataResult<P> result = field.partial.decode(ops, input);
97+
result.result().ifPresent(value -> container.array[field.key.count] = value);
98+
}
99+
100+
@Override
101+
public <T> Stream<T> keys(DynamicOps<T> ops) {
102+
return built.builder.fields.stream().flatMap(field -> field.partial.keys(ops));
103+
}
104+
};
105+
}
106+
107+
public Built<A> flatBuild(Function<Container, DataResult<A>> function) {
108+
return new Built<>(this, function);
109+
}
110+
111+
public Built<A> build(Function<Container, A> function) {
112+
return flatBuild(function.andThen(DataResult::success));
113+
}
114+
115+
public <T> Built<A> with(MapCodec<T> partial, Function<A, T> getter, BiFunction<KeyedRecordCodecBuilder<A>, Key<T>, Built<A>> rest) {
116+
Key<T> key = new Key<>(fields.size());
117+
Field<A, T> field = new Field<>(key, getter, partial);
118+
List<Field<A, ?>> newFields = new ArrayList<>(fields);
119+
newFields.add(field);
120+
KeyedRecordCodecBuilder<A> newBuilder = new KeyedRecordCodecBuilder<>(newFields);
121+
return rest.apply(newBuilder, key);
122+
}
123+
}

0 commit comments

Comments
 (0)