rocksdb/util/ribbon_config.cc

507 lines
15 KiB
C++
Raw Permalink Normal View History

Refine Ribbon configuration, improve testing, add Homogeneous (#7879) Summary: This change only affects non-schema-critical aspects of the production candidate Ribbon filter. Specifically, it refines choice of internal configuration parameters based on inputs. The changes are minor enough that the schema tests in bloom_test, some of which depend on this, are unaffected. There are also some minor optimizations and refactorings. This would be a schema change for "smash" Ribbon, to fix some known issues with small filters, but "smash" Ribbon is not accessible in public APIs. Unit test CompactnessAndBacktrackAndFpRate updated to test small and medium-large filters. Run with --thoroughness=100 or so for much better detection power (not appropriate for continuous regression testing). Homogenous Ribbon: This change adds internally a Ribbon filter variant we call Homogeneous Ribbon, in collaboration with Stefan Walzer. The expected "result" value for every key is zero, instead of computed from a hash. Entropy for queries not to be false positives comes from free variables ("overhead") in the solution structure, which are populated pseudorandomly. Construction is slightly faster for not tracking result values, and never fails. Instead, FP rate can jump up whenever and whereever entries are packed too tightly. For small structures, we can choose overhead to make this FP rate jump unlikely, as seen in updated unit test CompactnessAndBacktrackAndFpRate. Unlike standard Ribbon, Homogeneous Ribbon seems to scale to arbitrary number of keys when accepting an FP rate penalty for small pockets of high FP rate in the structure. For example, 64-bit ribbon with 8 solution columns and 10% allocated space overhead for slots seems to achieve about 10.5% space overhead vs. information-theoretic minimum based on its observed FP rate with expected pockets of degradation. (FP rate is close to 1/256.) If targeting a higher FP rate with fewer solution columns, Homogeneous Ribbon can be even more space efficient, because the penalty from degradation is relatively smaller. If targeting a lower FP rate, Homogeneous Ribbon is less space efficient, as more allocated overhead is needed to keep the FP rate impact of degradation relatively under control. The new OptimizeHomogAtScale tool in ribbon_test helps to find these optimal allocation overheads for different numbers of solution columns. And Ribbon widths, with 128-bit Ribbon apparently cutting space overheads in half vs. 64-bit. Other misc item specifics: * Ribbon APIs in util/ribbon_config.h now provide configuration data for not just 5% construction failure rate (95% success), but also 50% and 0.1%. * Note that the Ribbon structure does not exhibit "threshold" behavior as standard Xor filter does, so there is a roughly fixed space penalty to cut construction failure rate in half. Thus, there isn't really an "almost sure" setting. * Although we can extrapolate settings for large filters, we don't have a good formula for configuring smaller filters (< 2^17 slots or so), and efforts to summarize with a formula have failed. Thus, small data is hard-coded from updated FindOccupancy tool. * Enhances ApproximateNumEntries for public API Ribbon using more precise data (new API GetNumToAdd), thus a more accurate but not perfect reversal of CalculateSpace. (bloom_test updated to expect the greater precision) * Move EndianSwapValue from coding.h to coding_lean.h to keep Ribbon code easily transferable from RocksDB * Add some missing 'const' to member functions * Small optimization to 128-bit BitParity * Small refactoring of BandingStorage in ribbon_alg.h to support Homogeneous Ribbon * CompactnessAndBacktrackAndFpRate now has an "expand" test: on construction failure, a possible alternative to re-seeding hash functions is simply to increase the number of slots (allocated space overhead) and try again with essentially the same hash values. (Start locations will be different roundings of the same scaled hash values--because fastrange not mod.) This seems to be as effective or more effective than re-seeding, as long as we increase the number of slots (m) by roughly m += m/w where w is the Ribbon width. This way, there is effectively an expansion by one slot for each ribbon-width window in the banding. (This approach assumes that getting "bad data" from your hash function is as unlikely as it naturally should be, e.g. no adversary.) * 32-bit and 16-bit Ribbon configurations are added to ribbon_test for understanding their behavior, e.g. with FindOccupancy. They are not considered useful at this time and not tested with CompactnessAndBacktrackAndFpRate. Pull Request resolved: https://github.com/facebook/rocksdb/pull/7879 Test Plan: unit test updates included Reviewed By: jay-zhuang Differential Revision: D26371245 Pulled By: pdillinger fbshipit-source-id: da6600d90a3785b99ad17a88b2a3027710b4ea3a
2021-02-26 17:48:55 +01:00
// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "util/ribbon_config.h"
namespace ROCKSDB_NAMESPACE {
namespace ribbon {
namespace detail {
// Each instantiation of this struct is sufficiently unique for configuration
// purposes, and is only instantiated for settings where we support the
// configuration API. An application might only reference one instantiation,
// meaning the rest could be pruned at link time.
template <ConstructionFailureChance kCfc, uint64_t kCoeffBits, bool kUseSmash>
struct BandingConfigHelperData {
static constexpr size_t kKnownSize = 18U;
// Because of complexity in the data, for smaller numbers of slots
// (powers of two up to 2^17), we record known numbers that can be added
// with kCfc chance of construction failure and settings in template
// parameters. Zero means "unsupported (too small) number of slots".
// (GetNumToAdd below will use interpolation for numbers of slots
// between powers of two; double rather than integer values here make
// that more accurate.)
static const std::array<double, kKnownSize> kKnownToAddByPow2;
// For sufficiently large number of slots, doubling the number of
// slots will increase the expected overhead (slots over number added)
// by approximately this constant.
// (This is roughly constant regardless of ConstructionFailureChance and
// smash setting.)
// (Would be a constant if we had partial template specialization for
// static const members.)
static inline double GetFactorPerPow2() {
if (kCoeffBits == 128U) {
return 0.0038;
} else {
assert(kCoeffBits == 64U);
return 0.0083;
}
}
// Overhead factor for 2^(kKnownSize-1) slots
// (Would be a constant if we had partial template specialization for
// static const members.)
static inline double GetFinalKnownFactor() {
return 1.0 * (uint32_t{1} << (kKnownSize - 1)) /
kKnownToAddByPow2[kKnownSize - 1];
}
// GetFinalKnownFactor() - (kKnownSize-1) * GetFactorPerPow2()
// (Would be a constant if we had partial template specialization for
// static const members.)
static inline double GetBaseFactor() {
return GetFinalKnownFactor() - (kKnownSize - 1) * GetFactorPerPow2();
}
// Get overhead factor (slots over number to add) for sufficiently large
// number of slots (by log base 2)
static inline double GetFactorForLarge(double log2_num_slots) {
return GetBaseFactor() + log2_num_slots * GetFactorPerPow2();
}
// For a given power of two number of slots (specified by whole number
// log base 2), implements GetNumToAdd for such limited case, returning
// double for better interpolation in GetNumToAdd and GetNumSlots.
static inline double GetNumToAddForPow2(uint32_t log2_num_slots) {
assert(log2_num_slots <= 32); // help clang-analyze
if (log2_num_slots < kKnownSize) {
return kKnownToAddByPow2[log2_num_slots];
} else {
return 1.0 * (uint64_t{1} << log2_num_slots) /
GetFactorForLarge(1.0 * log2_num_slots);
}
}
};
// Based on data from FindOccupancy in ribbon_test
template <>
const std::array<double, 18>
BandingConfigHelperData<kOneIn2, 128U, false>::kKnownToAddByPow2{{
0,
0,
0,
0,
0,
0,
0,
0, // unsupported
252.984,
506.109,
1013.71,
2029.47,
4060.43,
8115.63,
16202.2,
32305.1,
64383.5,
128274,
}};
template <>
const std::array<double, 18>
BandingConfigHelperData<kOneIn2, 128U, /*smash*/ true>::kKnownToAddByPow2{{
0,
0,
0,
0,
0,
0,
0, // unsupported
126.274,
254.279,
510.27,
1022.24,
2046.02,
4091.99,
8154.98,
16244.3,
32349.7,
64426.6,
128307,
}};
template <>
const std::array<double, 18>
BandingConfigHelperData<kOneIn2, 64U, false>::kKnownToAddByPow2{{
0,
0,
0,
0,
0,
0,
0, // unsupported
124.94,
249.968,
501.234,
1004.06,
2006.15,
3997.89,
7946.99,
15778.4,
31306.9,
62115.3,
123284,
}};
template <>
const std::array<double, 18>
BandingConfigHelperData<kOneIn2, 64U, /*smash*/ true>::kKnownToAddByPow2{{
0,
0,
0,
0,
0,
0, // unsupported
62.2683,
126.259,
254.268,
509.975,
1019.98,
2026.16,
4019.75,
7969.8,
15798.2,
31330.3,
62134.2,
123255,
}};
template <>
const std::array<double, 18>
BandingConfigHelperData<kOneIn20, 128U, false>::kKnownToAddByPow2{{
0,
0,
0,
0,
0,
0,
0,
0, // unsupported
248.851,
499.532,
1001.26,
2003.97,
4005.59,
8000.39,
15966.6,
31828.1,
63447.3,
126506,
}};
template <>
const std::array<double, 18>
BandingConfigHelperData<kOneIn20, 128U, /*smash*/ true>::kKnownToAddByPow2{{
0,
0,
0,
0,
0,
0,
0, // unsupported
122.637,
250.651,
506.625,
1018.54,
2036.43,
4041.6,
8039.25,
16005,
31869.6,
63492.8,
126537,
}};
template <>
const std::array<double, 18>
BandingConfigHelperData<kOneIn20, 64U, false>::kKnownToAddByPow2{{
0,
0,
0,
0,
0,
0,
0, // unsupported
120.659,
243.346,
488.168,
976.373,
1948.86,
3875.85,
7704.97,
15312.4,
30395.1,
60321.8,
119813,
}};
template <>
const std::array<double, 18>
BandingConfigHelperData<kOneIn20, 64U, /*smash*/ true>::kKnownToAddByPow2{{
0,
0,
0,
0,
0,
0, // unsupported
58.6016,
122.619,
250.641,
503.595,
994.165,
1967.36,
3898.17,
7727.21,
15331.5,
30405.8,
60376.2,
119836,
}};
template <>
const std::array<double, 18>
BandingConfigHelperData<kOneIn1000, 128U, false>::kKnownToAddByPow2{{
0,
0,
0,
0,
0,
0,
0,
0, // unsupported
242.61,
491.887,
983.603,
1968.21,
3926.98,
7833.99,
15629,
31199.9,
62307.8,
123870,
}};
template <>
const std::array<double, 18> BandingConfigHelperData<
kOneIn1000, 128U, /*smash*/ true>::kKnownToAddByPow2{{
0,
0,
0,
0,
0,
0,
0, // unsupported
117.19,
245.105,
500.748,
1010.67,
1993.4,
3950.01,
7863.31,
15652,
31262.1,
62462.8,
124095,
}};
template <>
const std::array<double, 18>
BandingConfigHelperData<kOneIn1000, 64U, false>::kKnownToAddByPow2{{
0,
0,
0,
0,
0,
0,
0, // unsupported
114,
234.8,
471.498,
940.165,
1874,
3721.5,
7387.5,
14592,
29160,
57745,
115082,
}};
template <>
const std::array<double, 18>
BandingConfigHelperData<kOneIn1000, 64U, /*smash*/ true>::kKnownToAddByPow2{
{
0,
0,
0,
0,
0,
0, // unsupported
53.0434,
117,
245.312,
483.571,
950.251,
1878,
3736.34,
7387.97,
14618,
29142.9,
57838.8,
114932,
}};
// We hide these implementation details from the .h file with explicit
// instantiations below these partial specializations.
template <ConstructionFailureChance kCfc, uint64_t kCoeffBits, bool kUseSmash,
bool kHomogeneous>
uint32_t BandingConfigHelper1MaybeSupported<
kCfc, kCoeffBits, kUseSmash, kHomogeneous,
true /* kIsSupported */>::GetNumToAdd(uint32_t num_slots) {
using Data = detail::BandingConfigHelperData<kCfc, kCoeffBits, kUseSmash>;
if (num_slots == 0) {
return 0;
}
uint32_t num_to_add;
double log2_num_slots = std::log(num_slots) * 1.4426950409;
uint32_t floor_log2 = static_cast<uint32_t>(log2_num_slots);
if (floor_log2 + 1 < Data::kKnownSize) {
double ceil_portion = 1.0 * num_slots / (uint32_t{1} << floor_log2) - 1.0;
// Must be a supported number of slots
assert(Data::kKnownToAddByPow2[floor_log2] > 0.0);
// Weighted average of two nearest known data points
num_to_add = static_cast<uint32_t>(
ceil_portion * Data::kKnownToAddByPow2[floor_log2 + 1] +
(1.0 - ceil_portion) * Data::kKnownToAddByPow2[floor_log2]);
} else {
// Use formula for large values
double factor = Data::GetFactorForLarge(log2_num_slots);
assert(factor >= 1.0);
num_to_add = static_cast<uint32_t>(num_slots / factor);
}
if (kHomogeneous) {
// Even when standard filter construction would succeed, we might
// have loaded things up too much for Homogeneous filter. (Complete
// explanation not known but observed empirically.) This seems to
// correct for that, mostly affecting small filter configurations.
if (num_to_add >= 8) {
num_to_add -= 8;
} else {
assert(false);
}
}
return num_to_add;
}
template <ConstructionFailureChance kCfc, uint64_t kCoeffBits, bool kUseSmash,
bool kHomogeneous>
uint32_t BandingConfigHelper1MaybeSupported<
kCfc, kCoeffBits, kUseSmash, kHomogeneous,
true /* kIsSupported */>::GetNumSlots(uint32_t num_to_add) {
using Data = detail::BandingConfigHelperData<kCfc, kCoeffBits, kUseSmash>;
if (num_to_add == 0) {
return 0;
}
if (kHomogeneous) {
// Reverse of above in GetNumToAdd
num_to_add += 8;
}
double log2_num_to_add = std::log(num_to_add) * 1.4426950409;
uint32_t approx_log2_slots = static_cast<uint32_t>(log2_num_to_add + 0.5);
assert(approx_log2_slots <= 32); // help clang-analyze
double lower_num_to_add = Data::GetNumToAddForPow2(approx_log2_slots);
double upper_num_to_add;
if (approx_log2_slots == 0 || lower_num_to_add == /* unsupported */ 0) {
// Return minimum non-zero slots in standard implementation
return kUseSmash ? kCoeffBits : 2 * kCoeffBits;
} else if (num_to_add < lower_num_to_add) {
upper_num_to_add = lower_num_to_add;
--approx_log2_slots;
lower_num_to_add = Data::GetNumToAddForPow2(approx_log2_slots);
} else {
upper_num_to_add = Data::GetNumToAddForPow2(approx_log2_slots + 1);
}
assert(num_to_add >= lower_num_to_add);
assert(num_to_add < upper_num_to_add);
double upper_portion =
(num_to_add - lower_num_to_add) / (upper_num_to_add - lower_num_to_add);
double lower_num_slots = 1.0 * (uint64_t{1} << approx_log2_slots);
// Interpolation, round up
return static_cast<uint32_t>(upper_portion * lower_num_slots +
lower_num_slots + 0.999999999);
}
// These explicit instantiations enable us to hide most of the
// implementation details from the .h file. (The .h file currently
// needs to determine whether settings are "supported" or not.)
template struct BandingConfigHelper1MaybeSupported<kOneIn2, 128U, /*sm*/ false,
/*hm*/ false, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn2, 128U, /*sm*/ true,
/*hm*/ false, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn2, 128U, /*sm*/ false,
/*hm*/ true, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn2, 128U, /*sm*/ true,
/*hm*/ true, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn2, 64U, /*sm*/ false,
/*hm*/ false, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn2, 64U, /*sm*/ true,
/*hm*/ false, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn2, 64U, /*sm*/ false,
/*hm*/ true, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn2, 64U, /*sm*/ true,
/*hm*/ true, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn20, 128U, /*sm*/ false,
/*hm*/ false, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn20, 128U, /*sm*/ true,
/*hm*/ false, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn20, 128U, /*sm*/ false,
/*hm*/ true, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn20, 128U, /*sm*/ true,
/*hm*/ true, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn20, 64U, /*sm*/ false,
/*hm*/ false, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn20, 64U, /*sm*/ true,
/*hm*/ false, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn20, 64U, /*sm*/ false,
/*hm*/ true, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn20, 64U, /*sm*/ true,
/*hm*/ true, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<
kOneIn1000, 128U, /*sm*/ false, /*hm*/ false, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<
kOneIn1000, 128U, /*sm*/ true, /*hm*/ false, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<
kOneIn1000, 128U, /*sm*/ false, /*hm*/ true, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<
kOneIn1000, 128U, /*sm*/ true, /*hm*/ true, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<
kOneIn1000, 64U, /*sm*/ false, /*hm*/ false, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn1000, 64U, /*sm*/ true,
/*hm*/ false, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<
kOneIn1000, 64U, /*sm*/ false, /*hm*/ true, /*sup*/ true>;
template struct BandingConfigHelper1MaybeSupported<kOneIn1000, 64U, /*sm*/ true,
/*hm*/ true, /*sup*/ true>;
} // namespace detail
} // namespace ribbon
} // namespace ROCKSDB_NAMESPACE