Skip to content
This repository was archived by the owner on Jan 26, 2026. It is now read-only.

Full Comprehensive Comparison Tests#9

Merged
expanded-for-real merged 31 commits intomainfrom
dev
Jun 5, 2025
Merged

Full Comprehensive Comparison Tests#9
expanded-for-real merged 31 commits intomainfrom
dev

Conversation

@expanded-for-real
Copy link
Collaborator

Satisfies most requirements in - #7. Note that I'm excluding Thrift for a later PR since we have enough data now to proceed. Also excluded Blackbird from Jackson for now but can add later.

expanded-for-real and others added 26 commits June 1, 2025 13:23
…mance tracking; add comprehensive String benchmark
Try to enhance string deserialization
A full list of enhancements can be found here - #3
* Full comprehensive comparison tests with a lot of other libraries + some micro-optimizations added that were found along the way

* replace deprecated gradle methods with latest

---------

Co-authored-by: expand3d <>
# Conflicts:
#	src/jmh/java/com/imprint/benchmark/ComparisonBenchmark.java
#	src/main/java/com/imprint/core/ImprintRecord.java
#	src/main/java/com/imprint/types/TypeHandler.java
#	src/main/java/com/imprint/types/Value.java
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CI file should trigger a Comprehensive benchmark run on merges to main and add them as a report

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of changes in the build.gradle to add other frameworks we're comparing against; includes protobuf and flatbuffers which requires major gradle work as demonstrated below

}
}

// Download and setup FlatBuffers compiler for Linux (CI environment)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to download a C based flatbuffer compiler to the CI server in order to run the comparisons during the merge process.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flatbuffer schema file

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

protobuf schema file

public ImprintRecord(Header header, List<DirectoryEntry> directory, ByteBuffer payload) {
this.header = Objects.requireNonNull(header, "Header cannot be null");
this.directory = List.copyOf(Objects.requireNonNull(directory, "Directory cannot be null"));
this.directory = Collections.unmodifiableList(Objects.requireNonNull(directory, "Directory cannot be null"));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor performance gain

private final byte[] value;

public BytesValue(byte[] value) {
this.value = value.clone(); // defensive copy
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing debug comment notes to myself

private volatile String cachedString; // lazy decode
private volatile String cachedString;

private static final int THREAD_LOCAL_BUFFER_SIZE = 1024;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried and true thread local buffer pool for Strings. It doesn't make a huge difference in micro benchmarks of ~5 iterations but the time saved over larger operations can be significant

Value deserialize(ByteBuffer buffer) throws ImprintException;
void serialize(Value value, ByteBuffer buffer) throws ImprintException;
int estimateSize(Value value) throws ImprintException;
ByteBuffer readValueBytes(ByteBuffer buffer) throws ImprintException;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agavra I looked at the Rust implementation again you were correct, the previous version for readValueBytes was a bit wonky. I had added this originally since I wanted each type to define it but it's not needed anymore since we'll let the ImprintRecord set these boundaries through the Directory, even for nested values which had originally been causing me trouble. So pretty much all of this can be removed from the interface and underlying

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lot of test setup here between all the frameworks

bh.consume(result);
}

// ===== DESERIALIZATION BENCHMARKS =====
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imprint and Flatbuffers are kind of unfairly compared against the rest of the frameworks on this one since we're not actually deserializing anything and just setting it up, whereas Jackson for instance is deserializing straight to types. I plan on setting up additional fair tests in the next MR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imprint was designed to have zero-overhead deserialization and intentionally sacrifices the cost to deserialize an entire record, since it's larger (which happens pretty rarely in data pipelines). For fairness we can (and should) publish the results for full deserialization, but it's important to keep in mind what we care about when it comes to optimizations.

}

// ===== FIELD ACCESS BENCHMARKS =====
// Tests accessing a single field near the end of a large record
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are more accurate, at least in the sense of achieving the same end goal even though Imprint and Flatbuffers are still doing a lot less work


// ===== HELPER METHODS =====

private void setupAvro() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String blocks aren't until Java 15.....

return messagePackMapper.writeValueAsBytes(data);
}

private byte[] serializeWithAvro(TestRecord data) throws Exception {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avro setup is kind of annoying

return avroReader.read(null, decoder);
}

private byte[] serializeWithProtobuf(TestRecord data) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Protobuf setup is more annoying

return builder.build().toByteArray();
}

private ByteBuffer serializeWithFlatBuffers(TestRecord data) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if there's an easier way to do this but my God Flatbuffers is impossible to deal with

}

//Single allocation instead of duplicate + slice
var fieldBuffer = payload.duplicate();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

realized here on profiling that slice isn't actually needed.

…es in gradle file. Also fix permission issue
@github-actions
Copy link

github-actions bot commented Jun 5, 2025

Benchmark Results

Benchmark execution completed but no results file was found. Check the workflow logs for details.

@github-actions
Copy link

github-actions bot commented Jun 5, 2025

📊 Benchmark Results

Benchmark execution completed but no results file was found. Check the workflow logs for details.

@expanded-for-real
Copy link
Collaborator Author

@agavra results here - #7. Sorry for tagging you in multiple places

Copy link
Contributor

@agavra agavra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really cool, thanks for running all these benchmarks!

At first I was concerned when seeing the results of the merge benchmark then I remembered we haven't implemented the merge algorithm in Java yet 😅 let's make sure to do that before we publish the benchmarks in docs anywhere.

I think the other important benchmark that isn't covered here is "project + serialize". Flatbuffers does a really good job if you're projecting fields, but if you need to project it into a smaller record schema you actually have to reserialize everything you're projecting out. (Basically imagine you have record with fields [id, name, company, email, age, ...] and you only care about [id, name, email] so you want to serialize just those three fields and send it to a downstream application.

Re: serialization is expected to be a little slower with Imprint since typically you aren't serializing the entire records within a pipeline, though I am surprised that it's so much slower. That would be worth looking into to see if we're missing anything.

Lastly, note that for the comparison benchmarks the larger the record the better Imprint fairs. So it's actually interesting to test merge/project with varying record sizes (both number of fields as well as size of the fields, in particular the latter).

Comment on lines +98 to +101
- name: Run size comparison benchmarks
run: |
./gradlew jmhRunSizeComparisonBenchmarks
continue-on-error: true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should run the comparison benchmarks on CI - they can be run on demand since we don't expect the performance of other systems to change when we commit to Imprint and the Imprint-specific benchmarks should catch regressions (which is the point of running things on each PR).

Not only does this slow down the PR builds, but I think there's some limit to the free GH plan on how many minutes you can run workflows for (though I need to double check that, it may only apply to private repos)

Copy link
Collaborator Author

@expanded-for-real expanded-for-real Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes my life way easier lol. I can reduce it to just a gradle task for ease of use to run locally and remove all the complex custom tasking

}

if (latestFile) {
console.log(`📊 Found benchmark results: ${latestFile}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend having the minimal amount of scripting inside ci.yml and instead delegate it to a script that we can just run locally. See https://github.com/imprint-serde/imprint/blob/main/scripts/ci_bench.sh for an example, I generate the markdown within the script there and just call that script from the GHA workflow

bh.consume(result);
}

// ===== DESERIALIZATION BENCHMARKS =====
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imprint was designed to have zero-overhead deserialization and intentionally sacrifices the cost to deserialize an entire record, since it's larger (which happens pretty rarely in data pipelines). For fairness we can (and should) publish the results for full deserialization, but it's important to keep in mind what we care about when it comes to optimizations.

var usedFieldIds = new HashSet<Integer>();

// Copy fields from first record (takes precedence)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're publishing any of these benchmarks we should make sure to actually implement the merge algorithm 😉 if we need to deserialize/reserialize the entire two records that defeats the purpose of imprint

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to remove the merge comparisons for now. I'll create a task and start merge algo next and can always come back to them

@expanded-for-real expanded-for-real merged commit dc6d2de into main Jun 5, 2025
10 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants