Skip to content

Optimize Gorilla batch floating-point decoding#873

Merged
ColinLeeo merged 3 commits into
developfrom
perf/gorilla-wide-bit-reader
Jul 23, 2026
Merged

Optimize Gorilla batch floating-point decoding#873
ColinLeeo merged 3 commits into
developfrom
perf/gorilla-wide-bit-reader

Conversation

@ColinLeeo

@ColinLeeo ColinLeeo commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Motivation

The Gorilla batch decoder previously refilled its bit cache one byte at a time and decoded floating-point values through small temporary integer buffers. Profiling showed that most CPU time was spent in control-bit parsing, variable-width bit reads, and repeated batch bookkeeping.

This PR reduces that overhead while keeping the Gorilla encoding format unchanged.

Modifications

  • Replace the one-byte bit cache with a 64-bit bit reservoir.
    • Refill up to eight encoded bytes at once.
    • Preserve prefetched bits across batch, scalar, and skip operations.
  • Add a fast path for variable-width bit reads.
    • Reads contained in the current reservoir avoid the generic loop.
    • Cross-reservoir reads require at most one additional refill.
  • Decode floating-point batches directly into float and double output buffers.
    • Remove the temporary 129-value integer buffers.
    • Remove the associated copy/conversion pass.
  • Use unsigned bit accumulation to avoid signed-shift undefined behavior.
  • Validate truncated input and invalid Gorilla metadata.
  • Fix decoder state handling when switching from scalar decoding to batch decoding.
  • Preserve support for both wrapped and non-wrapped ByteStream inputs.

Performance

Measured on ARM64 macOS using a Release build with -O3 and LTO. Results are the median of three runs over 500,000 smooth floating-point values.

Type Before After Improvement
Float batch decode 3.04 ns/value 1.73 ns/value 1.76x faster
Double batch decode 5.43 ns/value 2.12 ns/value 2.57x faster

Profiling also showed that the temporary-buffer copy was eliminated and the proportion of samples in variable-width bit reading decreased significantly.

Compatibility

  • The encoded Gorilla data format is unchanged.
  • Existing encoded files remain compatible.
  • Integer, long, float, and double Gorilla decoders share the optimized bit reader.
  • Scalar, batch, and skip APIs can be interleaved safely.

Tests

Added coverage for:

  • Decoding one value per batch call.
  • Interleaving scalar, batch, and skip operations.
  • Full-width 64-bit XOR values.
  • Non-wrapped floating-point batch input.
  • Truncated input handling.

Validation results:

  • 23 targeted Gorilla tests passed.
  • 716 complete C++ tests passed.
  • 10 pre-existing tests remain disabled.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR optimizes the C++ Gorilla batch decoder by introducing a 64-bit bit reservoir to reduce refill overhead, adding faster variable-width bit reads, and decoding float/double batches directly into their output buffers (removing intermediate integer staging). It also adds targeted C++ tests to cover additional batch/scalar/skip interleavings and wrapped vs non-wrapped ByteStream usage.

Changes:

  • Rework the raw-pointer Gorilla bit reader to use a 64-bit reservoir with explicit validation for truncated/corrupted input in the wrapped fast path.
  • Update batch decode paths to write directly to float/double output buffers via a lightweight conversion helper.
  • Add C++ unit tests for unwrapped batch input, one-value-at-a-time batching, scalar/batch/skip interleaving, and full-width XOR patterns.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
cpp/src/encoding/gorilla_decoder.h Introduces the 64-bit reservoir bit reader and refactors batch decoding to be faster and write directly into float/double buffers.
cpp/test/encoding/gorilla_codec_test.cc Adds new Gorilla decoding tests to validate additional batch behaviors and input modes.
Comments suppressed due to low confidence (4)

cpp/test/encoding/gorilla_codec_test.cc:501

  • This test uses memcpy but the file doesn’t include <cstring>, which can cause compilation failures. Prefer std::memcpy with an explicit #include <cstring> (or add the appropriate header for memcpy).
    for (int i = 0; i < N; i++) {
        memcpy(&expected[i], &patterns[i], sizeof(double));
        ASSERT_EQ(encoder.encode(expected[i], stream), common::E_OK);
    }

cpp/test/encoding/gorilla_codec_test.cc:341

  • read_batch_float takes an int capacity, but actual.size() is size_t. On some builds this can trigger sign/width conversion warnings (often treated as errors). Cast explicitly to int (or use N).
    ASSERT_EQ(decoder.read_batch_float(actual.data(), actual.size(),
                                       actual_count, stream),
              common::E_OK);

cpp/test/encoding/gorilla_codec_test.cc:455

  • read_batch_double takes an int capacity, but batch.size() is size_t. Cast explicitly to int to avoid sign/width conversion warnings.
        decoder.read_batch_double(batch.data(), batch.size(), actual, wrapped),

cpp/test/encoding/gorilla_codec_test.cc:478

  • read_batch_double takes an int capacity, but tail.size() is size_t. Cast explicitly to int to avoid sign/width conversion warnings.
        decoder.read_batch_double(tail.data(), tail.size(), actual, wrapped),

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 545 to 549
actual = 0;
while (actual < capacity && has_remaining(in)) {
out[actual++] = decode(in);
out[actual++] = GorillaDecodeOutput<T, Output>::convert(decode(in));
}
return common::E_OK;
Comment on lines +330 to +332
for (int i = 0; i < N; i++) {
expected[i] = std::sin(i * 0.125f) * 23.0f;
ASSERT_EQ(encoder.encode(expected[i], stream), common::E_OK);
@codecov-commenter

codecov-commenter commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.84211% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.47%. Comparing base (928ca94) to head (4ce7d17).
⚠️ Report is 2 commits behind head on develop.

Files with missing lines Patch % Lines
cpp/src/encoding/gorilla_decoder.h 86.84% 20 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #873      +/-   ##
===========================================
+ Coverage    60.43%   60.47%   +0.03%     
===========================================
  Files          744      744              
  Lines        48963    49011      +48     
  Branches      7771     7784      +13     
===========================================
+ Hits         29589    29637      +48     
- Misses       17965    17970       +5     
+ Partials      1409     1404       -5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ColinLeeo
ColinLeeo requested a review from jt2594838 July 23, 2026 06:55

@jt2594838 jt2594838 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this referable to the Java edition?

@ColinLeeo
ColinLeeo requested a review from Copilot July 23, 2026 09:01
@ColinLeeo

Copy link
Copy Markdown
Contributor Author

Is this referable to the Java edition?

Yes, the core idea can be referenced for the Java implementation.
Java’s GorillaDecoderV2 also uses an 8-bit buffer and refills it byte by byte from ByteBuffer, so a wider bit reservoir may reduce refill and branching overhead.

However, this PR is C++-specific because it also relies on the C++ batch-decoding API and direct writes into float/double output arrays. Java currently decodes values individually into page/column builders, so it would require a separate implementation and Java-specific benchmarks before we can confirm the benefit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comment on lines +36 to +38
// The 64-bit reservoir amortizes bounds checks and refill work across up to
// eight encoded bytes. Valid bits are kept right-aligned in buffer; bits is
// the number of unread low bits.
Comment on lines +333 to +344
int take = bits < bits_left_ ? bits : bits_left_;
uint64_t chunk;
if (take == 64) {
chunk = buffer_;
} else {
// Shift to correct position and take only least significant
// bits.
uint8_t d =
(uint8_t)((((uint8_t)buffer_) >> (bits_left_ - bits)) &
((1 << bits) - 1));
value = (value << bits) + (d & 0xFF);
bits_left_ -= bits;
bits = 0;
chunk = (buffer_ >> (bits_left_ - take)) &
((uint64_t{1} << take) - 1);
value <<= take;
}
flush_byte_if_empty(in);
value |= chunk;
bits_left_ -= take;
bits -= take;
Comment thread cpp/test/encoding/gorilla_codec_test.cc Outdated
Comment on lines +448 to +451
ASSERT_TRUE(decoder.has_remaining(wrapped))
<< "remaining=" << wrapped.remaining_size()
<< " bits_left=" << decoder.bits_left_
<< " has_next=" << decoder.has_next_;
Comment thread cpp/test/encoding/gorilla_codec_test.cc Outdated
stream.read_buf(buf.data(), total, got);
ASSERT_EQ(got, total);
common::ByteStream wrapped(common::MOD_DEFAULT);
wrapped.wrap_from((const char*)buf.data(), total);

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment on lines +233 to +236
uint64_t xor_value = r.read_long(meaningful);
if (UNLIKELY(r.exhausted || r.invalid)) return false;
xor_value <<= stored_trailing_zeros;
stored_value ^= static_cast<int64_t>(xor_value);
Comment on lines +338 to +342
storage::FloatGorillaDecoder decoder;
std::vector<float> actual(N);
int actual_count = 0;
ASSERT_EQ(decoder.read_batch_float(actual.data(), N, actual_count, stream),
common::E_OK);
@ColinLeeo
ColinLeeo merged commit 7d44338 into develop Jul 23, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants