Skip to content

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

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

prtkgaur wants to merge 164 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

Adaptive Lossless Floating-Point (ALP)
is designed for floating-point data that commonly represents decimal values.
For these workloads, ALP can provide better compression and faster decoding
than general-purpose compression or existing Parquet encodings.

This PR adds ALP support for Parquet FLOAT and DOUBLE columns in the Arrow
C++ implementation.

Specification

What changes are included in this PR?

This PR adds:

  • The core ALP compression and decompression implementation.
  • Sampling logic for selecting encoding parameters.
  • Page metadata serialization, validation, and decoding.
  • Parquet encoders and decoders for FLOAT and DOUBLE.
  • Incremental decoding of ALP pages one vector at a time.
  • CMake and Meson build integration.
  • C++ documentation describing how to use the encoding.

ALP is opt-in on the write path. It is used only when the writer explicitly
selects Encoding::ALP and dictionary encoding is disabled. Arrow does not
currently select ALP automatically based on the input data.

Are these changes tested?

Yes. Test coverage includes:

  • Interoperability with the ALP conformance file in parquet-testing.
  • FLOAT and DOUBLE round trips.
  • Decimal-like, constant, random, and exception-heavy inputs.
  • Empty and all-null pages.
  • Configurable vector sizes.
  • Truncated or malformed page metadata.
  • Invalid offsets, element counts, bit widths, and exception positions.
  • Batched and incremental decoding.

Are there any user-facing changes?

Yes. Arrow C++ can read Parquet pages encoded with ALP. Writers can explicitly
select ALP for supported floating-point columns.

ALP requires reader support and is not selected automatically, so existing
writer behavior remains unchanged unless users opt in.

@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

Comment thread cpp/submodules/parquet-testing

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
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.
An empty vector's data() may be null, and memcpy and memset may not be
handed a null pointer even when the length is zero. Both endian helpers,
the two copies of the packed values and the zero fill for a bit width of
zero all reached that case: a vector with no exceptions or one whose
values the frame captures entirely. UndefinedBehaviorSanitizer reports
each as a nonnull violation and the sanitizer build makes it fatal.

Copying zero bytes never did anything, so skipping the call leaves the
encoded form and the decoded values unchanged.
The bounds admitted the largest float below 2^31 and the largest double
below 2^63. Fast rounding can carry a value up by one ulp, so those two
values round to exactly 2^31 and 2^63, which the target integer types
cannot represent. Pull each bound down by one ulp, which is both the
minimum and the maximum headroom the rounding needs.

The two affected values become encoding exceptions and still round-trip
exactly, so only the internal representation changes.
@prtkgaur
prtkgaur force-pushed the gh540-alp-pseudoDecimal-encoding branch from 66eadd1 to 7462b91 Compare September 14, 2026 15:42
sfc-gh-pgaur and others added 4 commits September 15, 2026 03:10
The ALP conformance fixture is already on parquet-testing main, so Arrow picks
it up on any routine submodule bump; skip the tests until the pin moves.
alp_internal.h opened with an eighty-four line flowchart drawing the
compression and decompression pipelines in ASCII boxes, one step per box, each
step restating what the function that implements it already says. Nothing else
under cpp/src draws comments this way -- the style appears in no other Arrow C++
file -- and the drawing had drifted from the code it described: two boxes were a
character short of closing, and it carried subscripts and arrows outside ASCII.
The five steps are now a paragraph.

The page layout was drawn three times, in two files, each copy followed by the
same three bullets about random access and parallel decompression. It is now
drawn once, in the file that reads and writes it, with the box closed and the
reason given in a sentence. Subscripts and arrows are spelled in ASCII
throughout, leaving one em dash in prose as the only byte above 126.

The tests drew their section headings as boxes. Arrow writes a single rule of
seventy dashes, so twenty-two headings lose their equals signs and the
twenty-two closing rules go.

Comments only. Stripping comments leaves all four files byte-identical to their
previous revision, clang-format reports no new warnings, and no line crosses
ninety columns.
Comment-only. Both files are byte-identical to their previous contents once
comments are stripped, and clang-format reports the same zero warnings before
and after.

Four comments described what the code used to be rather than what it has to
guarantee, so a reader learned the history of a fix instead of the constraint
the test pins. Each keeps its technical content:

- The preamble on the view-load test said LoadView "was previously vulnerable"
  and that "the old code used reinterpret_cast". It now states the constraint:
  exception_positions (uint16_t) and exceptions (T) sit at offsets that depend
  on bit_packed_size, an odd size leaves them misaligned, reading through them
  is undefined behavior that ubsan reports, and the view copies both into
  aligned storage.
- "this was where the ubsan error occurred" becomes a note that the load is
  where the alignment constraint applies.
- "previously spans that could be misaligned" becomes the aligned members the
  decompress path actually exercises.
- The random-data test no longer contrasts against a range it used to use; it
  states that a range staying inside the encodable window never reaches the
  fallback.

Two more restated the code directly below them: a comment naming the element
count already in the expression, and a "we need to process in chunks" line above
the chunked calls.
Split the ALP implementation into constants, metadata, compression,
sampler, and codec units. Simplify the internal APIs and update the
CMake/Meson build integration.

Remove the separate writer opt-in flag. ALP is selected explicitly with
Encoding::ALP while dictionary encoding is disabled.

Harden encoding and decoding:
- validate page headers, vector metadata, offset chains, bit widths,
  element counts, and exception positions;
- reject pages that leave ALP values unconsumed after all levels are read;
- reuse pool-backed scratch buffers and cache partially decoded vectors;
- use typed power-of-ten constants for exact decode semantics;
- emit a valid header-only page for all-null input.

Rework the ALP tests around the production APIs, remove redundant cases,
and add malformed-input, boundary, exception, and all-null coverage.
Move the end-to-end tests to arrow_encoding_test.cc and enable real
parquet-testing interoperability coverage. Update the submodule pin,
benchmarks, and C++ documentation.
@wgtmac

wgtmac commented Sep 22, 2026

Copy link
Copy Markdown
Member

I've finished a full round of review on my end. Instead of posting a lot of comments, I went ahead to create prtkgaur#3 against your fork. Please let me know what you think, @prtkgaur.

The layout tables for AlpInfo, AlpForInfo and the serialized vector were
lost when alp_internal.h was split up. Put them back next to the classes
they describe, alongside the page and header tables in alp_codec.cc.

The old ForInfo table gave 6 and 10 bytes for the float and double
specializations. Both are one byte smaller than that: a frame of
reference plus a bit width, with no padding on the wire.
AlpEncoder's static_assert only fires if the template is instantiated,
and for an unsupported physical type the encoder and decoder factories
throw before they instantiate it. Test that runtime path directly for
the six other physical types.

Also restore the note about falling back to PLAIN. ALP expands a few
columns in the paper's datasets and nothing in the writer decides
against it.
Preview status is already noted, but not the behaviour it relies on: no
write path chooses ALP, so a column carries it only where encoding()
names it.
@emkornfield

Copy link
Copy Markdown
Contributor

@prtkgaur it looks like CI failures are due to warnings, could you fix. @wgtmac after the merge of prtkgaur#3 do you have more concerns or want to review again?

@alamb

alamb commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

I went over this PRs comments again and I can't see any additional outstanding items.

Are we waiting on anything else to approve this PR? (perhaps @HuaHuaY or @wgtmac could comment as they are listed as code owners and have reviewed this PR before)

One thing I did find was @emkornfield 's suggestion to

I think the one other thing we should figure out is how to fall back to plain encoding at some point if ALP is completely failing on the dataset (i.e. it is consistently adding pages that take more space the PLAIN encoding). I think this can be done in a follow-up.

Maybe that would be good to file a follow on ticket

Move the decode target restriction from requires clauses to static
assertions. This keeps invalid target types as compile-time errors without
encoding constraints into exported symbol names. GCC can now match the
explicit instantiations, and Clang shared builds resolve the same symbols.
The sanitizer target links and all 74 ALP tests pass.
@emkornfield

Copy link
Copy Markdown
Contributor

MinGW failures seem possibly related:

[  FAILED  ] 4 tests, listed below:
[  FAILED  ] TestJSONWithLocalFile.JSONOutputWithStatistics
[  FAILED  ] TestJSONWithLocalFile.JSONOutput
[  FAILED  ] TestJSONWithLocalFile.JSONOutputFLBA
[  FAILED  ] TestJSONWithLocalFile.JSONOutputSortColumns

For JSON columns parquet I think. Maybe something with branch updates?

@prtkgaur

Copy link
Copy Markdown
Author

MinGW failures seem possibly related:

[  FAILED  ] 4 tests, listed below:
[  FAILED  ] TestJSONWithLocalFile.JSONOutputWithStatistics
[  FAILED  ] TestJSONWithLocalFile.JSONOutput
[  FAILED  ] TestJSONWithLocalFile.JSONOutputFLBA
[  FAILED  ] TestJSONWithLocalFile.JSONOutputSortColumns

For JSON columns parquet I think. Maybe something with branch updates?

Ack checking

@prtkgaur

Copy link
Copy Markdown
Author

[ FAILED ] 4 tests, listed below:
[ FAILED ] TestJSONWithLocalFile.JSONOutputWithStatistics
[ FAILED ] TestJSONWithLocalFile.JSONOutput
[ FAILED ] TestJSONWithLocalFile.JSONOutputFLBA
[ FAILED ] TestJSONWithLocalFile.JSONOutputSortColumns

@emkornfield, not this does not look related to my change.
I see it failed earlier too : https://github.com/apache/arrow/actions/runs/36635778787/job/109635923863

AMD64 macOS 15-intel C++ <--- this is a timeout unrelated to my change. 81 - arrow-s3fs-test (Timeout) arrow-tests filesystem unittest.

ARM64 macOS GLib & Ruby <--- Again doesn't look like this PR.

_deps/google_cloud_cpp-src/google/cloud/internal/openssl/parse_service_account_p12_file.cc:88:14:
  error: cannot initialize a variable of type 'X509_NAME *' with an rvalue of type 'const X509_NAME *

@prtkgaur

Copy link
Copy Markdown
Author

I went over this PRs comments again and I can't see any additional outstanding items.

Are we waiting on anything else to approve this PR? (perhaps @HuaHuaY or @wgtmac could comment as they are listed as code owners and have reviewed this PR before)

One thing I did find was @emkornfield 's suggestion to

I think the one other thing we should figure out is how to fall back to plain encoding at some point if ALP is completely failing on the dataset (i.e. it is consistently adding pages that take more space the PLAIN encoding). I think this can be done in a follow-up.

Maybe that would be good to file a follow on ticket

Yes this I plan to handle in a follow up PR.
Do you recommend I put in a TODO with the JIRA number?

@kou

kou commented Sep 30, 2026

Copy link
Copy Markdown
Member

We have issues for these failures:

@wgtmac wgtmac left a comment

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 looks good to me now. Thanks @prtkgaur for improving this PR over time!

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.

8 participants