streaming support for EF compression
authorPhilip Pronin <philipp@fb.com>
Sun, 3 Aug 2014 00:33:03 +0000 (17:33 -0700)
committerSara Golemon <sgolemon@fb.com>
Thu, 14 Aug 2014 18:49:04 +0000 (11:49 -0700)
Test Plan: fbconfig -r folly/experimental/test:eliasfano_test && fbmake opt -j32

Reviewed By: lucian@fb.com

Subscribers: chaoyc, search-fbcode-diffs@, unicorn-diffs@

FB internal diff: D1474625

Tasks: 4828866

folly/experimental/EliasFanoCoding.h
folly/experimental/test/CodingTestUtils.h
folly/experimental/test/EliasFanoCodingTest.cpp

index dd83a617e0345ff6c7250696a10887df219eef05..c7406fd00bc84365a31daa5efde9e093d99cf918 100644 (file)
@@ -32,7 +32,6 @@
 #error EliasFanoCoding.h requires x86_64
 #endif
 
-#include <algorithm>
 #include <cstdlib>
 #include <limits>
 #include <type_traits>
@@ -51,8 +50,7 @@
 namespace folly { namespace compression {
 
 struct EliasFanoCompressedList {
-  EliasFanoCompressedList()
-    : size(0), numLowerBits(0) { }
+  EliasFanoCompressedList() { }
 
   void free() {
     ::free(const_cast<unsigned char*>(lower.data()));
@@ -61,8 +59,8 @@ struct EliasFanoCompressedList {
     ::free(const_cast<unsigned char*>(forwardPointers.data()));
   }
 
-  size_t size;
-  uint8_t numLowerBits;
+  size_t size = 0;
+  uint8_t numLowerBits = 0;
 
   // WARNING: EliasFanoCompressedList has no ownership of
   // lower, upper, skipPointers and forwardPointers.
@@ -106,30 +104,29 @@ struct EliasFanoEncoder {
     return folly::findLastSet(upperBound / size) - 1;
   }
 
+  // Requires: input range (begin, end) is sorted (encoding
+  // crashes if it's not).
   // WARNING: encode() mallocates lower, upper, skipPointers
   // and forwardPointers. As EliasFanoCompressedList has
   // no ownership of them, you need to call free() explicitly.
-  static void encode(const ValueType* list, size_t size,
-                     EliasFanoCompressedList& result) {
-    encode(list, list + size, result);
-  }
-
-  // Range (begin, end) should be sorted.
   template <class RandomAccessIterator>
-  static void encode(RandomAccessIterator begin,
-                     RandomAccessIterator end,
-                     EliasFanoCompressedList& result) {
-    CHECK(std::is_sorted(begin, end));
-
-    auto list = begin;
-    const size_t size = end - begin;
+  static EliasFanoCompressedList encode(RandomAccessIterator begin,
+                                        RandomAccessIterator end) {
+    if (begin == end) {
+      return EliasFanoCompressedList();
+    }
+    EliasFanoEncoder encoder(end - begin, *(end - 1));
+    for (; begin != end; ++begin) {
+      encoder.add(*begin);
+    }
+    return encoder.finish();
+  }
 
+  EliasFanoEncoder(size_t size, ValueType upperBound) {
     if (size == 0) {
-      result = EliasFanoCompressedList();
       return;
     }
 
-    const ValueType upperBound = list[size - 1];
     uint8_t numLowerBits = defaultNumLowerBits(upperBound, size);
 
     // This is detail::writeBits56 limitation.
@@ -144,14 +141,8 @@ struct EliasFanoEncoder {
 
     // *** Lower bits.
     const size_t lowerSize = (numLowerBits * size + 7) / 8;
-    unsigned char* lower = nullptr;
     if (lowerSize > 0) {  // numLowerBits != 0
-      lower = static_cast<unsigned char*>(calloc(lowerSize + 7, 1));
-      const ValueType lowerMask = (ValueType(1) << numLowerBits) - 1;
-      for (size_t i = 0; i < size; ++i) {
-        const ValueType lowerBits = list[i] & lowerMask;
-        writeBits56(lower, i * numLowerBits, numLowerBits, lowerBits);
-      }
+      lower_ = static_cast<unsigned char*>(calloc(lowerSize + 7, 1));
     }
 
     // *** Upper bits.
@@ -161,22 +152,13 @@ struct EliasFanoEncoder {
       (upperBound >> numLowerBits) +  // Number of 0-bits to be stored.
       size;                           // 1-bits.
     const size_t upperSize = (upperSizeBits + 7) / 8;
-    unsigned char* const upper =
-      static_cast<unsigned char*>(calloc(upperSize + 7, 1));
-    for (size_t i = 0; i < size; ++i) {
-      const ValueType upperBits = list[i] >> numLowerBits;
-      const size_t pos = upperBits + i;  // upperBits 0-bits and (i + 1) 1-bits.
-      upper[pos / 8] |= 1U << (pos % 8);
-    }
+    upper_ = static_cast<unsigned char*>(calloc(upperSize + 7, 1));
 
     // *** Skip pointers.
     // Store (1-indexed) position of every skipQuantum-th
     // 0-bit in upper bits sequence.
-    SkipValueType* skipPointers = nullptr;
     size_t numSkipPointers = 0;
     /* static */ if (skipQuantum != 0) {
-      // Workaround to avoid 'division by zero' compile-time error.
-      constexpr size_t q = skipQuantum ?: 1;
       /* static */ if (kVersion > 0) {
         CHECK_LT(size, std::numeric_limits<SkipValueType>::max());
       } else {
@@ -186,33 +168,18 @@ struct EliasFanoEncoder {
       // more serialization-friendly way (upperSizeBits isn't known outside of
       // this function, unlike upperSize; thus numSkipPointers could easily be
       // deduced from upperSize).
-      numSkipPointers = (8 * upperSize - size) / q;
-      skipPointers = static_cast<SkipValueType*>(
+      numSkipPointers = (8 * upperSize - size) / (skipQuantum ?: 1);
+      skipPointers_ = static_cast<SkipValueType*>(
           numSkipPointers == 0
             ? nullptr
             : calloc(numSkipPointers, sizeof(SkipValueType)));
-
-      for (size_t i = 0, pos = 0; i < size; ++i) {
-        const ValueType upperBits = list[i] >> numLowerBits;
-        for (; (pos + 1) * q <= upperBits; ++pos) {
-          /* static */ if (kVersion > 0) {
-            // Since version 1, just the number of preceding 1-bits is stored.
-            skipPointers[pos] = i;
-          } else {
-            skipPointers[pos] = i + (pos + 1) * q;
-          }
-        }
-      }
     }
 
     // *** Forward pointers.
     // Store (1-indexed) position of every forwardQuantum-th
     // 1-bit in upper bits sequence.
-    SkipValueType* forwardPointers = nullptr;
     size_t numForwardPointers = 0;
     /* static */ if (forwardQuantum != 0) {
-      // Workaround to avoid 'division by zero' compile-time error.
-      constexpr size_t q = forwardQuantum ?: 1;
       /* static */ if (kVersion > 0) {
         CHECK_LT(upperBound >> numLowerBits,
                  std::numeric_limits<SkipValueType>::max());
@@ -220,34 +187,74 @@ struct EliasFanoEncoder {
         CHECK_LT(upperSizeBits, std::numeric_limits<SkipValueType>::max());
       }
 
-      numForwardPointers = size / q;
-      forwardPointers = static_cast<SkipValueType*>(
+      // '?: 1' is a workaround for false 'division by zero' compile-time error.
+      numForwardPointers = size / (forwardQuantum ?: 1);
+      forwardPointers_ = static_cast<SkipValueType*>(
         numForwardPointers == 0
           ? nullptr
           : malloc(numForwardPointers * sizeof(SkipValueType)));
+    }
+
+    // *** Result.
+    result_.size = size;
+    result_.numLowerBits = numLowerBits;
+    result_.lower.reset(lower_, lowerSize);
+    result_.upper.reset(upper_, upperSize);
+    result_.skipPointers.reset(
+        reinterpret_cast<unsigned char*>(skipPointers_),
+        numSkipPointers * sizeof(SkipValueType));
+    result_.forwardPointers.reset(
+        reinterpret_cast<unsigned char*>(forwardPointers_),
+        numForwardPointers * sizeof(SkipValueType));
+  }
+
+  void add(ValueType value) {
+    CHECK_GE(value, lastValue_);
 
-      for (size_t i = q - 1, pos = 0; i < size; i += q, ++pos) {
-        const ValueType upperBits = list[i] >> numLowerBits;
+    const auto numLowerBits = result_.numLowerBits;
+    const ValueType upperBits = value >> numLowerBits;
+
+    // Upper sequence consists of upperBits 0-bits and (size_ + 1) 1-bits.
+    const size_t pos = upperBits + size_;
+    upper_[pos / 8] |= 1U << (pos % 8);
+    // Append numLowerBits bits to lower sequence.
+    if (numLowerBits != 0) {
+      const ValueType lowerBits = value & ((ValueType(1) << numLowerBits) - 1);
+      writeBits56(lower_, size_ * numLowerBits, numLowerBits, lowerBits);
+    }
+
+    /* static */ if (skipQuantum != 0) {
+      while ((skipPointersSize_ + 1) * skipQuantum <= upperBits) {
+        /* static */ if (kVersion > 0) {
+          // Since version 1, just the number of preceding 1-bits is stored.
+          skipPointers_[skipPointersSize_] = size_;
+        } else {
+          skipPointers_[skipPointersSize_] =
+            size_ + (skipPointersSize_ + 1) * skipQuantum;
+        }
+        ++skipPointersSize_;
+      }
+    }
+
+    /* static */ if (forwardQuantum != 0) {
+      if ((size_ + 1) % forwardQuantum == 0) {
+        const auto pos = size_ / forwardQuantum;
         /* static */ if (kVersion > 0) {
           // Since version 1, just the number of preceding 0-bits is stored.
-          forwardPointers[pos] = upperBits;
+          forwardPointers_[pos] = upperBits;
         } else {
-          forwardPointers[pos] = upperBits + i + 1;
+          forwardPointers_[pos] = upperBits + size_ + 1;
         }
       }
     }
 
-    // *** Result.
-    result.size = size;
-    result.numLowerBits = numLowerBits;
-    result.lower.reset(lower, lowerSize);
-    result.upper.reset(upper, upperSize);
-    result.skipPointers.reset(
-        reinterpret_cast<unsigned char*>(skipPointers),
-        numSkipPointers * sizeof(SkipValueType));
-    result.forwardPointers.reset(
-        reinterpret_cast<unsigned char*>(forwardPointers),
-        numForwardPointers * sizeof(SkipValueType));
+    lastValue_ = value;
+    ++size_;
+  }
+
+  const EliasFanoCompressedList& finish() const {
+    CHECK_EQ(size_, result_.size);
+    return result_;
   }
 
  private:
@@ -261,6 +268,17 @@ struct EliasFanoEncoder {
     ptrv |= value << (pos % 8);
     folly::storeUnaligned<uint64_t>(ptr, ptrv);
   }
+
+  unsigned char* lower_ = nullptr;
+  unsigned char* upper_ = nullptr;
+  SkipValueType* skipPointers_ = nullptr;
+  SkipValueType* forwardPointers_ = nullptr;
+
+  ValueType lastValue_ = 0;
+  size_t size_ = 0;
+  size_t skipPointersSize_ = 0;
+
+  EliasFanoCompressedList result_;
 };
 
 // NOTE: It's recommended to compile EF coding with -msse4.2, starting
index fb01db8cd112344124135a6d8fc0608a858ae54f..e1ed84a6357a9e818e12b75dccb9cc8b2d46d457 100644 (file)
@@ -177,9 +177,8 @@ void testGoTo(const std::vector<uint32_t>& data, const List& list) {
 
 template <class Reader, class Encoder>
 void testEmpty() {
-  typename Encoder::CompressedList list;
   const typename Encoder::ValueType* const data = nullptr;
-  Encoder::encode(data, 0, list);
+  auto list = Encoder::encode(data, data);
   {
     Reader reader(list);
     EXPECT_FALSE(reader.next());
@@ -198,8 +197,7 @@ void testEmpty() {
 
 template <class Reader, class Encoder>
 void testAll(const std::vector<uint32_t>& data) {
-  typename Encoder::CompressedList list;
-  Encoder::encode(data.begin(), data.end(), list);
+  auto list = Encoder::encode(data.begin(), data.end());
   testNext<Reader>(data, list);
   testSkip<Reader>(data, list);
   testSkipTo<Reader>(data, list);
index be0ef4c56e36dbbc939e1263aaba0f019d0d20af..91da77875fd078af8b9db409bab4154e0940c26e 100644 (file)
@@ -82,6 +82,9 @@ typedef EliasFanoReader<Encoder> Reader;
 std::vector<uint32_t> data;
 std::vector<size_t> order;
 
+std::vector<uint32_t> encodeSmallData;
+std::vector<uint32_t> encodeLargeData;
+
 typename Encoder::CompressedList list;
 
 void init() {
@@ -89,7 +92,7 @@ void init() {
 
   data = generateRandomList(100 * 1000, 10 * 1000 * 1000, gen);
   //data = loadList("/home/philipp/pl_test_dump.txt");
-  Encoder::encode(data.data(), data.size(), bm::list);
+  list = Encoder::encode(data.begin(), data.end());
 
   order.clear();
   order.reserve(data.size());
@@ -97,6 +100,9 @@ void init() {
     order.push_back(i);
   }
   std::shuffle(order.begin(), order.end(), gen);
+
+  encodeSmallData = generateRandomList(10, 100 * 1000, gen);
+  encodeLargeData = generateRandomList(1000 * 1000, 100 * 1000 * 1000, gen);
 }
 
 void free() {
@@ -133,6 +139,8 @@ BENCHMARK(SkipTo1_SkipQ128_1M) {
   bmSkipTo<bm::Reader>(bm::list, bm::data, 1, bm::k1M);
 }
 
+BENCHMARK_DRAW_LINE();
+
 BENCHMARK(SkipTo10_SkipQ128_1M) {
   bmSkipTo<bm::Reader>(bm::list, bm::data, 10, bm::k1M);
 }
@@ -145,8 +153,22 @@ BENCHMARK(SkipTo1000_SkipQ128_1M) {
   bmSkipTo<bm::Reader>(bm::list, bm::data, 1000, bm::k1M);
 }
 
+BENCHMARK_DRAW_LINE();
+
+BENCHMARK(Encode_10) {
+  auto list = bm::Encoder::encode(bm::encodeSmallData.begin(),
+                                  bm::encodeSmallData.end());
+  list.free();
+}
+
+BENCHMARK(Encode_1M) {
+  auto list = bm::Encoder::encode(bm::encodeLargeData.begin(),
+                                  bm::encodeLargeData.end());
+  list.free();
+}
+
 #if 0
-Intel Xeon CPU E5-2660 @ 2.7GHz (turbo on), using instructions::Fast.
+Intel(R) Xeon(R) CPU E5-2660 @ 2.7GHz (turbo on), using instructions::Fast.
 
 ============================================================================
 folly/experimental/test/EliasFanoCodingTest.cpp relative  time/iter  iters/s
@@ -157,10 +179,14 @@ Skip10_ForwarQ128_1M                                        13.69ms    73.03
 Skip100_ForwardQ128_1M                                      26.76ms    37.37
 Skip1000_ForwardQ128_1M                                     20.66ms    48.40
 GoTo_ForwardQ128_1M                                         43.75ms    22.86
+----------------------------------------------------------------------------
 SkipTo1_SkipQ128_1M                                          9.74ms   102.70
 SkipTo10_SkipQ128_1M                                        30.62ms    32.66
 SkipTo100_SkipQ128_1M                                       37.70ms    26.53
 SkipTo1000_SkipQ128_1M                                      31.14ms    32.11
+----------------------------------------------------------------------------
+Encode_10                                                  208.16ns    4.80M
+Encode_1M                                                    8.90ms   112.37
 ============================================================================
 #endif