https://github.com/lorenzhs/BuRR
Raw File
Tip revision: 1c62832ad7d6eab5b337f386955868c3ce9a54ea authored by Lorenz Hübschle-Schneider on 11 September 2021, 12:56:53 UTC
README: paper link, bibtex
Tip revision: 1c62832
config.hpp
//  Copyright (c) Lorenz Hübschle-Schneider
//  Copyright (c) Facebook, Inc. and its affiliates.
//  All Rights Reserved.  This source code is licensed under the Apache 2.0
//  License (found in the LICENSE file in the root directory).

#pragma once

#include <tlx/math/integer_log2.hpp>

#include <cstdint>
#include <iomanip>
#include <utility>

// requires xxhash3, i.e. libxxhash v0.8.0 or later
// available from https://github.com/Cyan4973/xxHash/releases/tag/v0.8.0
#define XXH_INLINE_ALL
#include <xxhash.h>

namespace std {
ostream &operator<<(ostream &os, const __uint128_t &v) {
    return os << "0x" << ::std::hex << static_cast<uint64_t>(v >> 64)
              << ::std::setw(16) << static_cast<uint64_t>(v) << ::std::dec;
}
} // namespace std

namespace ribbon {

// clang-format off
template <size_t bits>
using at_least_t = std::conditional_t<bits <= 8, uint8_t,
                   std::conditional_t<bits <= 16, uint16_t,
                   std::conditional_t<bits <= 32, uint32_t,
                   std::conditional_t<bits <= 64, uint64_t,
                   std::conditional_t<bits <= 128, __uint128_t,
                                      /* error */ void>>>>>;

template <size_t bits>
using at_least_fast_t = std::conditional_t<bits <= 8, uint_fast8_t,
                        // use uint32_t here, it's much faster
                        std::conditional_t<bits <= 16, uint32_t,
                        std::conditional_t<bits <= 32, uint_fast32_t,
                        std::conditional_t<bits <= 64, uint_fast64_t,
                        std::conditional_t<bits <= 128, __uint128_t,
                                           /* error */ void>>>>>;

template <typename uint_type>
using make_fast_t = at_least_fast_t<8u * sizeof(uint_type)>;
// clang-format on

// threshold compression modes
enum class ThreshMode : int { normal = 0, onebit = 1, twobit = 2 };

template <typename Config>
constexpr unsigned thresh_meta_bits =
    Config::kThreshMode == ThreshMode::normal
        ? (Config::kSparseCoeffs
               ? tlx::integer_log2_floor(
                     Config::kBucketSize /
                     // sparse start pos alignment: L = 16,32: 4,  L = 64,128: 8
                     (sizeof(typename Config::CoeffRow) >= 8 ? 8 : 4))
               : tlx::integer_log2_floor(Config::kBucketSize))
        : (Config::kThreshMode == ThreshMode::onebit
               ? 1
               : (Config::kThreshMode == ThreshMode::twobit ? 2 :
                                                            /* fallthrough */ 9999));

// To avoid writing 'typename' everywhere that we use types like 'Index'
#define IMPORT_RIBBON_CONFIG(RibbonConfig)                                      \
    using CoeffRow = typename RibbonConfig::CoeffRow;                           \
    using ResultRow = typename RibbonConfig::ResultRow;                         \
    using Index = typename RibbonConfig::Index;                                 \
    using Hash = typename RibbonConfig::Hash;                                   \
    using Key = typename RibbonConfig::Key;                                     \
                                                                                \
    /* Some more additions */                                                   \
    [[maybe_unused]] static constexpr auto kCoeffBits =                         \
        static_cast<Index>(sizeof(CoeffRow) * 8U);                              \
    [[maybe_unused]] static constexpr auto kResultBits = Config::kResultBits;   \
    [[maybe_unused]] static constexpr Index kBucketSize =                       \
        RibbonConfig::kBucketSize;                                              \
                                                                                \
    /* Export to algorithm */                                                   \
    [[maybe_unused]] static constexpr bool kIsFilter = RibbonConfig::kIsFilter; \
    [[maybe_unused]] static constexpr bool kFirstCoeffAlwaysOne =               \
        RibbonConfig::kFirstCoeffAlwaysOne;                                     \
    [[maybe_unused]] static constexpr ThreshMode kThreshMode =                  \
        RibbonConfig::kThreshMode;                                              \
    [[maybe_unused]] static constexpr bool kSparseCoeffs =                      \
        RibbonConfig::kSparseCoeffs;                                            \
    [[maybe_unused]] static constexpr bool kUseInterleavedSol =                 \
        RibbonConfig::kUseInterleavedSol;                                       \
    [[maybe_unused]] static constexpr bool kUseCacheLineStorage =               \
        RibbonConfig::kUseCacheLineStorage;                                     \
    [[maybe_unused]] static constexpr bool kUseMHC = RibbonConfig::kUseMHC;     \
    [[maybe_unused]] static constexpr bool kUseMultiplyShiftHash =              \
        RibbonConfig::kUseMultiplyShiftHash;                                    \
                                                                                \
    static_assert(!kUseInterleavedSol || !kUseCacheLineStorage,                 \
                  "can't have both");                                           \
    static_assert(1 << tlx::integer_log2_floor(kBucketSize) == kBucketSize,     \
                  "bucket size must be a power of two");                        \
    static_assert(                                                              \
        sizeof(CoeffRow) + sizeof(ResultRow) + sizeof(Index) + sizeof(Hash) +   \
                sizeof(Key) + kBucketSize + kResultBits + kCoeffBits +          \
                kFirstCoeffAlwaysOne + (int)kThreshMode + kSparseCoeffs +       \
                kUseInterleavedSol + kUseCacheLineStorage + kUseMHC +           \
                kUseMultiplyShiftHash >                                         \
            0,                                                                  \
        "avoid unused warnings, semicolon expected after macro call")


// for printing sqlplottools RESULT lines
template <typename Config>
std::string dump_config() {
    IMPORT_RIBBON_CONFIG(Config);
    std::stringstream s;
    s << " L=" << kCoeffBits << " B=" << kBucketSize << " r=" << kResultBits
      << " mode=" << (int)kThreshMode << " sparse=" << kSparseCoeffs << " sol="
      << (kUseInterleavedSol ? "int" : (kUseCacheLineStorage ? "cls" : "basic"))
      << " fcao=" << kFirstCoeffAlwaysOne << " idxbits=" << 8u * sizeof(Index)
      << " keybits=" << 8u * sizeof(Key) << " filter=" << kIsFilter;
    return s.str();
}

// This is a very basic config that has all required attributes but doesn't
// necessarily make the best choices! Do not use this as is, instead have a look
// at RConfig and its derivatives below.  This class is mainly useful as it
// documents the effect of the various settings.
template <typename CoeffRow_, typename ResultRow_, typename Key_>
class DefaultConfig {
public:
    // An unsigned integer type.  The size of this type determines L, the width
    // of the ribbon.  A good choice is uint64_t.  Fewer than 16 bits yield high
    // overheads without improving performance, but correctness is maintained
    // down to uint8_t.  You can also use __uint128_t, which can be a bit slow
    // but yields excellent overhead (sub-0.1% possible).
    using CoeffRow = CoeffRow_;
    // A type that is big enough to hold the data that is stored in the data
    // structure (retrieval) / the fingerprints (filter)
    using ResultRow = ResultRow_;
    // The key type.  For retrieval, input consists of pairs of Key and
    // ResultRow, while for filters, it's just Key.
    using Key = Key_;

    // An unsigned type that is large enough to hold the maximum index in the
    // input (i.e., input size muts fit into Index).  Use uint64_t if you have
    // ribbons holding billions of items.
    using Index = uint32_t;
    // An unsigned type to hold hash values. This should likely always be
    // uint64_t.
    using Hash = uint64_t;

    // How many items form a bucket.  This should be O(L^2/log(L)) - see
    // recommended_bucket_size below, don't use this value
    static constexpr Index kBucketSize = 16u * sizeof(CoeffRow);

    // How many bits the retrieval data structure should store per key / how
    // many fingerprint bits to use in a filter.  When using interleaved
    // storage, values other than 8*sizeof(ResultRow) are efficiently supported.
    static constexpr Index kResultBits = 8u * sizeof(ResultRow);

    // Whether to use a shift-multiply hash function for arithmetic keys instead
    // of xxhash.  This can be problematic if keys aren't random.  Should likely
    // stay disabled.
    static constexpr bool kUseMultiplyShiftHash = false;

    // The hash function to use for mapping Key to Hash.  This should be a good
    // hash function that yields sufficiently independent values for different
    // seeds.
    static constexpr Hash HashFn(const Key &key, uint64_t seed) {
        if constexpr (std::is_arithmetic_v<Key>) {
            return XXH3_64bits_withSeed(reinterpret_cast<const char *>(&key),
                                        sizeof(Key), seed);
        } else {
            return XXH3_64bits_withSeed(key.data(), key.size(), seed);
        }
    }

    // Whether the data structure is used as a filter or for retrieval.
    static constexpr bool kIsFilter = true;
    // Whether the first coefficient should always be set.  This makes things
    // slightly faster.
    static constexpr bool kFirstCoeffAlwaysOne = true;
    // Which threshold compressor to use.  `twobit` is fast and pretty good,
    // `onebit` yields improved compression at the cost of some (query)
    // performance.  `normal` uses uncompressed thresholds and should not
    // normally be used.
    static constexpr ThreshMode kThreshMode = ThreshMode::twobit;
    // Whether to use sparse coefficients. This speeds up queries for
    // non-interleaved solution storage, but is not fully optimized - the
    // threshold parameters need separate tuning for the sparse case.  As is,
    // sparse mode is a prototype and should not be used productively.
    static constexpr bool kSparseCoeffs = false;
    // Whether to use interleaved storage.  This is usually faster than the
    // alternatives.
    static constexpr bool kUseInterleavedSol = false;
    // Whether to use contiguous storage with embedded metadata.  This is
    // cache-efficient but slow in practice and should likely not be used.
    static constexpr bool kUseCacheLineStorage = false;
    // Whether to use Master Hash Codes.  This causes keys to be hashed only
    // once during insertion and query, using hash remixing to derive the next
    // level's hash if the key was bumped.  This is almost always a good idea.
    static constexpr bool kUseMHC = false;
    // Whether to print timings and other information about the construction.
    static constexpr bool log = true;
};

namespace {
// Internal.  Whether compressed metadata should be used for Cache-Line
// Storage. For other storage types, the answer is always yes - for CLS, it
// depends how much metadata space is available and whether uncompressed
// thresholds would fit.  This is because we don't save anything by using less
// than the available space.
template <typename Index, Index kBucketSize, Index kResultBits>
struct shouldCompressMeta {
    // internal
    static constexpr Index _kBucketIdxBits = tlx::integer_log2_ceil(kBucketSize),
                           _kBucketBits = kBucketSize * kResultBits,
                           _clbits = 512,
                           _buckets_per_cl = (_kBucketBits > _clbits)
                                                 ? 1
                                                 : _clbits / _kBucketBits;
    static constexpr bool value = _kBucketIdxBits * _buckets_per_cl > kResultBits;
};
} // namespace

template <size_t coeff_bits, ThreshMode mode>
constexpr size_t recommended_bucket_size =
    // round down to next power of two
    size_t{1} << tlx::integer_log2_floor(
        // w * w / (factor * log(w))
        coeff_bits * coeff_bits /
        ((mode == ThreshMode::normal ? 2 : 4) * tlx::integer_log2_ceil(coeff_bits)));

// Reasonable default config, but specify sizes in bits not types and make
// smarter choices.  This exposes a lot of options that users may not want to
// think about, see below for some reasonable presets.
template <size_t coeff_bits, size_t result_bits, ThreshMode mode = ThreshMode::twobit,
          bool sparse = false, bool interleaved = false, bool cls = false,
          int bucket_sh = 0, typename Key = int>
struct RConfig
    : public DefaultConfig<at_least_t<coeff_bits>, at_least_t<result_bits>, Key> {
    using Super =
        DefaultConfig<at_least_t<coeff_bits>, at_least_t<result_bits>, int>;
    using Index = uint32_t;
    static constexpr Index kResultBits = result_bits;
    static constexpr ThreshMode kThreshMode =
        // if using CLS, override compression mode
        Super::kUseCacheLineStorage
            ? (shouldCompressMeta<Index, Super::kBucketSize, kResultBits>::value
                   ? ThreshMode::twobit
                   : ThreshMode::normal)
            : mode;

    // Bucket size w^2 / 2 log w (uncompressed) or w^2 / 4 log w (compressed),
    // rounded down to next power of two. Optionally shift by 'bucket_sh'
    static constexpr Index kBucketSize =
        bucket_sh < 0 ? (recommended_bucket_size<coeff_bits, mode> >> -bucket_sh)
                      : (recommended_bucket_size<coeff_bits, mode> << bucket_sh);
    static constexpr bool kSparseCoeffs = sparse,
                          kUseInterleavedSol = interleaved,
                          kUseCacheLineStorage = cls, kUseMHC = true;
};

/******************************************************************************/
// Suggested configurations for users

// A fast filter config.  This uses L=64, 2-bit thresholds, and interleaved
// storage with dense coefficients and master hash codes.  It is a good
// all-round preset for filters.  You can expect <0.5% overhead if result_bits
// is at least 4; we measured 1.6% overhead for result_bits = 1.  For
// result_bits = 8 we saw 0.23% overhead, declining further as result_bits is
// increased.
template <size_t result_bits, typename Key>
struct FastFilterConfig
    : public RConfig</* L */ 64, result_bits, ThreshMode::twobit, /* sparse */ false,
                     /* interleaved */ true, /* cls */ false, /* shift */ 0, Key> {
};

// A very space-efficient filter config.  This uses L=128 and 2-bit thresholds,
// with the other choices as for FastFilterConfig. Expect <0.1% overhead for
// result_bits >= 7, and <0.2% for result_bits between 3 and 6.  We measured
// 0.45% overhead for result_bits = 1 and 0.18% overhead for result_bits = 3.
// For result_bits = 8, we saw 0.09% overhead, declining further as result_bits
// is increased.
template <size_t result_bits, typename Key>
struct CompactFilterConfig
    : public RConfig</* L */ 128, result_bits, ThreshMode::twobit, /* sparse */ false,
                     /* interleaved */ true, /* cls */ false, /* shift */ 0, Key> {
};
// An extremely space-efficient filter config.  This uses L=128 and 1-bit
// thresholds, with the other choices as for FastFilterConfig. Expect <0.1%
// overhead for result_bits >= 4.  We measured 0.33% overhead for result_bits =
// 1 and 0.12% overhead for result_bits = 3.  For result_bits = 8, we saw 0.05%
// overhead, declining further as result_bits is increased.
template <size_t result_bits, typename Key>
struct UltraCompactFilterConfig
    : public RConfig</* L */ 128, result_bits, ThreshMode::onebit, /* sparse */ false,
                     /* interleaved */ true, /* cls */ false, /* shift */ 0, Key> {
};


// A fast retrieval config, see FastFilterConfig for details
template <size_t result_bits, typename Key>
struct FastRetrievalConfig : public FastFilterConfig<result_bits, Key> {
    static constexpr bool kIsFilter = false;
};

// A very space-efficient retrieval config, see CompactFilterConfig for details
template <size_t result_bits, typename Key>
struct CompactRetrievalConfig : public CompactFilterConfig<result_bits, Key> {
    static constexpr bool kIsFilter = false;
};

// An extremely space-efficient retrieval config, see UltraCompactFilterConfig
// for details
template <size_t result_bits, typename Key>
struct UltraCompactRetrievalConfig
    : public UltraCompactFilterConfig<result_bits, Key> {
    static constexpr bool kIsFilter = false;
};

} // namespace ribbon
back to top