pitrou commented on code in PR #55449:
URL: https://github.com/apache/arrow/pull/55449#discussion_r4522650106


##########
cpp/src/arrow/util/alp/alp_compression.cc:
##########
@@ -331,7 +323,17 @@ AlpExponentAndFactor 
AlpCompression<T>::FindBestExponentAndFactor(
     return combinations.front();
   }
 
-  const std::vector<T> sample = CreateSample(inputs);
+  // The sample is capped at 256 values. Keep it on the stack to avoid a heap
+  // allocation for every vector parameter search.
+  std::array<T, AlpSamplingOptions::kSamplesPerChunk> sample_storage;

Review Comment:
   Unrelated, but it's weird that it's called `AlpSamplingOptions` while it 
contains compile-time constants.



##########
cpp/src/arrow/util/alp/alp_compression.cc:
##########
@@ -58,6 +59,20 @@ class AlpInlines {
   using EncodedUnsigned = typename Constants::EncodedUnsigned;
   using EncodedSigned = typename Constants::EncodedSigned;
 
+  struct Multipliers {
+    T encode_exponent;
+    T encode_factor;
+    T decode_factor;
+    T decode_exponent;
+  };
+
+  static Multipliers GetMultipliers(const AlpExponentAndFactor 
exponent_and_factor) {
+    return {Constants::GetExponent(exponent_and_factor.exponent),
+            Constants::GetFactor(exponent_and_factor.factor),
+            Constants::GetExponent(exponent_and_factor.factor),
+            Constants::GetFactor(exponent_and_factor.exponent)};

Review Comment:
   Unrelated to this PR, but this reads weird until one realizes the 
definitions of `GetExponent` and `GetFactor` (misnamed functions IMHO).



##########
cpp/src/arrow/util/alp/alp_compression.cc:
##########


Review Comment:
   Unrelated to this PR, but what's the point of the "early exit" thing?
   Why would we not test all pre-selected combinations (especially as 
`kMaxCombinations = 5`)?



##########
cpp/src/arrow/util/alp/alp_sampler_internal.h:
##########
@@ -46,9 +46,11 @@ class ARROW_EXPORT AlpSampler {
 
  private:
   /// Adds one sampling chunk. Only the first
-  /// AlpFormatConstants::kDefaultVectorSize elements are examined.
+  /// AlpSamplingOptions::kSampleWindowSize elements are examined.

Review Comment:
   Rather `sampling_window_size` elements, no?



##########
cpp/src/arrow/util/alp/alp_sampler.cc:
##########


Review Comment:
   I'm curious, is `chunk_samples_` unbounded, or does it stop accumulating 
after a certain number of samples? It's not clear by reading this code.



##########
cpp/src/arrow/util/alp/alp_test.cc:
##########
@@ -776,11 +774,34 @@ TYPED_TEST(AlpSamplerTest, PresetGenerationDecimalData) {
       output, std::vector<TypeParam>(data.begin(), data.begin() + 
kNumValues)));
 }
 
+TYPED_TEST(AlpSamplerTest, SamplingWindowSize) {
+  constexpr int64_t kChunkSize = AlpSamplingOptions::kChunkSize;
+  const std::vector<TypeParam> nan_chunk(kChunkSize,
+                                         
std::numeric_limits<TypeParam>::quiet_NaN());
+  const std::vector<TypeParam> decimal_chunk(kChunkSize, 
static_cast<TypeParam>(1.25));
+  const auto decimal_preset = 
AlpCompression<TypeParam>::MakePreset({decimal_chunk});
+
+  // The sampler targets eight chunks per window. An eight-chunk window samples
+  // both inputs, but only decimal_chunk can contribute usable parameters.
+  AlpSampler<TypeParam> every_chunk(/*sampling_window_size=*/8 * kChunkSize);
+  every_chunk.AddSample(nan_chunk);
+  every_chunk.AddSample(decimal_chunk);
+  EXPECT_EQ(decimal_preset.combinations, 
every_chunk.MakePreset().combinations);
+
+  // A sixteen-chunk window samples every other chunk, skipping decimal_chunk.
+  // With only NaNs sampled, the preset falls back to identity.
+  AlpSampler<TypeParam> every_other_chunk(/*sampling_window_size=*/16 * 
kChunkSize);

Review Comment:
   Is this because `kSampleChunksPerWindow = 8`?



##########
cpp/src/arrow/util/alp/alp_compression.cc:
##########
@@ -563,17 +573,18 @@ void AlpCompression<T>::Decompress(const 
AlpEncodedVectorView<T>& encoded_view,
   // Read the exponent/factor once. AlpInfo stores them as uint8_t, which may
   // alias the output stores, so reading them inside the loop blocks 
vectorization.
   const AlpExponentAndFactor exponent_and_factor = 
alp_info.GetExponentAndFactor();
+  const auto multipliers = AlpInlines<T>::GetMultipliers(exponent_and_factor);
   for (size_t i = 0; i < output.size(); ++i) {
     const EncodedUnsigned unfored_value = scratch[i] + frame_of_ref;
     const EncodedSigned signed_value = 
util::SafeCopy<EncodedSigned>(unfored_value);
-    output[i] = AlpInlines<T>::DecodeValue(signed_value, exponent_and_factor);
+    output[i] = AlpInlines<T>::DecodeValue(signed_value, multipliers);
   }

Review Comment:
   This is the code path where I would expect unrolling and/or vectorization to 
be beneficial, but benchmarks don't seem conclusive:
   ```
   BM_AlpDecodingFloat/1024          815 ns          815 ns       345130 
bytes_per_second=4.68104Gi/s items_per_second=1.25656G/s
   BM_AlpDecodingFloat/4096         2681 ns         2681 ns       102889 
bytes_per_second=5.69219Gi/s items_per_second=1.52798G/s
   BM_AlpDecodingFloat/32768       20041 ns        20034 ns        13779 
bytes_per_second=6.09326Gi/s items_per_second=1.63565G/s
   BM_AlpDecodingFloat/65536       40186 ns        40183 ns         6992 
bytes_per_second=6.07578Gi/s items_per_second=1.63096G/s
   
   BM_AlpDecodingDouble/1024         878 ns          878 ns       318022 
bytes_per_second=8.68788Gi/s items_per_second=1.16607G/s
   BM_AlpDecodingDouble/4096        2870 ns         2869 ns        96807 
bytes_per_second=10.6353Gi/s items_per_second=1.42745G/s
   BM_AlpDecodingDouble/32768      21227 ns        21216 ns        13090 
bytes_per_second=11.5073Gi/s items_per_second=1.54448G/s
   BM_AlpDecodingDouble/65536      42987 ns        42934 ns         6532 
bytes_per_second=11.3728Gi/s items_per_second=1.52643G/s
   ```
   



##########
cpp/src/parquet/encoder.cc:
##########
@@ -1013,40 +1038,56 @@ class AlpEncoder : public EncoderImpl, virtual public 
TypedEncoder<DType> {
  public:
   using T = typename DType::c_type;
   using ArrowType = typename EncodingTraits<DType>::ArrowType;
+  using Codec = ::arrow::util::alp::AlpCodec<T>;
+  using EncodingPreset = ::arrow::util::alp::AlpEncodingPreset;
+  using Sampler = ::arrow::util::alp::AlpSampler<T>;
   using TypedEncoder<DType>::Put;
 
   static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>,
                 "ALP only supports float and double types");
 
-  explicit AlpEncoder(const ColumnDescriptor* descr,
-                      ::arrow::MemoryPool* pool = 
::arrow::default_memory_pool())
-      : EncoderImpl(descr, Encoding::ALP, pool), sink_{pool} {}
+  explicit AlpEncoder(const ColumnDescriptor* descr, ::arrow::MemoryPool* pool,
+                      const AlpOptions& alp_options)
+      : EncoderImpl(descr, Encoding::ALP, pool),
+        sink_{pool},
+        alp_options_(alp_options),
+        sampler_(alp_options.sampling_window_size) {}
 
   // TODO(GH-48701): use a ratio estimate instead of reporting the raw buffer 
size
   int64_t EstimatedDataEncodedSize() override { return sink_.length(); }
 
   std::shared_ptr<Buffer> FlushValues() override {
-    // TODO(GH-48701): allow configuring the vector size.
-    constexpr auto kVectorSize =
-        ::arrow::util::alp::AlpFormatConstants::kDefaultVectorSize;
-
-    // An all-null optional page still has to produce a header-only page.
     const int64_t num_elements = sink_.length() / 
static_cast<int64_t>(sizeof(T));
+    const std::span<const T> input{reinterpret_cast<const T*>(sink_.data()),
+                                   static_cast<size_t>(num_elements)};
+
+    if (num_elements > 0 && !preset_.has_value()) {
+      sampler_.AddSample(input);
+      sampled_values_ += num_elements;
+      if (sampled_values_ >= alp_options_.sampling_window_size) {
+        preset_ = sampler_.MakePreset();
+      }
+    }
+
+    EncodingPreset page_preset;
+    if (!preset_.has_value()) {
+      // Use a page-specific sampler to avoid affecting the global preset.

Review Comment:
   Why do we not use a page-specific sampler for all pages? Is it too expensive?



##########
cpp/src/parquet/encoder.cc:
##########
@@ -1062,13 +1103,23 @@ class AlpEncoder : public EncoderImpl, virtual public 
TypedEncoder<DType> {
 
   void PutSpaced(const T* src, int num_values, const uint8_t* valid_bits,
                  int64_t valid_bits_offset) override {
+    if (num_values == 0) {
+      return;
+    }

Review Comment:
   Not sure this is useful, the same test is already in `Put`



##########
cpp/src/parquet/encoder.cc:
##########
@@ -1013,40 +1038,56 @@ class AlpEncoder : public EncoderImpl, virtual public 
TypedEncoder<DType> {
  public:
   using T = typename DType::c_type;
   using ArrowType = typename EncodingTraits<DType>::ArrowType;
+  using Codec = ::arrow::util::alp::AlpCodec<T>;
+  using EncodingPreset = ::arrow::util::alp::AlpEncodingPreset;
+  using Sampler = ::arrow::util::alp::AlpSampler<T>;
   using TypedEncoder<DType>::Put;
 
   static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>,
                 "ALP only supports float and double types");
 
-  explicit AlpEncoder(const ColumnDescriptor* descr,
-                      ::arrow::MemoryPool* pool = 
::arrow::default_memory_pool())
-      : EncoderImpl(descr, Encoding::ALP, pool), sink_{pool} {}
+  explicit AlpEncoder(const ColumnDescriptor* descr, ::arrow::MemoryPool* pool,
+                      const AlpOptions& alp_options)
+      : EncoderImpl(descr, Encoding::ALP, pool),
+        sink_{pool},
+        alp_options_(alp_options),
+        sampler_(alp_options.sampling_window_size) {}
 
   // TODO(GH-48701): use a ratio estimate instead of reporting the raw buffer 
size
   int64_t EstimatedDataEncodedSize() override { return sink_.length(); }
 
   std::shared_ptr<Buffer> FlushValues() override {
-    // TODO(GH-48701): allow configuring the vector size.
-    constexpr auto kVectorSize =
-        ::arrow::util::alp::AlpFormatConstants::kDefaultVectorSize;
-
-    // An all-null optional page still has to produce a header-only page.
     const int64_t num_elements = sink_.length() / 
static_cast<int64_t>(sizeof(T));
+    const std::span<const T> input{reinterpret_cast<const T*>(sink_.data()),
+                                   static_cast<size_t>(num_elements)};
+
+    if (num_elements > 0 && !preset_.has_value()) {
+      sampler_.AddSample(input);
+      sampled_values_ += num_elements;
+      if (sampled_values_ >= alp_options_.sampling_window_size) {
+        preset_ = sampler_.MakePreset();
+      }
+    }
+
+    EncodingPreset page_preset;
+    if (!preset_.has_value()) {
+      // Use a page-specific sampler to avoid affecting the global preset.
+      Sampler page_sampler(alp_options_.sampling_window_size);
+      page_sampler.AddSample(input);
+      page_preset = page_sampler.MakePreset();
+    }
+    const auto& preset = preset_.has_value() ? *preset_ : page_preset;
+
     PARQUET_ASSIGN_OR_THROW(
         int64_t max_comp_size,
-        ::arrow::util::alp::AlpCodec<T>::GetMaxCompressedSize(num_elements, 
kVectorSize));
-
+        Codec::GetMaxCompressedSize(num_elements, alp_options_.vector_size));
     PARQUET_ASSIGN_OR_THROW(
-        std::shared_ptr<ResizableBuffer> compressed_buffer,
+        auto compressed_buffer,
         ::arrow::AllocateResizableBuffer(max_comp_size, this->memory_pool()));
-
-    PARQUET_ASSIGN_OR_THROW(
-        const int64_t compressed_size,
-        ::arrow::util::alp::AlpCodec<T>::Encode(
-            {reinterpret_cast<const T*>(sink_.data()), 
static_cast<size_t>(num_elements)},
-            kVectorSize,
-            {compressed_buffer->mutable_data(), 
static_cast<size_t>(max_comp_size)}));
-
+    PARQUET_ASSIGN_OR_THROW(const int64_t compressed_size,
+                            Codec::Encode(input, preset, 
alp_options_.vector_size,
+                                          {compressed_buffer->mutable_data(),
+                                           
static_cast<size_t>(max_comp_size)}));

Review Comment:
   Can write `compressed_buffer->mutable_span_as<uint8_t>()`



##########
cpp/src/arrow/util/alp/alp_compression.cc:
##########
@@ -524,16 +525,25 @@ template <typename TargetType>
 void AlpCompression<T>::PatchExceptions(
     std::span<const T> exceptions,
     std::span<const AlpFormatConstants::PositionType> exception_positions,
-    std::span<TargetType> outputs) {
+    const bool positions_strictly_increasing, std::span<TargetType> outputs) {
   ARROW_DCHECK_EQ(exceptions.size(), exception_positions.size());
-  // `ivdep` assumes the positions are distinct. The encoder writes them in
-  // order, and loading a page rejects positions that do not increase.
-  int64_t exception_idx = 0;
-  ARROW_ALP_UNROLL_AND_ASSUME_INDEPENDENT
-  for (const AlpFormatConstants::PositionType exception_position : 
exception_positions) {
-    ARROW_DCHECK_LT(exception_position, outputs.size());
-    outputs[exception_position] = static_cast<T>(exceptions[exception_idx]);
-    exception_idx++;
+  const size_t num_exceptions = exception_positions.size();
+  const auto patch_exception = [&](size_t i) {
+    const auto position = exception_positions[i];
+    ARROW_DCHECK_LT(position, outputs.size());
+    outputs[position] = static_cast<T>(exceptions[i]);
+  };
+  if (positions_strictly_increasing) {
+    // The encoder emits strictly increasing positions. Use the vectorized
+    // fast path for that common case.

Review Comment:
   Was the "vectorized fast path" measured to bring substantial benefits? After 
all, there should not be many exceptions...



##########
cpp/src/parquet/encoder.cc:
##########
@@ -1062,13 +1103,23 @@ class AlpEncoder : public EncoderImpl, virtual public 
TypedEncoder<DType> {
 
   void PutSpaced(const T* src, int num_values, const uint8_t* valid_bits,
                  int64_t valid_bits_offset) override {
+    if (num_values == 0) {
+      return;
+    }
     if (valid_bits != NULLPTR) {
-      PARQUET_ASSIGN_OR_THROW(auto buffer, ::arrow::AllocateBuffer(num_values 
* sizeof(T),
-                                                                   
this->memory_pool()));
+      const int num_valid_values = 
static_cast<int>(::arrow::internal::CountSetBits(
+          valid_bits, valid_bits_offset, static_cast<int64_t>(num_values)));
+      if (num_valid_values == 0) {
+        return;
+      }
+      PARQUET_ASSIGN_OR_THROW(
+          auto buffer,
+          ::arrow::AllocateBuffer(num_valid_values * sizeof(T), 
this->memory_pool()));
       T* data = buffer->template mutable_data_as<T>();
-      const int num_valid_values = ::arrow::util::internal::SpacedCompress<T>(
+      const int compacted_values = ::arrow::util::internal::SpacedCompress<T>(
           src, num_values, valid_bits, valid_bits_offset, data);
-      Put(data, num_valid_values);
+      ARROW_DCHECK_EQ(compacted_values, num_valid_values);
+      Put(data, compacted_values);

Review Comment:
   `Put` will merely copy `compacted_values` into `sink_`, so perhaps you can 
call `SpacedCompress` directly into `sink_`?



##########
cpp/src/arrow/util/alp/alp_sampler_internal.h:
##########
@@ -34,7 +34,7 @@ namespace arrow::util::alp {
 template <AlpFloatingType T>
 class ARROW_EXPORT AlpSampler {
  public:
-  AlpSampler() = default;
+  explicit AlpSampler(int64_t sampling_window_size);

Review Comment:
   I'm not sure I understand how this relates to `kSampleWindowSize`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to