This is an automated email from the ASF dual-hosted git repository.
pitrou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push:
new a811bcd6b50 GH-50333: [C++][Parquet] Add dense decode path for
FIXED_LEN_BYTE_ARRAY (#50335)
a811bcd6b50 is described below
commit a811bcd6b50755cc489ee43d8ede70cc8b9a7a25
Author: Marcin Krystianc <[email protected]>
AuthorDate: Wed Sep 9 16:57:22 2026 +0200
GH-50333: [C++][Parquet] Add dense decode path for FIXED_LEN_BYTE_ARRAY
(#50335)
### Rationale for this change
For performance reasons, we would like to decode float16 (FLBA) values
directly into the caller-provided buffer.
### What changes are included in this PR?
- Adds a `virtual int Decode(uint8_t* buffer, int max_values)` to
`FLBADecoder` in `encoding.h`. It writes decoded values contiguously into a
caller-owned buffer.
- Implements the new overload for all supported decoders. (The base
implementation throws ParquetException, so unimplemented encodings fail with a
clear message.)
- Replaces the old PARQUET-1508 TODO comment.
### Are these changes tested?
Yes
### Are there any user-facing changes?
No
### PS:
Closes #50333
Authored-by: Marcin Krystianc <[email protected]>
Signed-off-by: Antoine Pitrou <[email protected]>
---
cpp/src/parquet/decoder.cc | 74 +++++++++++++++++-
cpp/src/parquet/encoding.h | 19 ++++-
cpp/src/parquet/encoding_test.cc | 163 +++++++++++++++++++++++++++++++++++++++
3 files changed, 251 insertions(+), 5 deletions(-)
diff --git a/cpp/src/parquet/decoder.cc b/cpp/src/parquet/decoder.cc
index c4d3fe5a8a5..47f2b46ebaa 100644
--- a/cpp/src/parquet/decoder.cc
+++ b/cpp/src/parquet/decoder.cc
@@ -854,7 +854,26 @@ class PlainByteArrayDecoder : public
PlainDecoder<ByteArrayType> {
class PlainFLBADecoder : public PlainDecoder<FLBAType>, public FLBADecoder {
public:
using Base = PlainDecoder<FLBAType>;
+ using Base::Decode; // keep Decode(FixedLenByteArray*, int)
using Base::PlainDecoder;
+
+ // PLAIN-encoded FLBA values are already contiguous in the page buffer, so
+ // decode them with a single memcpy into the caller's buffer. This is the
same
+ // copy used by PlainDecoder<FLBAType>::DecodeArrow, without the builder.
+ int Decode(uint8_t* buffer, int max_values) override {
+ max_values = std::min(max_values, this->num_values_);
+ const int64_t bytes_to_decode = static_cast<int64_t>(this->type_length_) *
max_values;
+ if (bytes_to_decode > this->len_) {
+ ParquetException::EofException();
+ }
+ if (bytes_to_decode > 0) {
+ memcpy(buffer, this->data_, static_cast<size_t>(bytes_to_decode));
+ }
+ this->data_ += bytes_to_decode;
+ this->len_ -= static_cast<int>(bytes_to_decode);
+ this->num_values_ -= max_values;
+ return max_values;
+ }
};
// ----------------------------------------------------------------------
@@ -1431,6 +1450,36 @@ class DictByteArrayDecoderImpl : public
DictDecoderImpl<ByteArrayType> {
}
};
+// Dictionary decoder for FIXED_LEN_BYTE_ARRAY that can decode directly into a
+// caller-owned, densely packed byte buffer. DictDecoderImpl<FLBAType> on its
own
+// does not inherit FLBADecoder, so this thin subclass adds the dense Decode
+// overload (mirroring the DeltaByteArray and ByteStreamSplit FLBA decoders).
+class DictFLBADecoder : public DictDecoderImpl<FLBAType>, public FLBADecoder {
+ public:
+ using Base = DictDecoderImpl<FLBAType>;
+ using Base::Decode; // keep Decode(FixedLenByteArray*, int)
+ using Base::DictDecoderImpl;
+
+ // Read one index per value and copy that dictionary entry's type_length
bytes
+ // contiguously into the caller's buffer. Mirrors DecodeArrow without nulls.
+ int Decode(uint8_t* buffer, int max_values) override {
+ max_values = std::min(max_values, this->num_values_);
+ const auto* dict_values = this->dictionary_->data_as<FLBA>();
+ const int64_t type_length = this->type_length_;
+ for (int i = 0; i < max_values; ++i) {
+ int32_t index;
+ if (ARROW_PREDICT_FALSE(!this->idx_decoder_.Get(&index))) {
+ throw ParquetException("Dict decoding failed");
+ }
+ PARQUET_THROW_NOT_OK(this->IndexInBounds(index));
+ memcpy(buffer + i * type_length, dict_values[index].ptr,
+ static_cast<size_t>(type_length));
+ }
+ this->num_values_ -= max_values;
+ return max_values;
+ }
+};
+
// ----------------------------------------------------------------------
// DELTA_BINARY_PACKED decoder
@@ -2232,6 +2281,23 @@ class DeltaByteArrayFLBADecoder : public
DeltaByteArrayDecoderImpl<FLBAType>,
}
return decoded_values_size;
}
+
+ // Same internal decode as above, but copy the bytes contiguously into the
+ // caller's buffer instead of materializing per-value pointers.
+ int Decode(uint8_t* buffer, int max_values) override {
+ std::vector<ByteArray> decode_byte_array(max_values);
+ const int decoded_values_size = GetInternal(decode_byte_array.data(),
max_values);
+ const uint32_t type_length = static_cast<uint32_t>(this->type_length_);
+
+ for (int i = 0; i < decoded_values_size; i++) {
+ if (ARROW_PREDICT_FALSE(decode_byte_array[i].len != type_length)) {
+ throw ParquetException("Fixed length byte array length mismatch");
+ }
+ memcpy(buffer + static_cast<int64_t>(i) * type_length,
decode_byte_array[i].ptr,
+ type_length);
+ }
+ return decoded_values_size;
+ }
};
// ----------------------------------------------------------------------
@@ -2370,6 +2436,12 @@ class ByteStreamSplitDecoder<FLBAType> : public
ByteStreamSplitDecoderBase<FLBAT
}
return num_decoded;
}
+
+ // DecodeRaw already unsplits the byte streams into a contiguous buffer, so
+ // decode straight into the caller's buffer with no intermediate scratch.
+ int Decode(uint8_t* buffer, int max_values) override {
+ return this->DecodeRaw(buffer, max_values);
+ }
};
} // namespace
@@ -2475,7 +2547,7 @@ std::unique_ptr<Decoder> MakeDictDecoder(Type::type
type_num,
case Type::BYTE_ARRAY:
return std::make_unique<DictByteArrayDecoderImpl>(descr, pool);
case Type::FIXED_LEN_BYTE_ARRAY:
- return std::make_unique<DictDecoderImpl<FLBAType>>(descr, pool);
+ return std::make_unique<DictFLBADecoder>(descr, pool);
default:
break;
}
diff --git a/cpp/src/parquet/encoding.h b/cpp/src/parquet/encoding.h
index e3de4f2aa60..9a0cc55aa18 100644
--- a/cpp/src/parquet/encoding.h
+++ b/cpp/src/parquet/encoding.h
@@ -410,12 +410,23 @@ class BooleanDecoder : virtual public
TypedDecoder<BooleanType> {
class FLBADecoder : virtual public TypedDecoder<FLBAType> {
public:
+ using TypedDecoder<FLBAType>::Decode;
using TypedDecoder<FLBAType>::DecodeSpaced;
- // TODO(wesm): As possible follow-up to PARQUET-1508, we should examine if
- // there is value in adding specialized read methods for
- // FIXED_LEN_BYTE_ARRAY. If only Decimal data can occur with this data type
- // then perhaps not
+ /// \brief Decode values into a densely packed buffer
+ ///
+ /// Unlike Decode(FixedLenByteArray*, int), which writes one pointer per
+ /// value, this writes the raw fixed-width values back to back, with no
+ /// per-value pointers and no gaps.
+ ///
+ /// \param[in] buffer destination for decoded values; caller owns it and
+ /// must size it to at least max_values * descr->type_length() bytes.
+ /// \param[in] max_values max values to decode.
+ /// \return The number of values decoded. Should be identical to max_values
+ /// except at the end of the current data page.
+ ///
+ /// \note API EXPERIMENTAL
+ virtual int Decode(uint8_t* buffer, int max_values) = 0;
};
PARQUET_EXPORT
diff --git a/cpp/src/parquet/encoding_test.cc b/cpp/src/parquet/encoding_test.cc
index 831829e4a21..74b422cb606 100644
--- a/cpp/src/parquet/encoding_test.cc
+++ b/cpp/src/parquet/encoding_test.cc
@@ -2660,4 +2660,167 @@ TEST(DeltaByteArrayEncodingAdHoc, ArrowDirectPut) {
}
}
+// ----------------------------------------------------------------------
+// Dense FIXED_LEN_BYTE_ARRAY decode tests
+//
+// FLBADecoder::Decode(uint8_t*, int) writes decoded values back to back into a
+// densely packed buffer, with no per-value FixedLenByteArray pointers. Verify
+// it for every encoding that overrides it: PLAIN, RLE_DICTIONARY,
+// DELTA_BYTE_ARRAY and BYTE_STREAM_SPLIT.
+
+class TestFLBADenseDecode : public ::testing::Test {
+ public:
+ void SetUp() override {
+ descr_ = ExampleDescr<FLBAType>();
+ type_length_ = descr_->type_length();
+ draws_.resize(kNumValues);
+ GenerateData<FLBA>(kNumValues, draws_.data(), &data_buffer_);
+ }
+
+ // Decode densely and compare each value against the original draw.
+ void CheckDenseDecode(FLBADecoder* decoder, const std::vector<int>&
decode_sizes) {
+ ASSERT_NE(nullptr, decoder);
+ std::vector<uint8_t> dense(static_cast<size_t>(type_length_) * kNumValues);
+
+ int values_decoded = 0;
+ for (int decode_size : decode_sizes) {
+ const int expected_decoded = std::min(decode_size, kNumValues -
values_decoded);
+ ASSERT_EQ(expected_decoded,
+ decoder->Decode(
+ dense.data() + static_cast<int64_t>(values_decoded) *
type_length_,
+ decode_size));
+ values_decoded += expected_decoded;
+ ASSERT_EQ(kNumValues - values_decoded, decoder->values_left());
+ }
+ ASSERT_EQ(kNumValues, values_decoded);
+ ASSERT_EQ(0, decoder->Decode(dense.data(), /*max_values=*/1));
+ ASSERT_EQ(0, decoder->values_left());
+
+ for (int i = 0; i < kNumValues; ++i) {
+ ASSERT_EQ(0, memcmp(dense.data() + static_cast<int64_t>(i) *
type_length_,
+ draws_[i].ptr, type_length_))
+ << "mismatch at value " << i;
+ }
+ }
+
+ void CheckDenseDecode(FLBADecoder* decoder) {
+ CheckDenseDecode(decoder, {1, 17, kNumValues / 2, kNumValues});
+ }
+
+ protected:
+ static constexpr int kNumValues = 1000;
+ int type_length_;
+ std::vector<FLBA> draws_;
+ std::vector<uint8_t> data_buffer_;
+ std::shared_ptr<ColumnDescriptor> descr_;
+};
+
+// These encodings are all built the same way, so exercise them with a single
+// body. RLE_DICTIONARY needs a dictionary and is tested separately below.
+TEST_F(TestFLBADenseDecode, NonDictionaryEncodings) {
+ for (auto encoding :
+ {Encoding::PLAIN, Encoding::DELTA_BYTE_ARRAY,
Encoding::BYTE_STREAM_SPLIT}) {
+ SCOPED_TRACE(EncodingToString(encoding));
+
+ auto encoder =
+ MakeTypedEncoder<FLBAType>(encoding, /*use_dictionary=*/false,
descr_.get());
+ encoder->Put(draws_.data(), kNumValues);
+ auto buffer = encoder->FlushValues();
+
+ auto decoder = MakeTypedDecoder<FLBAType>(encoding, descr_.get());
+ decoder->SetData(kNumValues, buffer->data(),
static_cast<int>(buffer->size()));
+ // EXPECT rather than ASSERT so one failing encoding doesn't hide the
others.
+
EXPECT_NO_FATAL_FAILURE(CheckDenseDecode(dynamic_cast<FLBADecoder*>(decoder.get())));
+ }
+}
+
+TEST_F(TestFLBADenseDecode, Dictionary) {
+ auto base_encoder = MakeEncoder(::parquet::Type::FIXED_LEN_BYTE_ARRAY,
Encoding::PLAIN,
+ /*use_dictionary=*/true, descr_.get());
+ auto encoder = dynamic_cast<TypedEncoder<FLBAType>*>(base_encoder.get());
+ auto dict_traits = dynamic_cast<DictEncoder<FLBAType>*>(base_encoder.get());
+
+ encoder->Put(draws_.data(), kNumValues);
+ auto dict_buffer =
+ AllocateBuffer(default_memory_pool(), dict_traits->dict_encoded_size());
+ dict_traits->WriteDict(dict_buffer->mutable_data());
+ auto indices = encoder->FlushValues();
+
+ auto dict_decoder = MakeTypedDecoder<FLBAType>(Encoding::PLAIN,
descr_.get());
+ dict_decoder->SetData(dict_traits->num_entries(), dict_buffer->data(),
+ static_cast<int>(dict_buffer->size()));
+
+ auto decoder = MakeDictDecoder<FLBAType>(descr_.get());
+ decoder->SetDict(dict_decoder.get());
+ decoder->SetData(kNumValues, indices->data(),
static_cast<int>(indices->size()));
+ // dict_decoder must outlive the decode: the decoded bytes are owned by it.
+
ASSERT_NO_FATAL_FAILURE(CheckDenseDecode(dynamic_cast<FLBADecoder*>(decoder.get())));
+}
+
+// DELTA_BYTE_ARRAY stores each value as a prefix shared with its predecessor
plus
+// a suffix, so the dense decode path has to reconstruct the two halves. The
+// random draws used above hardly ever share a prefix, which leaves that
+// reconstruction untested; use prefixed data here instead.
+TEST_F(TestFLBADenseDecode, DeltaByteArrayPrefixedData) {
+ for (double prefixed_probability : {0.5, 1.0}) {
+ SCOPED_TRACE("prefixed_probability=" +
std::to_string(prefixed_probability));
+ GeneratePrefixedData<FLBA>(kNumValues, draws_.data(), &data_buffer_,
+ prefixed_probability);
+
+ auto encoder = MakeTypedEncoder<FLBAType>(Encoding::DELTA_BYTE_ARRAY,
+ /*use_dictionary=*/false,
descr_.get());
+ encoder->Put(draws_.data(), kNumValues);
+ auto buffer = encoder->FlushValues();
+
+ auto decoder = MakeTypedDecoder<FLBAType>(Encoding::DELTA_BYTE_ARRAY,
descr_.get());
+ decoder->SetData(kNumValues, buffer->data(),
static_cast<int>(buffer->size()));
+
EXPECT_NO_FATAL_FAILURE(CheckDenseDecode(dynamic_cast<FLBADecoder*>(decoder.get())));
+ }
+}
+
+TEST_F(TestFLBADenseDecode, PlainRejectsTruncatedDenseBuffer) {
+ auto encoder =
+ MakeTypedEncoder<FLBAType>(Encoding::PLAIN, /*use_dictionary=*/false,
descr_.get());
+ encoder->Put(draws_.data(), kNumValues);
+ auto buffer = encoder->FlushValues();
+
+ auto decoder = MakeTypedDecoder<FLBAType>(Encoding::PLAIN, descr_.get());
+ decoder->SetData(kNumValues, buffer->data(), static_cast<int>(buffer->size()
- 1));
+
+ std::vector<uint8_t> dense(static_cast<size_t>(type_length_) * kNumValues);
+ ASSERT_THROW(decoder->Decode(dense.data(), kNumValues), ParquetException);
+}
+
+TEST_F(TestFLBADenseDecode, DictionaryRejectsOutOfBoundsDenseIndex) {
+ auto dict_decoder = MakeTypedDecoder<FLBAType>(Encoding::PLAIN,
descr_.get());
+ dict_decoder->SetData(/*num_values=*/1, draws_[0].ptr, type_length_);
+
+ auto decoder = MakeDictDecoder<FLBAType>(descr_.get());
+ decoder->SetDict(dict_decoder.get());
+
+ // RLE_DICTIONARY data: bit width 1, followed by an RLE run of one index
value
+ // 1. The dictionary has only one entry, so the index is out of bounds.
+ const std::vector<uint8_t> indices = {1, 2, 1};
+ decoder->SetData(/*num_values=*/1, indices.data(),
static_cast<int>(indices.size()));
+
+ auto flba_decoder = dynamic_cast<FLBADecoder*>(decoder.get());
+ ASSERT_NE(nullptr, flba_decoder);
+ std::vector<uint8_t> dense(static_cast<size_t>(type_length_));
+ ASSERT_THROW(flba_decoder->Decode(dense.data(), /*max_values=*/1),
ParquetException);
+}
+
+TEST_F(TestFLBADenseDecode, DeltaByteArrayRejectsWrongFLBALengthDense) {
+ std::string suffix(static_cast<size_t>(type_length_ - 1), 'x');
+ auto buffer =
+ ::arrow::ConcatenateBuffers({DeltaEncode({0}), DeltaEncode({type_length_
- 1}),
+ std::make_shared<Buffer>(suffix)})
+ .ValueOrDie();
+
+ auto decoder = MakeTypedDecoder<FLBAType>(Encoding::DELTA_BYTE_ARRAY,
descr_.get());
+ decoder->SetData(/*num_values=*/1, buffer->data(),
static_cast<int>(buffer->size()));
+
+ std::vector<uint8_t> dense(static_cast<size_t>(type_length_));
+ ASSERT_THROW(decoder->Decode(dense.data(), /*max_values=*/1),
ParquetException);
+}
+
} // namespace parquet::test