This is an automated email from the ASF dual-hosted git repository.

wesm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/master by this push:
     new c7baa7c  ARROW-9029: [C++] Implement BitBlockCounter for much faster 
block popcounts of bitmaps
c7baa7c is described below

commit c7baa7c4dc4dc61af4955fad3e09ca54e4e1c21c
Author: Wes McKinney <[email protected]>
AuthorDate: Thu Jun 4 08:30:44 2020 -0500

    ARROW-9029: [C++] Implement BitBlockCounter for much faster block popcounts 
of bitmaps
    
    The purpose of this class is to scan validity bitmaps in segments of 256 
bits at a time (a "run") and return the number of true values using popcount 
hardware instrinsics. Processing code can then switch between nullable / 
non-nullable processing paths -- the non-nullable paths are often much faster 
as they don't have to branch or check individual bits.
    
    In the benchmark I wrote here, this strategy starts to become faster than 
using BitmapReader naively with a null density somewhere between 2% and 10%. I 
implemented a naive "sum non-null values" algorithm using BitBlockCounter 
versus a similarly naive version that uses BitmapReader. The benchmark state 
parameter is the average number of array values for each null
    
    ```
    
---------------------------------------------------------------------------------
    Benchmark                                          Time           CPU 
Iterations
    
---------------------------------------------------------------------------------
    BitBlockCounterSumNotNull/8                  1566649 ns    1566653 ns       
 450   638.304M items/s
    BitBlockCounterSumNotNull/64                  926258 ns     926257 ns       
 745   1079.61M items/s
    BitBlockCounterSumNotNull/512                 355224 ns     355224 ns       
1978   2.74915G items/s
    BitBlockCounterSumNotNull/4096                 77070 ns      77070 ns       
8597   12.6711G items/s
    BitBlockCounterSumNotNull/32768                20838 ns      20838 ns      
32902   46.8638G items/s
    BitBlockCounterSumNotNull/65536                18538 ns      18538 ns      
37300   52.6791G items/s
    BitBlockCounterSumNotNullWithOffset/8        1563335 ns    1563346 ns       
 426   639.654M items/s
    BitBlockCounterSumNotNullWithOffset/64        831714 ns     831720 ns       
 825   1.17415G items/s
    BitBlockCounterSumNotNullWithOffset/512       339929 ns     339931 ns       
2035   2.87283G items/s
    BitBlockCounterSumNotNullWithOffset/4096       75150 ns      75148 ns       
9349   12.9952G items/s
    BitBlockCounterSumNotNullWithOffset/32768      28726 ns      28727 ns      
24736   33.9948G items/s
    BitBlockCounterSumNotNullWithOffset/65536      26921 ns      26921 ns      
26026   36.2753G items/s
    BitmapReaderSumNotNull/8                     1897087 ns    1897098 ns       
 368   527.121M items/s
    BitmapReaderSumNotNull/64                    1050133 ns    1050134 ns       
 669    952.26M items/s
    BitmapReaderSumNotNull/512                    960722 ns     960728 ns       
 744   1040.88M items/s
    BitmapReaderSumNotNull/4096                   949578 ns     949584 ns       
 727   1053.09M items/s
    BitmapReaderSumNotNull/32768                  946948 ns     946955 ns       
 722   1056.02M items/s
    BitmapReaderSumNotNull/65536                  960649 ns     960637 ns       
 739   1040.98M items/s
    BitmapReaderSumNotNullWithOffset/8           1972476 ns    1972457 ns       
 350   506.982M items/s
    BitmapReaderSumNotNullWithOffset/64          1131682 ns    1131691 ns       
 636   883.634M items/s
    BitmapReaderSumNotNullWithOffset/512          991733 ns     991736 ns       
 729   1008.33M items/s
    BitmapReaderSumNotNullWithOffset/4096         982855 ns     982862 ns       
 719   1017.44M items/s
    BitmapReaderSumNotNullWithOffset/32768        983555 ns     983556 ns       
 691   1016.72M items/s
    BitmapReaderSumNotNullWithOffset/65536       1005651 ns    1005658 ns       
 701   994.374M items/s
    ```
    
    So we can see that the performance is around the same when 1 in 8 values is 
null, but when 1 out of 512 is null, the block-counter version is 3x faster. 
And the performance goes up from there, up to 50x faster on data that has nulls 
but not very many. In my experience, data with < 1% nulls is extremely common, 
much more so than data with 5% or more nulls. This is obviously a tradeoff but 
IMHO one worth making.
    
    As a bonus, `BitBlockCounter` doesn't inline any code.
    
    The implementation of this can probably be improved and the benchmark as 
well so I welcome your collective help with this.
    
    Closes #7346 from wesm/ARROW-9029
    
    Lead-authored-by: Wes McKinney <[email protected]>
    Co-authored-by: Wes McKinney <[email protected]>
    Signed-off-by: Wes McKinney <[email protected]>
---
 cpp/src/arrow/util/bit_util.cc           |  44 ++++++++++++
 cpp/src/arrow/util/bit_util.h            |  27 +++++++
 cpp/src/arrow/util/bit_util_benchmark.cc | 116 +++++++++++++++++++++++++++++++
 cpp/src/arrow/util/bit_util_test.cc      | 102 +++++++++++++++++++++++++++
 4 files changed, 289 insertions(+)

diff --git a/cpp/src/arrow/util/bit_util.cc b/cpp/src/arrow/util/bit_util.cc
index 5ec278a..07951d0 100644
--- a/cpp/src/arrow/util/bit_util.cc
+++ b/cpp/src/arrow/util/bit_util.cc
@@ -598,5 +598,49 @@ Result<std::shared_ptr<Buffer>> 
BitmapAllButOne(MemoryPool* pool, int64_t length
   return std::move(buffer);
 }
 
+BitBlockCounter::Block BitBlockCounter::NextBlock() {
+  auto load_word = [](const uint8_t* bytes) -> uint64_t {
+    return BitUtil::ToLittleEndian(util::SafeLoadAs<uint64_t>(bytes));
+  };
+  auto shift_word = [](uint64_t current, uint64_t next, int64_t shift) -> 
uint64_t {
+    return (current >> shift) | (next << (64 - shift));
+  };
+
+  // When the offset is > 0, we need there to be a word beyond the last aligned
+  // word in the bitmap for the bit shifting logic.
+  const int64_t bits_required_to_scan_words = offset_ == 0 ? 256 : 256 + (64 - 
offset_);
+  if (bits_remaining_ < bits_required_to_scan_words) {
+    // End of the bitmap, leave it to the caller to decide how to best check
+    // these bits, no need to do redundant computation here.
+    const int16_t run_length = static_cast<int16_t>(bits_remaining_);
+    bits_remaining_ -= run_length;
+    return {run_length, static_cast<int16_t>(CountSetBits(bitmap_, offset_, 
run_length))};
+  }
+
+  int64_t total_popcount = 0;
+  if (offset_ == 0) {
+    total_popcount += __builtin_popcountll(load_word(bitmap_));
+    total_popcount += __builtin_popcountll(load_word(bitmap_ + 8));
+    total_popcount += __builtin_popcountll(load_word(bitmap_ + 16));
+    total_popcount += __builtin_popcountll(load_word(bitmap_ + 24));
+  } else {
+    auto current = load_word(bitmap_);
+    auto next = load_word(bitmap_ + 8);
+    total_popcount += __builtin_popcountll(shift_word(current, next, offset_));
+    current = next;
+    next = load_word(bitmap_ + 16);
+    total_popcount += __builtin_popcountll(shift_word(current, next, offset_));
+    current = next;
+    next = load_word(bitmap_ + 24);
+    total_popcount += __builtin_popcountll(shift_word(current, next, offset_));
+    current = next;
+    next = load_word(bitmap_ + 32);
+    total_popcount += __builtin_popcountll(shift_word(current, next, offset_));
+  }
+  bitmap_ += BitUtil::BytesForBits(kTargetBlockLength);
+  bits_remaining_ -= 256;
+  return {256, static_cast<int16_t>(total_popcount)};
+}
+
 }  // namespace internal
 }  // namespace arrow
diff --git a/cpp/src/arrow/util/bit_util.h b/cpp/src/arrow/util/bit_util.h
index 12b1a13..d1d6a52 100644
--- a/cpp/src/arrow/util/bit_util.h
+++ b/cpp/src/arrow/util/bit_util.h
@@ -1170,5 +1170,32 @@ class BitsetStack {
   std::vector<int> offsets_;
 };
 
+/// \brief A class that scans through a true/false bitmap to yield blocks of up
+/// to 256 bits at a time along with their popcount. This is used to accelerate
+/// processing of mostly-not-null array data.
+class ARROW_EXPORT BitBlockCounter {
+ public:
+  struct Block {
+    int16_t length;
+    int16_t popcount;
+  };
+
+  static constexpr int16_t kTargetBlockLength = 256;
+
+  BitBlockCounter(const uint8_t* bitmap, int64_t start_offset, int64_t length)
+      : bitmap_(bitmap + start_offset / 8),
+        bits_remaining_(length),
+        offset_(start_offset % 8) {}
+
+  /// \brief Return the next run of available bits, up to 256. The returned
+  /// pair contains the size of run and the number of true values
+  Block NextBlock();
+
+ private:
+  const uint8_t* bitmap_;
+  int64_t bits_remaining_;
+  int64_t offset_;
+};
+
 }  // namespace internal
 }  // namespace arrow
diff --git a/cpp/src/arrow/util/bit_util_benchmark.cc 
b/cpp/src/arrow/util/bit_util_benchmark.cc
index 81ccb47..ae4ddc1 100644
--- a/cpp/src/arrow/util/bit_util_benchmark.cc
+++ b/cpp/src/arrow/util/bit_util_benchmark.cc
@@ -22,10 +22,12 @@
 #include <bitset>
 #include <vector>
 
+#include "arrow/array/array_base.h"
 #include "arrow/buffer.h"
 #include "arrow/builder.h"
 #include "arrow/memory_pool.h"
 #include "arrow/testing/gtest_util.h"
+#include "arrow/testing/random.h"
 #include "arrow/testing/util.h"
 #include "arrow/util/bit_util.h"
 
@@ -387,6 +389,114 @@ static void BitmapEquals(benchmark::State& state) {
   state.SetBytesProcessed(state.iterations() * buffer_size);
 }
 
+template <int64_t Offset = 0>
+static void BitBlockCounterSumNotNull(benchmark::State& state) {
+  using internal::BitBlockCounter;
+
+  random::RandomArrayGenerator rng(/*seed=*/0);
+
+  const int64_t bitmap_length = 1 << 20;
+
+  // State parameter is the average number of total values for each false
+  // value. So 100 means that 1 out of 100 on average are false.
+  double true_probability = 1. - 1. / state.range(0);
+  auto arr = rng.Int8(bitmap_length, 0, 100, true_probability);
+
+  const uint8_t* bitmap = arr->null_bitmap_data();
+
+  // Compute the expected result
+  int64_t expected = 0;
+  const auto& int8_arr = static_cast<const Int8Array&>(*arr);
+  for (int64_t i = Offset; i < bitmap_length; ++i) {
+    if (int8_arr.IsValid(i)) {
+      expected += int8_arr.Value(i);
+    }
+  }
+  for (auto _ : state) {
+    BitBlockCounter scanner(bitmap, Offset, bitmap_length - Offset);
+    int64_t result = 0;
+    int64_t position = Offset;
+    while (true) {
+      BitBlockCounter::Block block = scanner.NextBlock();
+      if (block.length == 0) {
+        break;
+      }
+      if (block.length == block.popcount) {
+        // All not-null
+        for (int64_t i = 0; i < block.length; ++i) {
+          result += int8_arr.Value(position + i);
+        }
+      } else if (block.popcount > 0) {
+        // Some but not all not-null
+        for (int64_t i = 0; i < block.length; ++i) {
+          if (BitUtil::GetBit(bitmap, position + i)) {
+            result += int8_arr.Value(position + i);
+          }
+        }
+      }
+      position += block.length;
+    }
+    // Sanity check
+    if (result != expected) {
+      std::abort();
+    }
+  }
+  state.SetItemsProcessed(state.iterations() * bitmap_length);
+}
+
+template <int64_t Offset = 0>
+static void BitmapReaderSumNotNull(benchmark::State& state) {
+  random::RandomArrayGenerator rng(/*seed=*/0);
+
+  const int64_t bitmap_length = 1 << 20;
+
+  // State parameter is the average number of total values for each false
+  // value. So 100 means that 1 out of 100 on average are false.
+  double true_probability = 1. - 1. / state.range(0);
+  auto arr = rng.Int8(bitmap_length, 0, 100, true_probability);
+
+  const uint8_t* bitmap = arr->null_bitmap_data();
+  // Compute the expected result
+  int64_t expected = 0;
+  const auto& int8_arr = static_cast<const Int8Array&>(*arr);
+  for (int64_t i = Offset; i < bitmap_length; ++i) {
+    if (int8_arr.IsValid(i)) {
+      expected += int8_arr.Value(i);
+    }
+  }
+  for (auto _ : state) {
+    internal::BitmapReader bit_reader(bitmap, Offset, bitmap_length - Offset);
+    int64_t result = 0;
+    for (int64_t i = Offset; i < bitmap_length; ++i) {
+      if (bit_reader.IsSet()) {
+        result += int8_arr.Value(i);
+      }
+      bit_reader.Next();
+    }
+    // Sanity check
+    if (result != expected) {
+      std::abort();
+    }
+  }
+  state.SetItemsProcessed(state.iterations() * bitmap_length);
+}
+
+static void BitBlockCounterSumNotNull(benchmark::State& state) {
+  BitBlockCounterSumNotNull<0>(state);
+}
+
+static void BitBlockCounterSumNotNullWithOffset(benchmark::State& state) {
+  BitBlockCounterSumNotNull<4>(state);
+}
+
+static void BitmapReaderSumNotNull(benchmark::State& state) {
+  BitmapReaderSumNotNull<0>(state);
+}
+
+static void BitmapReaderSumNotNullWithOffset(benchmark::State& state) {
+  BitmapReaderSumNotNull<4>(state);
+}
+
 static void BitmapEqualsWithoutOffset(benchmark::State& state) { 
BitmapEquals<0>(state); }
 
 static void BitmapEqualsWithOffset(benchmark::State& state) { 
BitmapEquals<4>(state); }
@@ -424,6 +534,12 @@ BENCHMARK(CopyBitmapWithOffsetBoth)->Arg(kBufferSize);
 BENCHMARK(BitmapEqualsWithoutOffset)->Arg(kBufferSize);
 BENCHMARK(BitmapEqualsWithOffset)->Arg(kBufferSize);
 
+// Range value: average number of total values per null
+BENCHMARK(BitBlockCounterSumNotNull)->Range(8, 1 << 16);
+BENCHMARK(BitBlockCounterSumNotNullWithOffset)->Range(8, 1 << 16);
+BENCHMARK(BitmapReaderSumNotNull)->Range(8, 1 << 16);
+BENCHMARK(BitmapReaderSumNotNullWithOffset)->Range(8, 1 << 16);
+
 #define AND_BENCHMARK_RANGES                      \
   {                                               \
     {kBufferSize * 4, kBufferSize * 16}, { 0, 2 } \
diff --git a/cpp/src/arrow/util/bit_util_test.cc 
b/cpp/src/arrow/util/bit_util_test.cc
index 8b87e69..45f6daf 100644
--- a/cpp/src/arrow/util/bit_util_test.cc
+++ b/cpp/src/arrow/util/bit_util_test.cc
@@ -1367,5 +1367,107 @@ TEST(Bitmap, VisitWordsAnd) {
     }
   }
 }
+
+class TestBitBlockCounter : public ::testing::Test {
+ public:
+  void Create(int64_t nbytes, int64_t offset, int64_t length) {
+    ASSERT_OK_AND_ASSIGN(buf_, AllocateBuffer(nbytes));
+    // Start with data zeroed out
+    std::memset(buf_->mutable_data(), 0, nbytes);
+    scanner_.reset(new BitBlockCounter(buf_->data(), offset, length));
+  }
+
+ protected:
+  std::shared_ptr<Buffer> buf_;
+  std::unique_ptr<BitBlockCounter> scanner_;
+};
+
+static constexpr int64_t kWordSize = 64;
+
+TEST_F(TestBitBlockCounter, Basics) {
+  const int64_t nbytes = 1024;
+
+  Create(nbytes, 0, nbytes * 8);
+
+  int64_t bits_scanned = 0;
+  for (int64_t i = 0; i < nbytes / 32; ++i) {
+    BitBlockCounter::Block block = scanner_->NextBlock();
+    ASSERT_EQ(block.length, 4 * kWordSize);
+    ASSERT_EQ(block.popcount, 0);
+    bits_scanned += block.length;
+  }
+  ASSERT_EQ(bits_scanned, 1024 * 8);
+
+  auto block = scanner_->NextBlock();
+  ASSERT_EQ(block.length, 0);
+  ASSERT_EQ(block.popcount, 0);
+}
+
+TEST_F(TestBitBlockCounter, Offsets) {
+  auto CheckWithOffset = [&](int64_t offset) {
+    const int64_t nwords = 15;
+
+    const int64_t total_bytes = nwords * 8 + 1;
+    // Trim a bit from the end of the bitmap so we can check the remainder bits
+    // behavior
+    Create(total_bytes, offset, nwords * kWordSize - offset - 1);
+
+    // Start with data all set
+    std::memset(buf_->mutable_data(), 0xFF, total_bytes);
+
+    BitBlockCounter::Block block = scanner_->NextBlock();
+    ASSERT_EQ(4 * kWordSize, block.length);
+    ASSERT_EQ(block.popcount, 256);
+
+    // Add some false values to the next 3 shifted words
+    BitUtil::SetBitTo(buf_->mutable_data(), 4 * kWordSize + offset, false);
+    BitUtil::SetBitTo(buf_->mutable_data(), 5 * kWordSize + offset, false);
+    BitUtil::SetBitTo(buf_->mutable_data(), 6 * kWordSize + offset, false);
+    block = scanner_->NextBlock();
+
+    ASSERT_EQ(block.length, 256);
+    ASSERT_EQ(block.popcount, 253);
+
+    BitUtil::SetBitsTo(buf_->mutable_data(), 8 * kWordSize + offset, 2 * 
kWordSize,
+                       false);
+
+    block = scanner_->NextBlock();
+    ASSERT_EQ(block.length, 256);
+    ASSERT_EQ(block.popcount, 128);
+
+    // Last block
+    block = scanner_->NextBlock();
+    ASSERT_EQ(block.length, 3 * kWordSize - offset - 1);
+    ASSERT_EQ(block.length, block.popcount);
+
+    // We can keep calling NextBlock safely
+    block = scanner_->NextBlock();
+    ASSERT_EQ(block.length, 0);
+    ASSERT_EQ(block.popcount, 0);
+  };
+
+  for (int64_t offset_i = 0; offset_i < 7; ++offset_i) {
+    CheckWithOffset(offset_i);
+  }
+}
+
+TEST_F(TestBitBlockCounter, RandomData) {
+  const int64_t nbytes = 1024;
+  auto buffer = *AllocateBuffer(nbytes);
+  random_bytes(nbytes, 0, buffer->mutable_data());
+
+  auto CheckWithOffset = [&](int64_t offset) {
+    BitBlockCounter scanner(buffer->data(), offset, nbytes * 8 - offset);
+    for (int64_t i = 0; i < nbytes / 32; ++i) {
+      BitBlockCounter::Block block = scanner.NextBlock();
+      ASSERT_EQ(block.popcount,
+                CountSetBits(buffer->data(), i * 256 + offset, block.length));
+    }
+  };
+  for (int64_t offset_i = 0; offset_i < 7; ++offset_i) {
+    CheckWithOffset(offset_i);
+  }
+}
+
 }  // namespace internal
 }  // namespace arrow

Reply via email to