Optimize Gorilla batch floating-point decoding#873
Conversation
There was a problem hiding this comment.
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/doubleoutput 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
memcpybut the file doesn’t include<cstring>, which can cause compilation failures. Preferstd::memcpywith an explicit#include <cstring>(or add the appropriate header formemcpy).
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_floattakes anint capacity, butactual.size()issize_t. On some builds this can trigger sign/width conversion warnings (often treated as errors). Cast explicitly toint(or useN).
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_doubletakes anint capacity, butbatch.size()issize_t. Cast explicitly tointto 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_doubletakes anint capacity, buttail.size()issize_t. Cast explicitly tointto 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.
| actual = 0; | ||
| while (actual < capacity && has_remaining(in)) { | ||
| out[actual++] = decode(in); | ||
| out[actual++] = GorillaDecodeOutput<T, Output>::convert(decode(in)); | ||
| } | ||
| return common::E_OK; |
| 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 Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
jt2594838
left a comment
There was a problem hiding this comment.
Is this referable to the Java edition?
Yes, the core idea can be referenced for the Java implementation. 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. |
| // 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. |
| 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; |
| ASSERT_TRUE(decoder.has_remaining(wrapped)) | ||
| << "remaining=" << wrapped.remaining_size() | ||
| << " bits_left=" << decoder.bits_left_ | ||
| << " has_next=" << decoder.has_next_; |
| 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); |
| 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); |
| 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); |
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
floatanddoubleoutput buffers.ByteStreaminputs.Performance
Measured on ARM64 macOS using a Release build with
-O3and LTO. Results are the median of three runs over 500,000 smooth floating-point values.Profiling also showed that the temporary-buffer copy was eliminated and the proportion of samples in variable-width bit reading decreased significantly.
Compatibility
Tests
Added coverage for:
Validation results: