Skip to content

GH-48701: [C++][Parquet] Add ALPpd encoding - #48345

Open
prtkgaur wants to merge 155 commits into
apache:mainfrom
prtkgaur:gh540-alp-pseudoDecimal-encoding
Open

GH-48701: [C++][Parquet] Add ALPpd encoding#48345
prtkgaur wants to merge 155 commits into
apache:mainfrom
prtkgaur:gh540-alp-pseudoDecimal-encoding

Conversation

@prtkgaur

@prtkgaur prtkgaur commented Dec 5, 2025

Copy link
Copy Markdown

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.

  • Alp h/cc : Houses core logic for encoding and decoding.
  • Sampler h/cc : Houses logic to sample and select parameters for encoding.
  • AlpWrapper h/cc : Binds together Alp and Sampler classes.

Integration of the above code was done in

  • Encoder/Decoder cc which exposes wrapper to encode buffer of data.

Are these changes tested?

  • We have added unit tests to test the code.
  • Also the benchmarks have been added that cover wide variety of floating point values from low precision to high precision.

Unit tests

  • alp_test.cc

Benchmark tests

  • encoding_benchmark.cc and encoding_alp_benchmark.cc

Are there any user-facing changes?

  • It's a new encoding so the only impact is query performance which we claim will only get better.

DuckDB

  • We did look at DuckDB's ALP implementation while we were implementing ALP and would like to give that team the desired credit.

@github-actions

github-actions Bot commented Dec 5, 2025

Copy link
Copy Markdown

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?

GH-${GITHUB_ISSUE_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

See also:

@prtkgaur
prtkgaur force-pushed the gh540-alp-pseudoDecimal-encoding branch 3 times, most recently from 1b78a5c to d563ce0 Compare December 7, 2025 15:46
Comment thread cpp/src/arrow/util/alp/data/floatingpoint_data.tar.gz Outdated
Comment thread cpp/src/parquet/types.h
@alamb

alamb commented Dec 8, 2025

Copy link
Copy Markdown
Contributor

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.

@prtkgaur prtkgaur changed the title [Gh540] Add ALPpd encoding to parquet [Gh539] Add ALPpd encoding to parquet Dec 8, 2025
Comment thread cpp/src/arrow/util/alp/data/floatingpoint_data.tar.gz Outdated
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";

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@Reviewer the data sits in the parquet-testing submodule
apache/parquet-testing#100

Comment thread cpp/src/arrow/util/small_vector.h Outdated

// 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) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Using this over resize gave us around 2-3% performance improvement

@prtkgaur prtkgaur changed the title [Gh539] Add ALPpd encoding to parquet [Gh539][Encoding] Add ALPpd encoding to parquet Dec 8, 2025
@prtkgaur prtkgaur changed the title [Gh539][Encoding] Add ALPpd encoding to parquet [Gh-539][Encoding] Add ALPpd encoding to parquet Dec 8, 2025
@prtkgaur
prtkgaur force-pushed the gh540-alp-pseudoDecimal-encoding branch from 0c035b7 to 1cb0852 Compare December 8, 2025 23:48
@prtkgaur prtkgaur changed the title [Gh-539][Encoding] Add ALPpd encoding to parquet [Gh-539][ParquetEncoding][c++] Add ALPpd encoding to parquet Dec 9, 2025
@emkornfield

Copy link
Copy Markdown
Contributor

Talked offline and wanted to capture notes on high-level changes:

  1. For headers, lets try to reduce duplication with values already in the parquet header.
  2. For remaining items in headers, lets try to be parsimonious with values (i.e. 4 bytes is probably overkill for enums)
  3. Naming convention on files is off (use snake_case).
  4. Given description of ALP, we probably want a top level encoding enum value for the 2 different modes of ALP.

@prtkgaur
prtkgaur force-pushed the gh540-alp-pseudoDecimal-encoding branch from 35f1ad7 to 0908342 Compare December 15, 2025 21:28
@prtkgaur

Copy link
Copy Markdown
Author

Talked offline and wanted to capture notes on high-level changes:

  1. For headers, lets try to reduce duplication with values already in the parquet header.
  2. For remaining items in headers, lets try to be parsimonious with values (i.e. 4 bytes is probably overkill for enums)
  3. Naming convention on files is off (use snake_case).
  4. Given description of ALP, we probably want a top level encoding enum value for the 2 different modes of ALP.

Thanks for the feedback @emkornfield. We have addressed

  1. Reduce duplication of fields between page header and alp header
  2. Other fields have been updated to use 1 byte. Header is now just 8 bytes compared to 40 bytes earlier.
  3. Naming of files has been updated.
  4. We do have the top level enums describing the mode and layout structure.
    enum class AlpBitPackLayout { kNormal }; and enum class AlpMode { kAlp };

@prtkgaur prtkgaur changed the title [Gh-539][ParquetEncoding][c++] Add ALPpd encoding to parquet [Gh-48701][ParquetEncoding][c++] Add ALPpd encoding to parquet Dec 31, 2025
@kou kou changed the title [Gh-48701][ParquetEncoding][c++] Add ALPpd encoding to parquet GH-48701: [C++][Parquet] Add ALPpd encoding Jan 1, 2026
@github-actions

github-actions Bot commented Jan 1, 2026

Copy link
Copy Markdown

⚠️ GitHub issue #48701 has been automatically assigned in GitHub to PR creator.

Comment thread cpp/src/parquet/decoder.cc Outdated

// Slow path: partial read - decode to intermediate buffer
// ALP Bit unpacker needs batches of 64
if (needs_decode_) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

TODO(prateek) : check with Antoine and other reviewers if there is a way to relax this constraint. Though this has negligible impact on performance.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

no action needed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

umm ideally this submodule shouldn't be attached with this commit.
Should revert the changes to this file.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Pull request for dataset
apache/parquet-testing#100

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Please check cpp/src/arrow/util/alp/ALP_Encoding_Specification_terse.md for a more terse spec of the encoding.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Replace 1024 with the constant specified in AlpConstant file.

@prtkgaur
prtkgaur force-pushed the gh540-alp-pseudoDecimal-encoding branch from 1b08599 to f5f5011 Compare January 12, 2026 16:00
@prtkgaur
prtkgaur marked this pull request as ready for review January 13, 2026 18:03
@prtkgaur
prtkgaur requested a review from wgtmac as a code owner January 13, 2026 18:03
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.
@prtkgaur
prtkgaur requested a review from wgtmac September 3, 2026 16:47
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.
Comment thread cpp/src/arrow/util/alp/alp.cc Outdated
const ExactType* data = input_vector.data();
const ExactType frame_of_ref = for_info.frame_of_reference();

#pragma GCC unroll AlpConstants::kLoopUnrolls

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These currently break Clang and MSVC builds under -Werror. Please make the ALP sources compile cleanly with both Clang and MSVC.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

@github-actions github-actions Bot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Sep 7, 2026
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.
@prtkgaur
prtkgaur requested a review from wgtmac September 8, 2026 00:04
@wgtmac

wgtmac commented Sep 8, 2026

Copy link
Copy Markdown
Member

CI is still failing. Could you make them happy?

@prtkgaur

prtkgaur commented Sep 8, 2026

Copy link
Copy Markdown
Author

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants