Skip to content

Performance Analysis of BigInteger/BigDecimal Serialization #102

@PRIESt512

Description

@PRIESt512

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...

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions