During testing, an interesting observation was made regarding the performance of BigInteger serialization. Replacing the automatically generated Delegate for BigInteger with a custom implementation significantly improved throughput.
Original Delegate Implementation
The original delegate for BigInteger serialization is complex, handling internal fields like signum, mag, bitCountPlusOne, etc., using low-level operations.
public final class Delegate0_BigInteger extends MagicAccessorImpl implements Delegate {
private final Map fields;
public Delegate0_BigInteger(Map var1) {
this.fields = var1;
}
public final void calcSize(Object var1, CalcSizeStream var2) throws IOException {
BigInteger var3 = (BigInteger)var1;
var2.writeObject(var3.mag);
var2.count += 20;
}
public final void write(Object var1, DataStream var2) throws IOException {
BigInteger var3 = (BigInteger)var1;
var2.writeInt(var3.signum);
var2.writeObject(var3.mag);
var2.writeInt(var3.bitCountPlusOne);
var2.writeInt(var3.bitLengthPlusOne);
var2.writeInt(var3.lowestSetBitPlusTwo);
var2.writeInt(var3.firstNonzeroIntNumPlusTwo);
}
public final Object read(DataStream var1) throws IOException, ClassNotFoundException {
BigInteger var10002 = new BigInteger();
var1.register(var10002);
JavaInternals.putInt(var10002, var1.readInt(), 12L);
JavaInternals.putObject(var10002, (int[])var1.readObject(), 32L);
var10002.bitCountPlusOne = var1.readInt();
var10002.bitLengthPlusOne = var1.readInt();
var10002.lowestSetBitPlusTwo = var1.readInt();
var10002.firstNonzeroIntNumPlusTwo = var1.readInt();
return var10002;
}
public final void skip(DataStream var1) throws IOException, ClassNotFoundException {
var1.skipBytes(4);
var1.readObject();
var1.skipBytes(16);
}
public final void toJson(Object var1, StringBuilder var2) throws IOException {
BigInteger var3 = (BigInteger)var1;
Json.appendObject(var2.append("{\"signum\":").append(var3.signum).append(",\"mag\":"), var3.mag);
var2.append(",\"bitCountPlusOne\":").append(var3.bitCountPlusOne).append(",\"bitLengthPlusOne\":").append(var3.bitLengthPlusOne).append(",\"lowestSetBitPlusTwo\":").append(var3.lowestSetBitPlusTwo).append(",\"firstNonzeroIntNumPlusTwo\":").append(var3.firstNonzeroIntNumPlusTwo).append('}');
}
public final Object fromJson(JsonReader var1) throws IOException, ClassNotFoundException {
var1.expect(123, "Expected object");
BigInteger var10000 = new BigInteger();
if (var1.skipWhitespace() != 125) {
while (true) {
var1.skipWhitespace();
var1.expect(58, "Expected key-value pair");
var1.skipWhitespace();
switch (var1.readString()) {
case "signum":
JavaInternals.putInt(var10000, var1.readInt(), 12L);
break;
case "mag":
JavaInternals.putObject(var10000, var1.next == 110 ? var1.readNull() : (int[])var1.readObject(int[].class), 32L);
break;
case "lowestSetBitPlusTwo":
var10000.lowestSetBitPlusTwo = var1.readInt();
break;
case "firstNonzeroIntNumPlusTwo":
var10000.firstNonzeroIntNumPlusTwo = var1.readInt();
break;
case "bitLengthPlusOne":
var10000.bitLengthPlusOne = var1.readInt();
break;
case "bitCountPlusOne":
var10000.bitCountPlusOne = var1.readInt();
break;
default:
var1.readObject();
}
if (var1.skipWhitespace() == 125) {
break;
}
var1.expect(44, "Unexpected end of object");
var1.skipWhitespace();
}
}
var1.read();
return var10000;
}
}
Custom BigInteger Serializer
The custom serializer simplifies the process by converting BigInteger to a string for serialization, reducing complexity and improving performance.
public class BigIntegerSerializer extends Serializer<BigInteger> {
protected BigIntegerSerializer() {
super(BigInteger.class);
}
@Override
public void calcSize(BigInteger value, CalcSizeStream css) throws IOException {
css.count += Utf8.length(value.toString());
}
@Override
public void write(BigInteger value, DataStream out) throws IOException {
out.writeUTF(value.toString());
}
@Override
public BigInteger read(DataStream in) throws IOException, ClassNotFoundException {
BigInteger value = new BigInteger((String) in.readUTF());
return value;
}
@Override
public void skip(DataStream in) throws IOException, ClassNotFoundException {
in.skipBytes(in.readInt());
}
@Override
public void toJson(BigInteger obj, StringBuilder builder) throws IOException {
// Not implemented
}
@Override
public BigInteger fromJson(JsonReader in) throws IOException, ClassNotFoundException {
return null; // Not implemented
}
}
Benchmark Setup
@Fork(1)
@Threads(8)
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 3)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class BenchmarkRunner {
private static final BigInteger VALUE = BigInteger.valueOf(100500);
@Benchmark
public BigInteger test() {
try (PersistStream out = new PersistStream()) {
out.writeObject(VALUE);
byte[] buf = out.toByteArray();
DeserializeStream in = new DeserializeStream(buf);
return (BigInteger) in.readObject();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(BenchmarkRunner.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
I also have a branch where I replaced Unsafe to work with serialization with VarHandle
Performance Results
The benchmark results compare the original delegate and the custom serializer across different JVMs and configurations (Unsafe vs. VarHandle).
Summary Table
| Implementation |
Oracle JVM 23 (Unsafe) |
GraalVM 24 (Unsafe) |
Oracle JVM 23 (VarHandle) |
GraalVM 24 (VarHandle) |
| Generated Delegate |
30,788 ops/µs |
Not supported |
28,787 ops/µs |
Not supported |
| BigIntegerSerializer |
78,475 ops/µs |
120,004 ops/µs |
72,450 ops/µs |
136,164 ops/µs |
BigDecimal Results
For BigDecimal, the performance difference is even more pronounced (Oracle JVM 23, Unsafe):
- Custom Serializer: 90,517 ops/µs
- Generated Delegate: 23,834 ops/µs
This is a very simple test and may not show some nuances that I missed. The question here is whether it makes sense to abandon the delegate generation within the library and make a manual version...
During testing, an interesting observation was made regarding the performance of
BigIntegerserialization. Replacing the automatically generatedDelegateforBigIntegerwith a custom implementation significantly improved throughput.Original Delegate Implementation
The original delegate for
BigIntegerserialization is complex, handling internal fields likesignum,mag,bitCountPlusOne, etc., using low-level operations.Custom BigInteger Serializer
The custom serializer simplifies the process by converting
BigIntegerto a string for serialization, reducing complexity and improving performance.Benchmark Setup
I also have a branch where I replaced Unsafe to work with serialization with VarHandle
Performance Results
The benchmark results compare the original delegate and the custom serializer across different JVMs and configurations (
Unsafevs.VarHandle).Summary Table
BigDecimal Results
For
BigDecimal, the performance difference is even more pronounced (Oracle JVM 23, Unsafe):This is a very simple test and may not show some nuances that I missed. The question here is whether it makes sense to abandon the delegate generation within the library and make a manual version...