-
Notifications
You must be signed in to change notification settings - Fork 74
feat: eliminate GenericDatum in Avro reader for performance #374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
eae14b1 to
e7b55b2
Compare
Replace GenericDatum intermediate layer with direct Avro decoder access to improve manifest I/O performance. Changes: - Add avro_direct_decoder_internal.h with DecodeAvroToBuilder API - Add avro_direct_decoder.cc implementing direct Avro→Arrow decoding - Primitive types: bool, int, long, float, double, string, binary, fixed - Temporal types: date, time, timestamp - Logical types: uuid, decimal (with validation) - Nested types: struct, list, map - Union type handling with bounds checking - Field skipping with proper multi-block handling for arrays/maps - Modify avro_reader.cc to use DataFileReaderBase with direct decoder - Replace DataFileReader<GenericDatum> with DataFileReaderBase - Use decoder.decodeInt(), decodeLong(), etc. directly - Remove GenericDatum allocation and extraction overhead - Update CMakeLists.txt to include new decoder source Validation added: - Union branch bounds checking - Decimal byte width validation (uses schema fixedSize, not calculated) - Decimal precision sufficiency validation - Logical type presence validation - Type mismatch error handling Documentation: - Comprehensive API documentation in header - Schema evolution handling via SchemaProjection explained - Error handling behavior documented - Limitations noted (default values not supported) Performance improvement: - Before: Avro binary → GenericDatum → Extract → Arrow (3 steps) - After: Avro binary → decoder.decodeInt() → Arrow (2 steps) This matches Java implementation which uses Decoder directly via ValueReader interface, avoiding intermediate object allocation. All 173 avro_test cases pass. Issue: apache#332
e7b55b2 to
e54929d
Compare
src/iceberg/avro/avro_reader.cc
Outdated
| ICEBERG_RETURN_UNEXPECTED( | ||
| AppendDatumToBuilder(reader_->readerSchema().root(), *context_->datum_, | ||
| projection_, *read_schema_, context_->builder_.get())); | ||
| DecodeAvroToBuilder(reader_->readerSchema().root(), reader_->decoder(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be better to use a feature flag to enable users to use the old reader just in case there is any bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion!
2334ea3 to
d16b0f1
Compare
|
Could you help add some test cases? The original avro reader is simply a wrapper on top of avro-cpp so existing test cases are very minimal. We need better test coverage to be confident. Perhaps we can use the avro writer to create in-memory avro files (using |
043bb08 to
eae860e
Compare
Add extensive test coverage to validate the direct decoder implementation: - All primitive types (boolean, int, long, float, double, string, binary) - Temporal types (date, time, timestamp) - Complex nested structures (nested structs, lists, maps) - Null handling and optional fields - Large datasets (1000+ rows) - Direct decoder vs GenericDatum comparison tests Add benchmark tool to measure performance improvements: - Benchmarks with various data patterns (primitives, nested, lists, nulls) - Compares direct decoder vs GenericDatum performance - Expected speedup: 1.5x - 2.5x due to eliminated intermediate copies Add feature flag for direct Avro decoder: - ReaderProperties::kAvroUseDirectDecoder (default: true) - Allows fallback to GenericDatum implementation if issues arise - Dual-path implementation with helper functions to reduce code duplication Test results: - 16 comprehensive Avro reader tests (vs 5 before) - 180 total tests in avro_test suite - 100% passing rate This addresses review feedback from wgtmac to provide better test coverage and prove performance improvements of the direct decoder implementation.
eae860e to
4ed80b0
Compare
| Status SkipAvroValue(const ::avro::NodePtr& avro_node, ::avro::Decoder& decoder) { | ||
| switch (avro_node->type()) { | ||
| case ::avro::AVRO_NULL: | ||
| // Nothing to skip |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to call decoder.decodeNull()? Even if nothing todo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point
Address reviewer comment to explicitly call decoder.decodeNull() even though AVRO_NULL has no data to skip. This is more consistent with other type handlers and makes the decoder state handling explicit.
bf67046 to
96b0521
Compare
| SchemaProjection projection_; | ||
| // The avro reader to read the data into a datum. | ||
| std::unique_ptr<::avro::DataFileReader<::avro::GenericDatum>> reader_; | ||
| // The avro reader base - provides direct access to decoder (new path). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rather not call them new or legacy path...
| ToString(avro_node)); | ||
| } | ||
| auto* builder = internal::checked_cast<::arrow::BinaryBuilder*>(array_builder); | ||
| std::vector<uint8_t> bytes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to avoid frequent small object allocation like this? Perhaps we can reuse it by adding a class DecodeContext where a std::vector<uint8_t> scratch object is supposed to be used from it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same for other temp vector and string variables.
| return InvalidArgument("Expected Avro fixed for decimal field, got: {}", | ||
| ToString(avro_node)); | ||
| } | ||
| if (avro_node->logicalType().type() != ::avro::LogicalType::DECIMAL) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we combine the two ifs above just like other branches?
|
|
||
| // Validate byte width is sufficient for precision | ||
| // Max value with P digits: 10^P - 1, needs ceil(P * log(10) / log(256)) bytes | ||
| size_t min_bytes = (decimal_type.precision() * 415) / 1000 + 1; // ceil(P * 0.415) | ||
| if (byte_width < min_bytes) { | ||
| return InvalidArgument( | ||
| "Decimal byte width {} insufficient for precision {}, need at least {} bytes", | ||
| byte_width, decimal_type.precision(), min_bytes); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // Validate byte width is sufficient for precision | |
| // Max value with P digits: 10^P - 1, needs ceil(P * log(10) / log(256)) bytes | |
| size_t min_bytes = (decimal_type.precision() * 415) / 1000 + 1; // ceil(P * 0.415) | |
| if (byte_width < min_bytes) { | |
| return InvalidArgument( | |
| "Decimal byte width {} insufficient for precision {}, need at least {} bytes", | |
| byte_width, decimal_type.precision(), min_bytes); | |
| } |
I don't think we need this check.
| // Handle null fields (fields in projection but not in Avro) | ||
| for (size_t proj_idx = 0; proj_idx < projections.size(); ++proj_idx) { | ||
| const auto& field_projection = projections[proj_idx]; | ||
| if (field_projection.kind == FieldProjection::Kind::kNull) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Return error for other unsupported kind?
|
|
||
| /// \brief Use direct Avro decoder (true) or GenericDatum-based decoder (false). | ||
| /// Default: true (use direct decoder for better performance). | ||
| inline static Entry<bool> kAvroUseDirectDecoder{"avro.use-direct-decoder", true}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would call it read.avro.skip-datum, WDYT?
| WriteAndVerify(schema, expected_string); | ||
| } | ||
|
|
||
| // Skip Fixed and UUID tests for now - they require specific binary encoding |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // Skip Fixed and UUID tests for now - they require specific binary encoding |
| WriteAndVerify(schema, expected_string); | ||
| } | ||
|
|
||
| // Comprehensive tests using in-memory MockFileIO |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // Comprehensive tests using in-memory MockFileIO |
| WriteAndVerify(schema, expected_string); | ||
| } | ||
|
|
||
| TEST_F(AvroReaderTest, MapType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we test a map with a non-string-typed key?
| } | ||
|
|
||
| // Test both direct decoder and GenericDatum paths | ||
| TEST_F(AvroReaderTest, DirectDecoderVsGenericDatum) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would recommend changing it to parameterized test to enable/disable direct decoder for all cases in this file.
| } | ||
| } | ||
|
|
||
| TEST_F(AvroReaderTest, LargeDataset) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that we don't have test for projection. For example, only some columns are selected and they are reordered.
|
BTW, it is possible to add a executable under |
Replace GenericDatum intermediate layer with direct Avro decoder access to improve manifest I/O performance.
Changes:
Performance improvement:
This matches Java implementation which uses Decoder directly via ValueReader interface, avoiding intermediate object allocation.
All avro_test cases pass.
Issue: #332