GH-48701: [C++][Parquet] Add ALPpd encoding - #48345
Conversation
|
Thanks for opening a pull request! If this is not a minor PR. Could you open an issue for this pull request on GitHub? https://github.com/apache/arrow/issues/new/choose Opening GitHub issues ahead of time contributes to the Openness of the Apache Arrow project. Then could you also rename the pull request title in the following format? or See also: |
1b78a5c to
d563ce0
Compare
|
Thanks @prtkgaur -- it is super exciting to see this movement. Unfortunately, I am not familiar with the C/C++ codebase to give this a realistic review. I started the CI checks on this PR and had some comments about the testing. |
| std::string tarball_path = std::string(__FILE__); | ||
| tarball_path = tarball_path.substr(0, tarball_path.find_last_of("/\\")); | ||
| tarball_path = tarball_path.substr(0, tarball_path.find_last_of("/\\")); | ||
| tarball_path += "/arrow/cpp/submodules/parquet-testing/data/floatingpoint_data.tar.gz"; |
There was a problem hiding this comment.
@Reviewer the data sits in the parquet-testing submodule
apache/parquet-testing#100
|
|
||
| // Unsafe resize without initialization - use only when you will immediately | ||
| // overwrite the memory (e.g., before memcpy). Only safe for POD types. | ||
| void UnsafeResize(size_t n) { |
There was a problem hiding this comment.
Using this over resize gave us around 2-3% performance improvement
0c035b7 to
1cb0852
Compare
|
Talked offline and wanted to capture notes on high-level changes:
|
35f1ad7 to
0908342
Compare
Thanks for the feedback @emkornfield. We have addressed
|
|
|
|
|
||
| // Slow path: partial read - decode to intermediate buffer | ||
| // ALP Bit unpacker needs batches of 64 | ||
| if (needs_decode_) { |
There was a problem hiding this comment.
TODO(prateek) : check with Antoine and other reviewers if there is a way to relax this constraint. Though this has negligible impact on performance.
There was a problem hiding this comment.
umm ideally this submodule shouldn't be attached with this commit.
Should revert the changes to this file.
There was a problem hiding this comment.
Please check cpp/src/arrow/util/alp/ALP_Encoding_Specification_terse.md for a more terse spec of the encoding.
There was a problem hiding this comment.
Also this file will be removed once the spec in parquet format repository is merged.
|
|
||
| ## 2. Data Layout | ||
|
|
||
| ALP encoding consists of a page-level header followed by one or more encoded vectors. Each vector contains up to 1024 elements. |
There was a problem hiding this comment.
Replace 1024 with the constant specified in AlpConstant file.
1b08599 to
f5f5011
Compare
Each test was checked against the code with its validation removed, so a regression in any of the three shows up as a failure.
The batched test fails if the decoder's remaining count and its scratch index disagree, which is the divergence the old three-variable state allowed.
AlpCodec<T>::VectorReader validates the header and offset chain once, then decodes any single vector on demand, so a batched reader needs scratch for one vector rather than for a whole page.
arrow_reader_writer_test.cc is already long, so the encoding round trips move to arrow/arrow_encoding_test.cc, which later encodings can share.
The names now say what the install rules already do: these headers are not installed and carry no backward-compatibility obligation.
The test hand-duplicated a float block and a double block, which is what the type-parameterized suite is for. Also move the float decode benchmark's output buffer out of the loop and stop timing its SetData, matching the double one.
Incremental decode calls DecodeVector once per vector, and each call built a fresh view and a fresh vector of unpacked integers, so a page's worth of vectors meant a heap allocation per vector (three, when the vector had exceptions). Let the reader hold that scratch and reuse it: ResetDataOnly points an existing view at the next vector's data, and DecompressVectorView now unpacks into a caller-provided span instead of returning a vector. Decoding 32 vectors of doubles allocates 11 times for the first vector and nothing after it, where before it allocated on every one.
The ALP decode benchmarks were the only ones in the file hiding SetData behind PauseTiming, which left their numbers incomparable to their neighbours'.
parquet.thrift is a vendored copy of the format repo's file, and ALP has since been merged there. The comment written here says the same thing in different words, and points at Encodings.md#alp for the full spec, which now lives in AlpEncoding.md.
parquet/encoder.cc and decoder.cc call AlpCodec, whose out-of-line definitions live in libarrow, so the ten symbols they reference have to cross the library boundary. Every other arrow symbol libparquet imports carries ARROW_EXPORT; these did not, which breaks an MSVC shared build and any build that gives arrow hidden default visibility. Also fold the three-deep namespace into one, which is what clang-tidy's modernize-concat-nested-namespaces asks for.
Four of the five const members were copies of AlpConstants values and two of those were only ever read to derive the sampling interval, which is itself a constant. Reference the constants directly and keep the derived one as a static constexpr, which removes the constructor and 40 bytes from every sampler.
| const ExactType* data = input_vector.data(); | ||
| const ExactType frame_of_ref = for_info.frame_of_reference(); | ||
|
|
||
| #pragma GCC unroll AlpConstants::kLoopUnrolls |
There was a problem hiding this comment.
These currently break Clang and MSVC builds under -Werror. Please make the ALP sources compile cleanly with both Clang and MSVC.
There was a problem hiding this comment.
Both pragmas now sit behind defined(__GNUC__) && !defined(__clang__), so Clang and MSVC see nothing to warn about.
I compared the assembly before choosing that over simply deleting them: GCC's output for the two decode loops does change without them, so the guard keeps its codegen rather than trading it away, and the other two compilers fall back to whatever their optimizers work out. A -Wall -Wextra sweep over the ALP sources is clean.
| /// can then decode any single vector on its own, so serving a batch of values | ||
| /// costs work in proportion to the batch rather than to the buffer, and needs | ||
| /// scratch for one vector rather than for the whole buffer. | ||
| class VectorReader { |
There was a problem hiding this comment.
parquet_shared calls these out-of-line methods, but Windows cannot see their symbols.
Please export the VectorReader methods. Then verify that the MinGW shared build links Open, VectorLength, and DecodeVector.
There was a problem hiding this comment.
Added ARROW_EXPORT to the nested class as well, which is the idiom the other nested classes in arrow that libparquet calls into follow.
One thing worth recording, because it caught me out: GCC propagates the enclosing class's visibility attribute into a nested class, so the usual local proxy for this defect — compile with -fvisibility=hidden and read the ELF visibility of the symbol — reports DEFAULT for Open, VectorLength and DecodeVector whether or not the nested marker is there. There is no clang or mingw toolchain on the machine I am working on, so I cannot claim the MinGW link is verified; CI is the check for it.
| bit-packing; values that cannot be converted losslessly are stored as | ||
| exceptions. See Encodings.md for the detailed specification. | ||
| */ | ||
| ALP = 10; |
There was a problem hiding this comment.
Please run cpp/build-support/update-thrift.sh and commit the updated cpp/src/generated/parquet_types.h and cpp/src/generated/parquet_types.cpp.
There was a problem hiding this comment.
Regenerated and committed. I used thrift 0.23.0, which is the version that produced the checked-in files — the vendored copy in the build tree is 0.22.0 and regenerating with it churns the whole file. The diff is the ALP enum value and nothing else.
| PARQUET_ASSIGN_OR_THROW( | ||
| reader_, ::arrow::util::alp::AlpCodec<T>::VectorReader::Open(data, len)); | ||
| total_values_ = reader_.num_elements(); | ||
| if (total_values_ > num_values) { |
There was a problem hiding this comment.
This compares the ALP non-null count with the page slot count. A corrupt optional page can therefore leave encoded values unread without an error. Please reject the page if the decoder is not exhausted after all definition levels are consumed.
There was a problem hiding this comment.
Good catch — an optional page satisfies that count check with room to spare, so it cannot see this.
The decoder now records the page's level count and the levels that carried no value, and rejects the page once every level is accounted for while the payload still has values unread. The accounting is derived from decoder state rather than accumulated per call, which matters because the base DecodeSpaced delegates to Decode: counting in both places would double-count, and deriving it keeps the check idempotent no matter which path a reader takes. A new round-trip test covers it for float and double, and it fails on both when the check is removed.
Clang and MSVC warn on an unknown pragma, which -Werror turns into an error. GCC keeps both; dropping them there changes its codegen.
dllexport does not reach into a nested class, so libparquet cannot resolve the three reader symbols it imports when MSVC links parquet.dll.
Machine output of update-thrift.sh with thrift 0.23.0.
The header count check an optional page passes cannot see this, because such a page legitimately holds fewer values than levels.
|
CI is still failing. Could you make them happy? |
Ack yes. Let me look into them. |
MSVC treats the double-to-size_t narrowing as an error. Integer ceiling division gives the same count.
clang cannot attach a tparam to a member template declared in its class, so the doc build rejects it.
The marker on a class does not reach a member template, so the Windows link could not find the instantiations.
CompressVector takes int32_t, so passing a size_t narrows and clang rejects it with -Wshorten-64-to-32 under -Werror.
Their definitions live in implementation files, so a separate test executable cannot reach them across a shared library boundary without the marker.
The index is 64-bit, so a 32-bit shift operand makes MSVC warn that the shift may have been meant to be 64-bit, and CHECKIN treats that as an error.
The new ALP test file defines a write-then-read helper called DoRoundtrip in an anonymous namespace. arrow_reader_writer_test.cc, which shares its test target, already has a DoRoundtrip whose trailing parameters are defaulted, so a four-argument call matches both signatures exactly. Separate translation units never see each other's helper, so the collision is invisible until the two files are compiled as one, which is what the Windows build does with unity builds enabled. Rename ours after what it does.
AlpEncodedVectorInfo is exported, so for anything linking against the shared library its static constexpr kStoredSize is an import. Reading the value is fine, because a constexpr initializer is folded at compile time, but binding a reference to it asks for an address, and a constexpr member has no out-of-line definition in the library to take the address of. Windows GCC then fails to link the test. The same assertion on AlpEncodedForVectorInfo two lines below is unaffected: that one is a class template, so its static members are instantiated in the translation unit that uses them. Only the non-template class needs the value read by hand, and the test already checks it through GetStoredSize on the next line, which returns by value.
Co-authored-by: dhirhan17@gmail.com
Rationale for this change
ALP significantly improves on the compression ratio and decompression speed over of float/double columns over other encoding/compression techniques.
Spec
Spec
This PR also contains a terse version of the spec in the file cpp/src/arrow/util/alp/ALP_Encoding_Specification_terse.md which can go in the Encodings.md
Parquet Format PR
Dataset PR (parquet-testing)
apache/parquet-testing#100
What changes are included in this PR?
This PR
Introduces ALP (pseudo-decimal) encoding into c++ arrow code.
We also provide benchmarks and dataset to prove the effectiveness of the above algorithm.
Adding above needed us to add following classes.
Integration of the above code was done in
Are these changes tested?
Unit tests
Benchmark tests
Are there any user-facing changes?
DuckDB