From 916c845c41364d8fdffb4dcdfd625b9a0cc6ddea Mon Sep 17 00:00:00 2001 From: joaquintides Date: Wed, 2 Apr 2025 20:42:19 +0200 Subject: [PATCH] added documentation * removed unneeded explicit * fixed boundary results for capacity_for and fpr_for * renamed used_block_size to used_value_size * added reset(n,n) * added initial documentation draft * static asserted assumption on Block size * synced up naming in comment with that of docs * added implementation notes * editorial * expanded tables * removed unneeded explicit * fixed boundary results for capacity_for and fpr_for * renamed used_block_size to used_value_size * added reset(n,n) * added initial documentation draft * static asserted assumption on Block size * synced up naming in comment with that of docs * added implementation notes * editorial * added benchmarks * editorial * added configuration section * editorial * s/multiinsertion/multi-insertion * added section on use cases * editorial --- doc/Jamfile.v2 | 23 + doc/bloom.adoc | 41 + doc/bloom/benchmarks.adoc | 1202 +++++++++++++++++ doc/bloom/configuration.adoc | 99 ++ doc/bloom/copyright.adoc | 10 + doc/bloom/fpr_estimation.adoc | 74 + doc/bloom/implementation_notes.adoc | 130 ++ doc/bloom/intro.adoc | 49 + doc/bloom/primer.adoc | 118 ++ doc/bloom/reference.adoc | 14 + doc/bloom/reference/block.adoc | 42 + doc/bloom/reference/fast_multiblock32.adoc | 52 + doc/bloom/reference/fast_multiblock64.adoc | 52 + doc/bloom/reference/filter.adoc | 711 ++++++++++ doc/bloom/reference/header_block.adoc | 17 + .../reference/header_fast_multiblock32.adoc | 17 + .../reference/header_fast_multiblock64.adoc | 17 + doc/bloom/reference/header_filter.adoc | 42 + doc/bloom/reference/header_multiblock.adoc | 17 + doc/bloom/reference/multiblock.adoc | 45 + doc/bloom/reference/subfilters.adoc | 57 + doc/bloom/release_notes.adoc | 9 + doc/bloom/tutorial.adoc | 204 +++ doc/img/block_insertion.png | Bin 0 -> 4711 bytes doc/img/block_multi_insertion.png | Bin 0 -> 4707 bytes doc/img/bloom_insertion.png | Bin 0 -> 4915 bytes doc/img/bloom_lookup.png | Bin 0 -> 2978 bytes doc/img/db_speedup.png | Bin 0 -> 4718 bytes doc/img/fpr_c.png | Bin 0 -> 57609 bytes doc/img/fpr_n_k.png | Bin 0 -> 10870 bytes doc/img/fpr_n_k_bk.png | Bin 0 -> 12650 bytes doc/img/multiblock_insertion.png | Bin 0 -> 5252 bytes include/boost/bloom/detail/block_base.hpp | 3 + include/boost/bloom/detail/core.hpp | 37 +- include/boost/bloom/filter.hpp | 2 +- test/test_capacity.cpp | 16 + test/test_fpr.cpp | 6 +- 37 files changed, 3087 insertions(+), 19 deletions(-) create mode 100644 doc/Jamfile.v2 create mode 100644 doc/bloom.adoc create mode 100644 doc/bloom/benchmarks.adoc create mode 100644 doc/bloom/configuration.adoc create mode 100644 doc/bloom/copyright.adoc create mode 100644 doc/bloom/fpr_estimation.adoc create mode 100644 doc/bloom/implementation_notes.adoc create mode 100644 doc/bloom/intro.adoc create mode 100644 doc/bloom/primer.adoc create mode 100644 doc/bloom/reference.adoc create mode 100644 doc/bloom/reference/block.adoc create mode 100644 doc/bloom/reference/fast_multiblock32.adoc create mode 100644 doc/bloom/reference/fast_multiblock64.adoc create mode 100644 doc/bloom/reference/filter.adoc create mode 100644 doc/bloom/reference/header_block.adoc create mode 100644 doc/bloom/reference/header_fast_multiblock32.adoc create mode 100644 doc/bloom/reference/header_fast_multiblock64.adoc create mode 100644 doc/bloom/reference/header_filter.adoc create mode 100644 doc/bloom/reference/header_multiblock.adoc create mode 100644 doc/bloom/reference/multiblock.adoc create mode 100644 doc/bloom/reference/subfilters.adoc create mode 100644 doc/bloom/release_notes.adoc create mode 100644 doc/bloom/tutorial.adoc create mode 100644 doc/img/block_insertion.png create mode 100644 doc/img/block_multi_insertion.png create mode 100644 doc/img/bloom_insertion.png create mode 100644 doc/img/bloom_lookup.png create mode 100644 doc/img/db_speedup.png create mode 100644 doc/img/fpr_c.png create mode 100644 doc/img/fpr_n_k.png create mode 100644 doc/img/fpr_n_k_bk.png create mode 100644 doc/img/multiblock_insertion.png diff --git a/doc/Jamfile.v2 b/doc/Jamfile.v2 new file mode 100644 index 0000000..fd54444 --- /dev/null +++ b/doc/Jamfile.v2 @@ -0,0 +1,23 @@ +# Copyright 2025 Joaquín M López Muñoz. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) +# +# See http://www.boost.org/libs/bloom for library home page. + +import asciidoctor ; + +html bloom.html : bloom.adoc ; + +install html_ : bloom.html : html ; + +pdf bloom.pdf : bloom.adoc ; +explicit bloom.pdf ; + +install pdf_ : bloom.pdf : bloom ; +explicit pdf_ ; + +alias boostdoc ; +explicit boostdoc ; +alias boostrelease : html_ ; +explicit boostrelease ; \ No newline at end of file diff --git a/doc/bloom.adoc b/doc/bloom.adoc new file mode 100644 index 0000000..79998c0 --- /dev/null +++ b/doc/bloom.adoc @@ -0,0 +1,41 @@ += Boost.Bloom +:toc: left +:toclevels: 3 +:idprefix: +:docinfo: private-footer +:source-highlighter: rouge +:source-language: c++ +:nofooter: +:sectlinks: +:leveloffset: +1 +:imagesdir: ../img +:stem: latexmath +:small: pass:[] +:small-end: pass:[] + +++++ + +++++ + +include::bloom/intro.adoc[] +include::bloom/primer.adoc[] +include::bloom/tutorial.adoc[] +include::bloom/configuration.adoc[] +include::bloom/benchmarks.adoc[] +include::bloom/reference.adoc[] +include::bloom/fpr_estimation.adoc[] +include::bloom/implementation_notes.adoc[] +include::bloom/release_notes.adoc[] +include::bloom/copyright.adoc[] diff --git a/doc/bloom/benchmarks.adoc b/doc/bloom/benchmarks.adoc new file mode 100644 index 0000000..bab9ecd --- /dev/null +++ b/doc/bloom/benchmarks.adoc @@ -0,0 +1,1202 @@ +[#benchmarks] += Benchmarks + +:idprefix: benchmarks_ + +(More results in a +https://github.com/joaquintides/boost_bloom_benchmarks[dedicated repo^].) + +The tables show the false positive rate and execution times in nanoseconds per operation +for nine different configurations of `boost::bloom::filter` where 10M elements have +been inserted. + +* **ins.:** Insertion. +* **succ. lkp.:** Successful lookup (the element is in the filter). +* **uns. lkp.:** Unsuccessful lookup (the element is not in the filter, though lookup may return `true`). + +Filters are constructed with a capacity `c * N` (bits), so `c` is the number of +bits used per element. For each combination of `c` and a given filter configuration, we have +selected the optimum value of `K` (that yielding the minimum FPR). +Standard release-mode settings are used; AVX2 is indicated for Visual Studio builds +(`/arch:AVX2`) and GCC/Clang builds (`-mavx2`), which causes +`fast_multiblock32` and `fast_multiblock64` to use their AVX2 variant. + +== GCC 14, x64 + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
filter<K>filter<1,block<uint64_t,K>>filter<1,block<uint64_t,K>,1>
cKFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
862.156612.2111.0616.9443.34624.444.734.7353.04485.235.375.38
1290.314618.0917.0817.8651.03105.025.075.1560.82446.876.346.28
16110.045628.6729.4317.8160.40356.306.486.3170.28857.437.297.57
20140.006646.5439.9119.2670.187910.0810.499.5380.11859.689.089.68
filter<1,multiblock<uint64_t,K>>filter<1,multiblock<uint64_t,K>,1>filter<1,fast_multiblock32<K>>
cKFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
852.45155.686.496.5052.32086.067.477.7152.72343.373.803.75
1280.42447.399.459.3680.37588.2010.0810.1280.54072.723.383.35
16110.077611.2815.0815.13110.064117.9015.6515.55110.11746.766.874.87
20130.014814.3920.0318.67140.012016.4122.9422.46130.02779.389.606.48
filter<1,fast_multiblock32<K>,1>filter<1,fast_multiblock64<K>>filter<1,fast_multiblock64<K>,1>
cKFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
852.46253.363.733.7052.43884.925.655.5852.31985.035.495.57
1280.44283.353.693.6780.41903.464.774.7680.37474.815.525.46
16110.08666.697.185.10110.07818.639.827.79110.06519.809.557.63
20130.01809.089.057.13130.014711.6013.649.10140.011211.2915.1216.84
++++ + +== Clang 18, x64 + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
filter<K>filter<1,block<uint64_t,K>>filter<1,block<uint64_t,K>,1>
cKFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
862.156610.8410.5116.4743.34623.834.634.4953.04484.495.195.19
1290.314615.6915.3716.9651.03104.295.104.9660.82444.985.785.73
16110.045623.8324.8216.9960.40355.466.316.1370.28856.177.837.52
20140.006642.2439.9220.0270.18798.799.6115.2380.11855.616.205.94
filter<1,multiblock<uint64_t,K>>filter<1,multiblock<uint64_t,K>,1>filter<1,fast_multiblock32<K>>
cKFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
852.45153.534.134.1052.32083.573.953.9552.72343.033.023.04
1280.42443.033.693.6680.37584.054.184.2280.54072.472.552.55
16110.07767.077.797.99110.06417.268.078.04110.11745.455.854.45
20130.01489.1010.9910.58140.01209.6211.6812.15130.02777.778.397.29
filter<1,fast_multiblock32<K>,1>filter<1,fast_multiblock64<K>>filter<1,fast_multiblock64<K>,1>
cKFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
852.46253.072.922.9552.43884.184.734.7152.31984.274.604.57
1280.44282.962.792.7880.41903.204.054.1380.37474.334.534.66
16110.08665.545.623.92110.07816.627.535.91110.06517.037.616.42
20130.01809.889.246.20130.014710.0411.538.07140.011210.1411.207.99
++++ + +== Clang 15, ARM64 + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
filter<K>filter<1,block<uint64_t,K>>filter<1,block<uint64_t,K>,1>
cKFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
862.15669.567.9217.7543.34621.953.563.3253.04482.782.832.85
1290.314623.4321.4922.6851.03105.866.514.6560.82445.335.765.96
16110.045640.5132.7322.2660.40358.988.137.8470.28859.189.258.74
20140.006667.3550.6824.7670.18799.5110.229.3780.11858.187.947.73
filter<1,multiblock<uint64_t,K>>filter<1,multiblock<uint64_t,K>,1>filter<1,fast_multiblock32<K>>
cKFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
852.45153.042.813.4852.32083.483.913.6752.72343.063.463.47
1280.42447.577.397.9980.37586.958.089.2280.54072.736.676.46
16110.077615.169.9211.60110.064115.3512.6711.48110.117410.8510.727.26
20130.014817.7717.0518.43140.012020.0217.3617.71130.027711.0613.688.15
filter<1,fast_multiblock32<K>,1>filter<1,fast_multiblock64<K>>filter<1,fast_multiblock64<K>,1>
cKFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
852.46253.244.323.1952.45153.674.584.3352.32083.244.294.17
1280.44285.935.954.5480.42447.688.479.1580.37584.124.684.52
16110.08667.367.475.01110.07769.488.738.70110.06419.468.538.50
20130.01809.4610.425.96130.014814.2913.2513.52140.012015.8213.6313.47
++++ + +== VS 2022, x64 + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
filter<K>filter<1,block<uint64_t,K>>filter<1,block<uint64_t,K>,1>
cKFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
862.156614.8413.0517.9143.34627.064.494.5053.04488.115.745.85
1290.314625.1820.5918.5851.03109.135.445.5060.824410.507.776.62
16110.045636.5539.3119.4660.403513.407.317.2870.288512.048.9114.47
20140.006683.3083.9324.9870.187916.3112.6415.8280.118520.8115.8315.73
filter<1,multiblock<uint64_t,K>>filter<1,multiblock<uint64_t,K>,1>filter<1,fast_multiblock32<K>>
cKFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
852.45159.506.316.3952.32089.616.456.4352.72343.784.274.26
1280.424415.7510.5111.3080.375820.979.039.3780.54073.526.144.50
16110.077625.5820.3118.44110.064127.3515.2419.41110.117410.9214.3212.54
20130.014834.7830.3633.15140.012038.8728.7825.22130.027714.1619.4613.75
filter<1,fast_multiblock32<K>,1>filter<1,fast_multiblock64<K>>filter<1,fast_multiblock64<K>,1>
cKFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
KFPR
[%]
ins.succ.
lkp.
uns.
lkp.
852.46253.674.184.2352.43885.066.175.9652.31985.125.825.86
1280.44283.866.115.1080.41905.788.727.1680.37477.777.716.91
16110.08666.948.878.60110.078112.5511.109.40110.065112.3215.2315.45
20130.018012.2216.9614.46130.014718.5624.0218.81140.011223.0521.3714.28
++++ \ No newline at end of file diff --git a/doc/bloom/configuration.adoc b/doc/bloom/configuration.adoc new file mode 100644 index 0000000..928105d --- /dev/null +++ b/doc/bloom/configuration.adoc @@ -0,0 +1,99 @@ +[#configuration] += Choosing a Filter Configuration + +:idprefix: configuration_ + +Boost.Bloom offers a plethora of compile-time and run-time configuration options, +so it may be difficult to make a choice. +If you're aiming for a given FPR or have a particular capacity in mind and +you'd like to choose the most appropriate filter type, the following chart +may come handy. + +image::fpr_c.png[align=center, title="FPR vs. _c_ for different filter types."] + +The chart plots FPR vs. _c_ (capacity / number of elements inserted) for several +`boost::bloom::filter`+++s+++ where `K` has been set to its optimum value (minimum FPR) +as shown in the table below. + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
c = capacity / number of elements inserted
4 5 6 7 8 9 10 11 12 1314 15 16 17 18 19 20 21 22 23 24
filter<1,block<uint32_t,K>> 3 3 3 4 4 5 5 5 5 55 5 6 6 7 7 7 7 7 7 7
filter<1,block<uint32_t,K>,1> 2 3 4 4 4 4 5 5 5 66 6 6 6 6 6 7 7 7 7 7
filter<1,block<uint64_t,K>> 2 3 4 4 5 5 5 5 5 66 6 6 6 7 7 7 7 7 7 7
filter<1,block<uint64_t,K>,1> 2 3 4 4 4 5 6 6 6 77 7 7 7 8 8 8 8 8 9 9
filter<1,multiblock<uint32_t,K>> 3 3 4 5 6 6 8 8 8 89 9 9 10 13 13 15 15 15 16 16
filter<1,multiblock<uint32_t,K>,1> 3 3 4 5 6 6 7 7 8 89 9 10 10 12 12 14 14 14 14 15
filter<1,multiblock<uint64_t,K>> 4 4 5 5 6 6 6 7 8 810 10 12 13 14 15 15 15 15 16 17
filter<1,multiblock<uint64_t,K>,1> 3 3 4 5 5 6 6 7 9 1010 11 11 12 12 13 13 13 15 16 16
filter<K> 3 4 4 5 5 6 6 8 8 910 11 12 13 13 13 14 16 16 16 17
++++ + +Let's see how this can be used by way of an example. Suppose we plan to insert 10M elements +and want to keep the FPR at 10^-4^. The chart gives us five possibilities: + +* `filter` -> _c_ ≅ 19 bits per element +* `filter<1, multiblock, 1>` -> _c_ ≅ 20 bits per element +* `filter<1, multiblock>` -> _c_ ≅ 21 bits per element +* `filter<1, multiblock, 1>` -> _c_ ≅ 21.5 bits per element +* `filter<1, multiblock>` -> _c_ ≅ 23 bits per element + +These options have different tradeoffs in terms of space used and performance. If +we choose `filter<1, multiblock, 1>` as a compromise (or better yet, +`filter<1, fast_multiblock32, 1>`), the only remaining step is to consult the +value of `K` in the table for _c_ = 21 or 22, and we get our final configuration: + +[listing,subs="+macros,+quotes"] +----- +using my_filter=filter, 1>; +----- + +The resulting filter can be constructed in any of the following ways: + +[listing,subs="+macros,+quotes"] +----- +// 1) calculate the capacity from the value of c we got from the chart +my_filter pass:[f((]std::size_t)(10'000'000 * 21.5)); + +// 2) let the library calculate the capacity from n and target fpr +// expect some deviation from the capacity in 1) +my_filter f(10'000'000, 1E-4); + +// 3) equivalent to 2) +my_filter f(my_filter::capacity_for(10'000'000, 1E-4)); +----- diff --git a/doc/bloom/copyright.adoc b/doc/bloom/copyright.adoc new file mode 100644 index 0000000..1fe26e8 --- /dev/null +++ b/doc/bloom/copyright.adoc @@ -0,0 +1,10 @@ +[#copyright] += Copyright and License + +:idprefix: copyright_ + +Of this documentation: + +* Copyright © 2025 Joaquín M López Muñoz + +Distributed under the http://www.boost.org/LICENSE_1_0.txt[Boost Software License, Version 1.0^]. diff --git a/doc/bloom/fpr_estimation.adoc b/doc/bloom/fpr_estimation.adoc new file mode 100644 index 0000000..0af5e7f --- /dev/null +++ b/doc/bloom/fpr_estimation.adoc @@ -0,0 +1,74 @@ +[#fpr_estimation] += Appendix A: FPR Estimation + +:idprefix: fpr_estimation_ + +For a classical Bloom filter, the theoretical false positive rate, under some simplifying assumptions, +is given by + +[.text-center] +{small}stem:[\text{FPR}(n,m,k)=\left(1 - \left(1 - \displaystyle\frac{1}{m}\right)^{kn}\right)^k \approx \left(1 - e^{-kn/m}\right)^k]{small-end} for large {small}stem:[m]{small-end}, + +where {small}stem:[n]{small-end} is the number of elements inserted in the filter, {small}stem:[m]{small-end} its capacity in bits and {small}stem:[k]{small-end} the +number of bits set per insertion (see a https://en.wikipedia.org/wiki/Bloom_filter#Probability_of_false_positives[derivation^] +of this formula). For a given inverse load factor {small}stem:[c=m/n]{small-end}, the optimum {small}stem:[k]{small-end} is +the integer closest to: + +[.text-center] +{small}stem:[k_{\text{opt}}=c\cdot\ln2,]{small-end} + +yielding a minimum attainable FPR of {small}stem:[1/2^{k_{\text{opt}}} \approx 0.6185^{c}]{small-end}. + +In the case of filter of the form `boost::bloom::filter>`, we can extend +the approach from https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=f376ff09a64b388bfcde2f5353e9ddb44033aac8[Putze et al.^] +to derive the (approximate but very precise) formula: + +[.text-center] +{small}stem:[\text{FPR}_{\text{block}}(n,m,b,k,k')=\left(\displaystyle\sum_{i=0}^{\infty} \text{Pois}(i,nbk/m) \cdot \text{FPR}(i,b,k')\right)^{k},]{small-end} + +where + +[.text-center] +{small}stem:[\text{Pois}(i,\lambda)=\displaystyle\frac{\lambda^i e^{-\lambda}}{i!}]{small-end} + +is the probability mass function of a https://en.wikipedia.org/wiki/Poisson_distribution[Poisson distribution^] +with mean {small}stem:[\lambda]{small-end}, and {small}stem:[b]{small-end} is the size of `Block` in bits. If we're using `multiblock`, we have + +[.text-center] +{small}stem:[\text{FPR}_\text{multiblock}(n,m,b,k,k')=\left(\displaystyle\sum_{i=0}^{\infty} \text{Pois}(i,nbkk'/m) \cdot \text{FPR}(i,b,1)^{k'}\right)^{k}.]{small-end} + +As we have commented xref:primer_multiblock_filters[before], in general + +[.text-center] +{small}stem:[\text{FPR}_\text{block}(n,m,b,k,k') \geq \text{FPR}_\text{multiblock}(n,m,b,k,k') \geq \text{FPR}(n,m,kk'),]{small-end} + +that is, block and multiblock filters have worse FPR than the classical filter for the same number of bits +set per insertion, but they will be faster. We have the particular case + +[.text-center] +{small}stem:[\text{FPR}_{\text{block}}(n,m,b,k,1)=\text{FPR}_{\text{multiblock}}(n,m,b,k,1)=\text{FPR}(n,m,k),]{small-end} + +which follows simply from the observation that using `{block|multiblock}` behaves exactly as +a classical Bloom filter. + +We don't know of any closed, simple formula for the FPR of block and multiblock filters when +`Bucketsize` is not its "natural" size `xref:subfilters_used_value_size[_used-value-size_]`, +that is, when subfilter subarrays overlap. +We can use the following approximations ({small}stem:[s]{small-end} = `BucketSize` in bits): + +[.text-center] +{small}stem:[\text{FPR}_{\text{block}}(n,m,b,s,k,k')=\left(\displaystyle\sum_{i=0}^{\infty} \text{Pois}\left(i,\frac{n(2b-s)k}{m}\right) \cdot \text{FPR}(i,2b-s,k')\right)^{k},]{small-end} + +{small}stem:[\text{FPR}_\text{multiblock}(n,m,b,s,k,k')=\left(\displaystyle\sum_{i=0}^{\infty} \text{Pois}\left(i,\frac{n(2bk'-s)k}{m}\right) \cdot \text{FPR}\left(i,\frac{2bk'-s}{k'},1\right)^{k'}\right)^{k},]{small-end} + +where the replacement of {small}stem:[b]{small-end} with {small}stem:[2b-s]{small-end} +(or {small}stem:[bk']{small-end} with {small}stem:[2bk'-s]{small-end} for multiblock filters) accounts +for the fact that the window of hashing positions affecting a particular bit spreads due to +overlapping. Note that the formulas reduce to the non-ovelapping case when {small}stem:[s]{small-end} takes its +default value (stem:[b] for block, stem:[bk'] for multiblock). These approximations are acceptable for +low values of {small}stem:[k']{small-end} but tend to underestimate the actual FPR as {small}stem:[k']{small-end} grows. +In general, the use of overlapping improves (decreases) FPR by a factor ranging from +0.6 to 0.9 for typical filter configurations. + +{small}stem:[\text{FPR}_{\text{block}}(n,m,b,s,k,k')]{small-end} and {small}stem:[\text{FPR}_\text{multiblock}(n,m,b,s,k,k')]{small-end} +are the formulas used by the implementation of +`xref:filter_fpr_estimation[boost::filter::fpr_for]`. diff --git a/doc/bloom/implementation_notes.adoc b/doc/bloom/implementation_notes.adoc new file mode 100644 index 0000000..a255c09 --- /dev/null +++ b/doc/bloom/implementation_notes.adoc @@ -0,0 +1,130 @@ +[#implementation_notes] += Appendix B: Implementation Notes + +:idprefix: implementation_notes_ + +== Hash Mixing + +This is the bit-mixing post-process we use to improve the statistical properties +of the hash function when it doesn't have the avalanching property: + +[.text-center] +{small}stem:[m\leftarrow\text{mulx}(h,C)]{small-end}, + +{small}stem:[h'\leftarrow\text{high}(m)\text{ xor }\text{low}(m)]{small-end}, + +where {small}stem:[\text{mulx}]{small-end} denotes 128-bit multiplication of two 64-bit factors, +{small}stem:[\text{high}(m)]{small-end} and {small}stem:[\text{low}(m)]{small-end} +are the high and low 64-bit words of {small}stem:[m]{small-end}, respectively, +{small}stem:[C=\lfloor 2^{64}/\varphi \rfloor]{small-end} and +{small}stem:[\varphi]{small-end} is the https://en.wikipedia.org/wiki/Golden_ratio[golden ratio^]. + +== 32-bit mode + +Internally, we always use 64-bit hash values even if in 32-bit mode, where +the user-provided hash function produces 32-bit outputs. To expand +a 32-bit hash value to 64 bits, we use the same mixing procedure +described +xref:implementation_notes_hash_mixing[above]. + +== Dispensing with Multiple Hash Functions + +Direct implementations of a Bloom filter with {small}stem:[k]{small-end} +bits per operation require {small}stem:[k]{small-end} different and independent +hash functions {small}stem:[h_i(x)]{small-end}, which incurs an important +performance penalty, particularly if the objects are expensive to hash +(e.g. strings). https://www.eecs.harvard.edu/~michaelm/postscripts/rsa2008.pdf[Kirsch and Mitzenmacher^] +show how to relax this requirement down to two different hash functions +{small}stem:[h_1(x)]{small-end} and {small}stem:[h_2(x)]{small-end} linearly +combined as + +[.text-center] +{small}stem:[g_i(x)=h_1(x)+ih_2(x).]{small-end} + +Without formal justification, we have relaxed this even further to just one +initial hash value {small}stem:[h_0=h_0(x)]{small-end}, where new values +{small}stem:[h_i]{small-end} are computed from {small}stem:[h_{i-1}]{small-end} +by means of very cheap mixing schemes. In what follows +{small}stem:[k]{small-end}, {small}stem:[k']{small-end} are the homonym values +in a filter of the form `boost::bloom::filter>`, +{small}stem:[b]{small-end} is `sizeof(Block) * CHAR_BIT`, +and {small}stem:[r]{small-end} is the number of buckets in the filter. + +=== Bucket Location + +To produce a location (i.e. a number {small}stem:[p]{small-end} in {small}stem:[[0,r)]{small-end}) from +{small}stem:[h_{i-1}]{small-end}, instead of the straightforward but costly +procedure {small}stem:[p\leftarrow h_{i-1}\bmod r]{small-end} we resort to +Lemire's https://arxiv.org/pdf/1805.10941[fastrange technique^]. Moreover, +we combine this calculation with the production of {small}stem:[h_{i}]{small-end} +from {small}stem:[h_{i-1}]{small-end} as follows: + +[.text-center] +{small}stem:[m\leftarrow\text{mulx}(h_{i-1},r),]{small-end} + +{small}stem:[p\leftarrow\lfloor m/2^{64} \rfloor=\text{high}(m),]{small-end} + +{small}stem:[h_i\leftarrow m \bmod 2^{64}=\text{low}(m).]{small-end} + +The transformation {small}stem:[h_{i-1} \rightarrow h_i]{small-end} is +a simple https://en.wikipedia.org/wiki/Linear_congruential_generator[multiplicative congruential generator^] +over {small}stem:[2^{64}]{small-end}. For this MCG to produce long +cycles, {small}stem:[h_0]{small-end} must be odd and the multiplicative constant +{small}stem:[r]{small-end} must be {small}stem:[\equiv \pm 3 \text{ (mod 8)}]{small-end}: +to meet these requirements, the implementation adjusts {small}stem:[h_0]{small-end} +to {small}stem:[h_0']{small-end} and {small}stem:[r]{small-end} +to {small}stem:[r']{small-end}. This renders the least significant bit +of {small}stem:[h_i]{small-end} unsuitable for pseudorandomization +(it is always one). + +=== Bit selection + +Inside a subfilter, we must produce {small}stem:[k']{small-end} +values from {small}stem:[h_i]{small-end} in the range +{small}stem:[[0,b)]{small-end} (the positions of the {small}stem:[k']{small-end} +bits). We do this by successively taking {small}stem:[\log_2b]{small-end} bits +from {small}stem:[h_i]{small-end} without utilizing the portion containing +its least significant bit (which is always one as we have discussed). +If we run out of bits (which happens when +{small}stem:[k'> 63/\log_2b]{small-end}), we produce a new hash value +{small}stem:[h_{i+1}]{small-end} from {small}stem:[h_{i}]{small-end} +using the mixing procedure +xref:implementation_notes_hash_mixing[already described]. + +== SIMD algorithms + +=== `fast_multiblock32` + +When using AVX2, we select up to 8 bits at a time by creating +a `+++__+++m256i` of 32-bit values {small}stem:[(x_0,x_1,...,x_7)]{small-end} +where each {small}stem:[x_i]{small-end} is constructed from +a different 5-bit portion of the hash value, and calculating from this +the `+++__+++m256i` {small}stem:[(2^{x_0},2^{x_1},...,2^{x_7})]{small-end} +with https://www.intel.com/content/www/us/en/docs/cpp-compiler/developer-guide-reference/2021-10/mm256-sllv-epi32-64.html[`+++_+++mm256_sllv_epi32`^]. +If more bits are needed, we generate a new hash value as +xref:implementation_notes_hash_mixing[described before] and repeat. + +For little-endian Neon, the algorithm is similar but the computations +are carried out with two `uint32x4_t`+++s+++ in parallel as Neon does not have +256-bit registers. + +In the case of SSE2, we don't have the 128-bit equivalent of +`+++_+++mm256_sllv_epi32`, so we use the following, mildly interesting +technique: a `+++__+++m128i` of the form + +[.text-center] +{small}stem:[((x_0+127)\cdot 2^{23},(x_1+127)\cdot 2^{23},(x_2+127)\cdot 2^{23},(x_3+127)\cdot 2^{23}),]{small-end} + +where each {small}stem:[x_i]{small-end} is in {small}stem:[[0,32)]{small-end}, +can be `reinterpret_cast`+++ed+++ to (i.e., has the same binary representation as) +the `+++__+++m128` (register of `float`+++s+++) + +[.text-center] +{small}stem:[(2^{x_0},2^{x_1},2^{x_2},2^{x_3}),]{small-end} + +from which our desired `+++__+++m128i` of shifted 1s can be obtained +with https://www.intel.com/content/www/us/en/docs/cpp-compiler/developer-guide-reference/2021-10/conversion-intrinsics-003.html#GUID-B1CFE576-21E9-4E70-BE5E-B9B18D598C12[`+++_+++mm_cvttps_epi32`^]. + +=== `fast_multiblock64` + +We only provide a SIMD implementation for AVX2 that relies in two +parallel `+++__+++m256i`+++s+++ for the generation of up +to 8 64-bit values with shifted 1s. For Neon and SSE2, emulation +through 4 128-bit registers proved slower than non-SIMD `multiblock`. \ No newline at end of file diff --git a/doc/bloom/intro.adoc b/doc/bloom/intro.adoc new file mode 100644 index 0000000..659c224 --- /dev/null +++ b/doc/bloom/intro.adoc @@ -0,0 +1,49 @@ +[#intro] += Introduction + +:idprefix: intro_ + +Boost.Bloom provides the class template `xref:tutorial[boost::bloom::filter]` +that can be configured to implement a classical Bloom filter as well as +variations discussed in the literature such as block filters, multiblock filters, +and more. + +[listing,subs="+macros,+quotes"] +----- +#include +#include +#include + +int main() +{ + // Bloom filter of strings with 5 bits set per insertion + using filter = boost::bloom::filter; + + // create filter with a capacity of 1'000'000 **bits** + filter f(1'000'000); + + // insert elements (they can't be erased, Bloom filters are insert-only) + f.insert("hello"); + f.insert("Boost"); + //... + + // elements inserted are always correctly checked as such + assert(f.may_contain("hello") == true); + + // elements not inserted may incorrectly be identified as such with a + // false positive rate (FPR) which is a function of the array capacity, + // the number of bits set per element and generally how the boost::bloom::filter + // was specified + if(f.may_contain("bye")) { // likely false + //... + } +} +----- + +The different filter variations supported are specified at compile time +as part of the `boost::bloom::filter` instantiation definition. +Boost.Bloom has been implemented with a focus on performance; +SIMD technologies such as AVX2, Neon and SSE2 can be leveraged to speed up +operations. + +Boost.Bloom is a header-only library. C++11 or later required. \ No newline at end of file diff --git a/doc/bloom/primer.adoc b/doc/bloom/primer.adoc new file mode 100644 index 0000000..80ea1b2 --- /dev/null +++ b/doc/bloom/primer.adoc @@ -0,0 +1,118 @@ +[#primer] += Bloom Filter Primer + +:idprefix: primer_ + +A Bloom filter is a probabilistic data structure where inserted elements can be looked up +with 100% accuracy, whereas looking up for a non-inserted element may fail with +some probability called the filter's _false positive rate_ or FPR. The tradeoff here is +that Bloom filters occupy much less space than traditional non-probabilistic containers +(typically, around 8-20 bits per element) for an acceptably low FPR. The greater +the filter's _capacity_ (its size in bits), the lower the resulting FPR. + +One prime application of Bloom filters and similar data structures is for the prevention +of expensive disk/network accesses when these would fail to retrieve a given piece of +information. +For instance, suppose we are developing a frontend for a database with access time +10 ms and we know 50% of the requests will not succeed (the record does not exist). +Inserting a Bloom filter with a lookup time of 200 ns and a FPR of 0.5% will reduce the +average response time of the system from 10 ms to + +[.text-center] +(10 + 0.0002) × 50.25% + 0.0002 × 49.75% ≅ 5.03 ms, + +that is, we get a ×1.99 overall speedup. If the database holds 1 billion records, +an in-memory filter with say 8 bits per element will occupy 0.93 GB, +which is perfectly realizable. + +image::db_speedup.png[align=center, title="Improving DB negative access time with a Bloom filter."] + +In general, Bloom filters are useful to prevent/mitigate queries against large data sets +when exact retrieval is costly and/or can't be made in main memory. +Applications have been described in the areas of web caching, +dictionary compression, network routing and genomics, among others. +https://www.eecs.harvard.edu/~michaelm/postscripts/im2005b.pdf[Broder and Mitzenmacher^] +provide a rather extensive review of use cases with a focus on networking. + +== Implementation + +The implementation of a Bloom filter consists of an array of _m_ bits, initially set to zero. +Inserting an element _x_ reduces to selecting _k_ positions pseudorandomly (with the help +of _k_ independent hash functions) and setting them to one. + +image::bloom_insertion.png[align=center, title="Insertion in a classical Bloom filter, _k_ = 6."] + +To check if an element _y_ is in the filter, we follow the same procedure and see if +the selected bits are all set to one. In the example figure there are two unset bits, which +definitely indicates _y_ was not inserted in the filter. + +image::bloom_lookup.png[align=center, title="Lookup in a classical Bloom filter."] + +A false positive occurs when the bits checked happen to be all set to one due to +other, unrelated insertions. The probability of having a false positive increases as we +add more elements to the filter, whereas for a given number _n_ of inserted elements, a filter +with greater capacity (larger bit array) will have a lower FPR. +The number _k_ of bits set per operation also affects the FPR, albeit in a more complicated way: +when the array is sparsely populated, a higher value of _k_ improves (decreases) the FPR, +as there are more chances that we hit a non-set bit; however, if _k_ is very high +the array will have more and more bits set to one as new elements are inserted, which +eventually will reach a point where we lose out to a filter with a lower _k_ and +thus a smaller proportions of set bits. + +image::fpr_n_k.png[align=center, title="FPR vs. number of inserted elements for two filters with _m_ = 10^5^ bits."] + +For given values of _n_ and _m_, the optimum _k_ is the integer closest to + +[.text-center] +{small}stem:[k_{\text{opt}}=\displaystyle\frac{m\cdot\ln2}{n}]{small-end} + +for a minimum FPR of +{small}stem:[1/2^{k_{\text{opt}}} \approx 0.6185^{m/n}]{small-end}. See the appendix +on xref:fpr_estimation[FPR estimation] for mode details. + +== Variations on the Classical Filter + +=== Block Filters + +An operation on a Bloom filter involves accessing _k_ different positions in memory, +which, for large arrays, results in _k_ CPU cache misses and affects the +operation's performance. A variation on the classical approach called a +_block filter_ seeks to minimize cache misses by concentrating all bit +setting/checking in a small block of _b_ bits pseudorandomly selected from the +entire array. If the block is small enough, it will fit in a CPU cacheline, +thus drastically reducing the number of cache misses. + +image::block_insertion.png[align=center, title="Block filter."] + +The downside is that the resulting FPR is worse than that of a classical filter for +the same values of _n_, _m_ and _k_. Intuitively, block filters reduce the +uniformity of the distribution of bits in the array, which ultimately hurts their +probabilistic performance. + +image::fpr_n_k_bk.png[align=center, title="FPR (logarithmic scale) vs. number of inserted elements for a classical and a block filter, _m_ = 10^5^ bits."] + +A further variation in this idea is to have operations select _k_ blocks +with _k'_ bits set on each. This, again, will have a worse FPR than a classical +filter with _k·k'_ bits per operation, but improves on a plain +_k·k'_ block filter. + +image::block_multi_insertion.png[align=center, title="Block filter with multi-insertion."] + +=== Multiblock Filters + +_Multiblock filters_ take block filters' approach further by having +bit setting/checking done on a sequence of consecutive blocks of size _b_, +so that each block takes exactly one bit. This still maintains a good cache +locality but improves FPR with respect to block filters because bits set to one +are more spread out across the array. + +image::multiblock_insertion.png[align=center, title="Multiblock filter."] + +Multiblock filters can also be combined with multi-insertion. In general, +for the same number of bits per operation and equal values of _n_ and _m_, +a classical Bloom filter will have the better (lower) FPR, followed by +multiblock filters and then block filters. Execution speed will roughly go +in the reverse order. When considering block/multiblock filters with +multi-insertion, the number of available configurations grows quickly and +you will need to do some experimenting to locate your preferred point in the +(FPR, capacity, speed) tradeoff space. \ No newline at end of file diff --git a/doc/bloom/reference.adoc b/doc/bloom/reference.adoc new file mode 100644 index 0000000..563ab7d --- /dev/null +++ b/doc/bloom/reference.adoc @@ -0,0 +1,14 @@ +[#reference] += Reference + +include::reference/header_filter.adoc[] +include::reference/filter.adoc[] +include::reference/subfilters.adoc[] +include::reference/header_block.adoc[] +include::reference/block.adoc[] +include::reference/header_multiblock.adoc[] +include::reference/multiblock.adoc[] +include::reference/header_fast_multiblock32.adoc[] +include::reference/fast_multiblock32.adoc[] +include::reference/header_fast_multiblock64.adoc[] +include::reference/fast_multiblock64.adoc[] diff --git a/doc/bloom/reference/block.adoc b/doc/bloom/reference/block.adoc new file mode 100644 index 0000000..ffe7bab --- /dev/null +++ b/doc/bloom/reference/block.adoc @@ -0,0 +1,42 @@ +[#block] +== Class Template `block` + +:idprefix: block_ + +`boost::bloom::block` -- A xref:subfilter[subfilter] over an integral type. + +=== Synopsis + +[listing,subs="+macros,+quotes"] +----- +// #include + +namespace boost{ +namespace bloom{ + +template +struct block +{ + static constexpr std::size_t k = K; + using value_type = Block; + + // the rest of the interface is not public + +} // namespace bloom +} // namespace boost +----- + +=== Description + +*Template Parameters* + +[cols="1,4"] +|=== + +|`Block` +|An unsigned integral type. + +|`K` +| Number of bits set/checked per operation. Must be greater than zero. + +|=== diff --git a/doc/bloom/reference/fast_multiblock32.adoc b/doc/bloom/reference/fast_multiblock32.adoc new file mode 100644 index 0000000..63f7b5a --- /dev/null +++ b/doc/bloom/reference/fast_multiblock32.adoc @@ -0,0 +1,52 @@ +[#fast_multiblock32] +== Class Template `fast_multiblock32` + +:idprefix: fast_multiblock32_ + +`boost::bloom::fast_multiblock32` -- A faster replacement of +`xref:multiblock[multiblock]`. + +=== Synopsis + +[listing,subs="+macros,+quotes"] +----- +// #include + +namespace boost{ +namespace bloom{ + +template +struct fast_multiblock32 +{ + static constexpr std::size_t k = K; + using value_type = _implementation-defined_; + + // might not be present + static constexpr std::size_t used_value_size = _implementation-defined_; + + // the rest of the interface is not public + +} // namespace bloom +} // namespace boost +----- + +=== Description + +*Template Parameters* + +[cols="1,4"] +|=== + +|`K` +| Number of bits set/checked per operation. Must be greater than zero. + +|=== + +`fast_multiblock32` is statistically equivalent to +`xref:multiblock[multiblock]`, but takes advantage +of selected SIMD technologies, when available at compile time, to perform faster. +Currently supported: AVX2, little-endian Neon, SSE2. +The non-SIMD case falls back to regular `multiblock`. + +`xref:subfilters_used_value_size[_used-value-size_]>` is +`4 * K`. diff --git a/doc/bloom/reference/fast_multiblock64.adoc b/doc/bloom/reference/fast_multiblock64.adoc new file mode 100644 index 0000000..938f1a0 --- /dev/null +++ b/doc/bloom/reference/fast_multiblock64.adoc @@ -0,0 +1,52 @@ +[#fast_multiblock64] +== Class Template `fast_multiblock64` + +:idprefix: fast_multiblock64_ + +`boost::bloom::fast_multiblock64` -- A faster replacement of +`xref:multiblock[multiblock]`. + +=== Synopsis + +[listing,subs="+macros,+quotes"] +----- +// #include + +namespace boost{ +namespace bloom{ + +template +struct fast_multiblock64 +{ + static constexpr std::size_t k = K; + using value_type = _implementation-defined_; + + // might not be present + static constexpr std::size_t used_value_size = _implementation-defined_; + + // the rest of the interface is not public + +} // namespace bloom +} // namespace boost +----- + +=== Description + +*Template Parameters* + +[cols="1,4"] +|=== + +|`K` +| Number of bits set/checked per operation. Must be greater than zero. + +|=== + +`fast_multiblock64` is statistically equivalent to +`xref:multiblock[multiblock]`, but takes advantage +of selected SIMD technologies, when available at compile time, to perform faster. +Currently supported: AVX2. +The non-SIMD case falls back to regular `multiblock`. + +`xref:subfilters_used_value_size[_used-value-size_]>` is +`8 * K`. diff --git a/doc/bloom/reference/filter.adoc b/doc/bloom/reference/filter.adoc new file mode 100644 index 0000000..55fc179 --- /dev/null +++ b/doc/bloom/reference/filter.adoc @@ -0,0 +1,711 @@ +[#filter] +== Class Template `filter` + +:idprefix: filter_ + +`boost::bloom::filter` -- A data structure that supports element insertion +and _probabilistic_ lookup, where an element can be determined to be in the filter +with high confidence or else not be with absolute certainty. The probability +that lookup erroneously classifies a non-present element as present is called +the filter's _false positive rate_ (FPR). + +`boost::bloom::filter` maintains an internal array of `m` bits where `m` is the +filter's _capacity_. Unlike traditional containers, inserting an +element `x` does not store a copy of `x` within the filter, but rather results +in a fixed number of bits in the array being set to one, where the positions +of the bits are pseudorandomly produced from the hash value of `x`. Lookup +for `y` simply checks whether all the bits associated to `y` are actually set. + +* For a given filter, the FPR increases as new elements are inserted. +* For a given number of inserted elements, a filter with higher capacity +has a lower FPR. + +By convention, we say that a filter is _empty_ if its capacity is zero or +all the bits in the internal array are set to zero. + +=== Synopsis + +[listing,subs="+macros,+quotes"] +----- +// #include + +namespace boost{ +namespace bloom{ + +template< + typename T, std::size_t K, + typename Subfilter = block, std::size_t BucketSize = 0, + typename Hash = boost::hash, typename Allocator = std::allocator +> +class filter +{ +public: + // types and constants + using value_type = T; + static constexpr std::size_t k = K; + using subfilter = Subfilter; + static constexpr std::size_t xref:filter_bucket_size[bucket_size] = xref:filter_bucket_size[__see below__]; + using hasher = Hash; + using allocator_type = Allocator; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using reference = value_type&; + using const_reference = const value_type&; + using pointer = value_type*; + using const_pointer = const value_type*; + + // construct/copy/destroy + xref:#filter_default_constructor[filter](); + explicit xref:#filter_capacity_constructor[filter]( + size_type m, const hasher& h = hasher(), + const allocator_type& al = allocator_type()); + xref:#filter_capacity_constructor[filter]( + size_type n, double fpr, const hasher& h = hasher(), + const allocator_type& al = allocator_type()); + template + xref:#filter_iterator_range_constructor[filter]( + InputIterator first, InputIterator last, + size_type m, const hasher& h = hasher(), + const allocator_type& al = allocator_type()); + template + xref:#filter_iterator_range_constructor[filter]( + InputIterator first, InputIterator last, + size_type n, double fpr, const hasher& h = hasher(), + const allocator_type& al = allocator_type()); + xref:#filter_copy_constructor[filter](const filter& x); + xref:#filter_move_constructor[filter](filter&& x); + template + xref:#filter_iterator_range_constructor_with_allocator[filter]( + InputIterator first, InputIterator last, + size_type m, const allocator_type& al); + template + xref:#filter_iterator_range_constructor_with_allocator[filter]( + InputIterator first, InputIterator last, + size_type n, double fpr, const allocator_type& al); + explicit xref:#filter_allocator_constructor[filter](const allocator_type& al); + xref:#filter_copy_constructor_with_allocator[filter](const filter& x, const allocator_type& al); + xref:#filter_move_constructor_with_allocator[filter](filter&& x, const allocator_type& al); + xref:#filter_initializer_list_constructor[filter]( + std::initializer_list il, + size_type m, const hasher& h = hasher(), + const allocator_type& al = allocator_type()); + xref:#filter_initializer_list_constructor[filter]( + std::initializer_list il, + size_type n, double fpr, const hasher& h = hasher(), + const allocator_type& al = allocator_type()); + xref:#filter_capacity_constructor_with_allocator[filter](size_type m, const allocator_type& al); + xref:#filter_capacity_constructor_with_allocator[filter](size_type n, double fpr, const allocator_type& al); + xref:#filter_initializer_list_constructor_with_allocator[filter]( + std::initializer_list il, + size_type m, const allocator_type& al); + xref:#filter_initializer_list_constructor_with_allocator[filter]( + std::initializer_list il, + size_type n, double fpr, const allocator_type& al); + xref:#filter_destructor[~filter](); + filter& xref:#filter_copy_assignment[operator+++=+++](const filter& x); + filter& xref:#filter_move_assignment[operator+++=+++](filter&& x) + noexcept( + std::allocator_traits::is_always_equal::value || + std::allocator_traits::propagate_on_container_move_assignment::value); + filter& xref:#filter_initializer_list_assignment[operator+++=+++](std::initializer_list il); + allocator_type xref:#filter_get_allocator[get_allocator]() const noexcept; + + // capacity + size_type xref:#filter_capacity_2[capacity]() const noexcept; + static size_type xref:#filter_capacity_estimation[capacity_for](size_type n, double fpr); + static double xref:#filter_fpr_estimation[fpr_for](size_type n,size_type m) + + // modifiers + template + void xref:#filter_emplace[emplace](Args&&... args); + void xref:#filter_insert[insert](const value_type& x); + template + void xref:#filter_insert[insert](const U& x); + template + void xref:#filter_insert_iterator_range[insert](InputIterator first, InputIterator last); + void xref:#filter_insert_initializer_list[insert](std::initializer_list il); + + void xref:#filter_swap[swap](filter& x) + noexcept(std::allocator_traits::is_always_equal::value || + std::allocator_traits::propagate_on_container_swap::value); + void xref:#filter_clear[clear]() noexcept; + void xref:#filter_reset[reset](size_type m = 0); + void xref:#filter_reset[reset](size_type n, double fpr); + + filter& xref:#filter_combine_with_and[operator&=](const filter& x); + filter& xref:#filter_combine_with_or[operator|=](const filter& x); + + // observers + hasher xref:#filter_hash_function[hash_function]() const; + + // lookup + bool xref:#filter_may_contain[may_contain](const value_type& x) const; + template + bool xref:#filter_may_contain[may_contain](const U& x) const; +}; + +} // namespace bloom +} // namespace boost +----- + +=== Description + +*Template Parameters* + +[cols="1,4"] +|=== + +|`T` +|The cv-unqualified object type of the elements inserted into the filter. + +|`K` +| Number of times the associated subfilter is invoked per element upon insertion or lookup. +`K` must be greater than zero. + +|`Subfilter` +| A xref:subfilter[subfilter] type providing the exact algorithm for +bit setting/checking into the filter's internal array. The subfilter is invoked `K` times +per operation on `K` pseudorandomly selected portions of the array (_subarrays_) of width +`xref:subfilters_used_value_size[_used-value-size_]`. + +|`BucketSize` +| Distance in bytes between the initial positions of consecutive subarrays. +If `BucketSize` is specified as zero, the actual distance is automatically selected to +`_used-value-size_` (non-overlapping subarrays). +Otherwise, `BucketSize` must be not greater than `_used-value-size_`. + +|`Hash` +|A https://en.cppreference.com/w/cpp/named_req/Hash[Hash^] type over `T`. + +|`Allocator` +|An https://en.cppreference.com/w/cpp/named_req/Allocator[Allocator^] whose value type is `T`. + +|=== + +Allocation and deallocation of the internal array is done through an internal copy of the +provided allocator. `value_type` construction/destruction (which only happens in +`xref:filter_emplace[emplace]`) uses +`std::allocator_traits::construct`/`destroy`. + +If `link:../../../unordered/doc/html/unordered/reference/hash_traits.html#hash_traits_hash_is_avalanching[boost::unordered::hash_is_avalanching]::value` +is `true` and `sizeof(std::size_t) >= 8`, +the hash function is used as-is; otherwise, a bit-mixing post-processing stage +is added to increase the quality of hashing at the expense of extra computational cost. + +=== Types and Constants + +[[filter_bucket_size]] +[listing,subs="+macros,+quotes"] +---- +static constexpr std::size_t bucket_size; +---- + +Equal to `BucketSize` if that parameter was specified as distinct from zero. +Otherwise, equal to `xref:subfilters_used_value_size[_used-value-size_]`. + +=== Constructors + +==== Default Constructor +[listing,subs="+macros,+quotes"] +---- +filter(); +---- + +Constructs an empty filter using `hasher()` as the hash function and +`allocator_type()` as the allocator. + +[horizontal] +Preconditions:;; `hasher`, and `allocator_type` must be https://en.cppreference.com/w/cpp/named_req/DefaultConstructible[DefaultConstructible^]. +Postconditions:;; `capacity() == 0`. + +==== Capacity Constructor +[listing,subs="+macros,+quotes"] +---- +explicit filter( + size_type m, const hasher& h = hasher(), + const allocator_type& al = allocator_type()); +filter( + size_type n, double fpr, const hasher& h = hasher(), + const allocator_type& al = allocator_type()); +---- + +Constructs an empty filter using copies of `h` and `al` as the hash function and allocator, respectively. + +[horizontal] +Postconditions:;; `capacity() == 0` if `m == 0`, `capacity() >= m` otherwise (first overload). + +`capacity() == capacity_for(n, fpr)` (second overload). + +==== Iterator Range Constructor +[listing,subs="+macros,+quotes"] +---- +template + filter( + InputIterator first, InputIterator last, + size_type m, const hasher& h = hasher(), + const allocator_type& al = allocator_type()); +template + filter( + InputIterator first, InputIterator last, + size_type n, double fpr, const hasher& h = hasher(), + const allocator_type& al = allocator_type()); +---- + +Constructs a filter using copies of `h` and `al` as the hash function and allocator, respectively, +and inserts the values from `[first, last)` into it. + +[horizontal] +Preconditions:;; `InputIterator` is a https://en.cppreference.com/w/cpp/named_req/InputIterator[LegacyInputIterator^] referring to `value_type`. + +`[first, last)` is a valid range. +Postconditions:;; `capacity() == 0` if `m == 0`, `capacity() >= m` otherwise (first overload). + +`capacity() == capacity_for(n, fpr)` (second overload). + +`may_contain(x)` for all values `x` from `[first, last)`. + +==== Copy Constructor +[listing,subs="+macros,+quotes"] +---- +filter(const filter& x); +---- + +Constructs a filter using copies of `x`++'++s internal array, `x.hash_function()` +and `std::allocator_traits::select_on_container_copy_construction(x.get_allocator())`. + +[horizontal] +Postconditions:;; `*this == x`. + +==== Move Constructor + +[listing,subs="+macros,+quotes"] +---- +filter(filter&& x); +---- + +Constructs a filter tranferring `x`++'++s internal array to `*this` and using +a hash function and allocator move-constructed from `x`++'++s hash function +and allocator, respectively. + +[horizontal] +Postconditions:;; `x.capacity() == 0`. + +==== Iterator Range Constructor with Allocator + +[listing,subs="+macros,+quotes"] +---- +template + filter( + InputIterator first, InputIterator last, + size_type m, const allocator_type& al); +template + filter( + InputIterator first, InputIterator last, + size_type n, double fpr, const allocator_type& al); +---- + +Equivalent to `xref:#filter_iterator_range_constructor[filter](first, last, m, hasher(), al)` (first overload) +or `xref:#filter_iterator_range_constructor[filter](first, last, n, fpr, hasher(), al)` (second overload). + +==== Allocator Constructor + +[listing,subs="+macros,+quotes"] +---- +explicit filter(const allocator_type& al); +---- + +Constructs an empty filter using `hasher()` as the hash function and +a copy of `al` as the allocator. + +[horizontal] +Preconditions:;; `hasher` must be https://en.cppreference.com/w/cpp/named_req/DefaultConstructible[DefaultConstructible^]. +Postconditions:;; `capacity() == 0`. + +==== Copy Constructor with Allocator + +[listing,subs="+macros,+quotes"] +---- +filter(const filter& x, const allocator_type& al); +---- + +Constructs a filter using copies of `x`++'++s internal array, `x.hash_function()` +and `al`. + +[horizontal] +Postconditions:;; `*this == x`. + +==== Move Constructor with Allocator + +[listing,subs="+macros,+quotes"] +---- +filter(filter&& x, const allocator_type& al); +---- + +Constructs a filter tranferring `x`++'++s internal array to `*this` if +`al == x.get_allocator()`, or using a copy of the array otherwise. +The hash function of the new filter is move-constructed from `x`++'++s +hash function and the allocator is a copy of `al`. + +[horizontal] +Postconditions:;; `x.capacity() == 0`. + +==== Initializer List Constructor + +[listing,subs="+macros,+quotes"] +---- +filter( + std::initializer_list il, + size_type m, const hasher& h = hasher(), + const allocator_type& al = allocator_type()); +filter( + std::initializer_list il, + size_type n, double fpr, const hasher& h = hasher(), + const allocator_type& al = allocator_type()); +---- + +Equivalent to `xref:#filter_iterator_range_constructor[filter](il.begin(), il.end(), m, h, al)` (first overload) +or `xref:#filter_iterator_range_constructor[filter](il.begin(), il.end(), n, fpr, h, al)` (second overload). + + +==== Capacity Constructor with Allocator + +[listing,subs="+macros,+quotes"] +---- +filter(size_type m, const allocator_type& al); +filter(size_type n, double fpr, const allocator_type& al); +---- + +Equivalent to `xref:#filter_capacity_constructor[filter](m, hasher(), al)` (first overload) +or `xref:#filter_capacity_constructor[filter](n, fpr, hasher(), al)` (second overload). + + +==== Initializer List Constructor with Allocator + +[listing,subs="+macros,+quotes"] +---- +filter( + std::initializer_list il, + size_type m, const allocator_type& al); +filter( + std::initializer_list il, + size_type n, double fpr, const allocator_type& al); +---- + +Equivalent to `xref:#filter_initializer_list_constructor[filter](il, m, hasher(), al)` (first overload) +or `xref:#filter_initializer_list_constructor[filter](il, n, fpr, hasher(), al)` (second overload). + +=== Destructor + +[listing,subs="+macros,+quotes"] +---- +~filter(); +---- + +Deallocates the internal array and destructs the internal hash function and allocator. + +=== Assignment + +==== Copy Assignment + +[listing,subs="+macros,+quotes"] +---- +filter& operator=(const filter& x); +---- + +Let `pocca` be `std::allocator_traits::propagate_on_container_copy_assignment::value`. +If `pocca`, replaces the internal allocator `al` with a copy of `x.get_allocator()`. +If `capacity() != x.capacity()` or `pocca && al != x.get_allocator()`, replaces the internal array +with a new one with capacity `x.capacity()`. +Copies the values of `x`++'++s internal array. +Replaces the internal hash function with a copy of `x.hash_function()`. + +[horizontal] +Preconditions:;; If `pocca`, +`Allocator` is nothrow https://en.cppreference.com/w/cpp/named_req/CopyAssignable[CopyAssignable^]. + +`hasher` is nothrow https://en.cppreference.com/w/cpp/named_req/Swappable[Swappable^]. +Postconditions:;; `*this == x`. +Returns:;; `*this`. + +==== Move Assignment + +[listing,subs="+macros,+quotes"] +---- +filter& operator=(filter&& x) + noexcept( + std::allocator_traits::is_always_equal::value || + std::allocator_traits::propagate_on_container_move_assignment::value); +---- + +Let `pocma` be `std::allocator_traits::propagate_on_container_move_assignment::value`. +If `pocma`, replaces the internal allocator with a copy of `x.get_allocator()`. +If `get_allocator() == x.get_allocator()`, transfers `x`++'++s internal array to `*this`; +otherwise, replaces the internal array with a new one with capacity `x.capacity()` +and copies the values of `x`++'++s internal array. +Replaces the internal hash function with a copy of `x.hash_function()`. + +[horizontal] +Preconditions:;; If `pocma`, +`Allocator` is nothrow https://en.cppreference.com/w/cpp/named_req/CopyAssignable[CopyAssignable^]. + +`hasher` is nothrow https://en.cppreference.com/w/cpp/named_req/Swappable[Swappable^]. +Postconditions:;; `x.capacity() == 0`. +Returns:;; `*this`. + +==== Initializer List Assignment + +[listing,subs="+macros,+quotes"] +---- +filter& operator=(std::initializer_list il); +---- + +Clears the filter and inserts the values from `il`. + +[horizontal] +Returns:;; `*this`. + +=== Capacity + +==== Capacity + +[listing,subs="+macros,+quotes"] +---- +size_type capacity() const noexcept; +---- + +[horizontal] +Returns:;; The size in bits of the internal array. + +==== Capacity Estimation + +[listing,subs="+macros,+quotes"] +---- +static size_type capacity_for(size_type n, double fpr); +---- + +[horizontal] +Preconditions:;; `fpr` is between 0.0 and 1.0. +Postconditions:;; `filter(capacity_for(n, fpr)).capacity() == capacity_for(n, fpr)`. + +`capacity_for(n, 1.0) == 0`. +Returns:;; An estimation of the capacity required by a `filter` to attain a false positive rate +equal to `fpr` when `n` distinct elements have been inserted. + +==== FPR Estimation + +[listing,subs="+macros,+quotes"] +---- +static double fpr_for(size_type n, size_type m); +---- + +[horizontal] +Postconditions:;; `fpr_for(n, m)` is between 0.0 and 1.0. + +`fpr_for(n, 0) == 1.0`. + +`fpr_for(0, m) == 0.0` (if `m != 0`). + +Returns:;; An estimation of the resulting false positive rate when +`n` distinct elements have been inserted into a `filter` +with capacity `m`. + +=== Modifiers + +==== Emplace + +[listing,subs="+macros,+quotes"] +---- +template void emplace(Args&&... args); +---- + +Inserts an element constructed from `std::forward(args)+++...+++`. + +[horizontal] +Preconditions:;; `value_type` is https://en.cppreference.com/w/cpp/named_req/EmplaceConstructible[EmplaceConstructible^] +into `filter` from `std::forward(args)+++...+++`. + +`value_type` is https://en.cppreference.com/w/cpp/named_req/Erasable[Erasable^] from `filter`. + +==== Insert + +[listing,subs="+macros,+quotes"] +---- +void insert(const value_type& x); +template void insert(const U& x); +---- + +If `capacity() != 0`, sets to one `k * subfilter::k` (not necessarily distinct) +bits of the internal array deterministically selected from the value +`hash_function()(x)`. + +[horizontal] +Postconditions:;; `may_contain(x)`. +Notes:;; The second overload only participates in overload resolution if +`hasher::is_transparent` is a valid member typedef. + +==== Insert Iterator Range + +[listing,subs="+macros,+quotes"] +---- +template + void insert(InputIterator first, InputIterator last); +---- + +Equivalent to `while(first != last) xref:#filter_insert[insert](*first++)`. + +[horizontal] +Preconditions:;; `InputIterator` is a https://en.cppreference.com/w/cpp/named_req/InputIterator[LegacyInputIterator^] referring to `value_type`. + +`[first, last)` is a valid range. + +==== Insert Initializer List + +[listing,subs="+macros,+quotes"] +---- +void insert(std::initializer_list il); +---- + +Equivalent to `xref:#filter_insert_iterator_range[insert](il.begin(), il.end())`. + +==== Swap + +[listing,subs="+macros,+quotes"] +---- +void swap(filter& x) + noexcept(std::allocator_traits::is_always_equal::value || + std::allocator_traits::propagate_on_container_swap::value); +---- + +Let `pocs` be `std::allocator_traits::propagate_on_container_swap::value`. +Swaps the internal array and hash function with those of `x`. +If `pocs`, swaps the internal allocator with that of `x`. + +[horizontal] +Preconditions:;; `pocs || get_allocator() == x.get_allocator()`. + +If `pocs`, `Allocator` is nothrow https://en.cppreference.com/w/cpp/named_req/Swappable[Swappable^]. + +`hasher` is nothrow https://en.cppreference.com/w/cpp/named_req/Swappable[Swappable^]. + + +==== Clear + +[listing,subs="+macros,+quotes"] +---- +void clear() noexcept; +---- + +Sets to zero all the bits in the internal array. + +==== Reset + +[listing,subs="+macros,+quotes"] +---- +void reset(size_type m = 0); +void reset(size_type n, double fpr); +---- + +First overload: Replaces the internal array if the resulting capacity calculated from `m` is not +equal to `capacity()`, and clears the filter. + +Second overload: Equivalent to `reset(capacity_for(n, fpr))`. + +[horizontal] +Postconditions:;; In general, `capacity() >= m`. + +If `m == 0` or `m == capacity()` or `m == capacity_for(n, fpr)` for some `n` and `fpr`, then `capacity() == m`. + +==== Combine with AND + +[listing,subs="+macros,+quotes"] +---- +filter& operator&=(const filter& x); +---- + +If `capacity() != x.capacity()`, throws a `std::invalid_argument` exception; +otherwise, changes the value of each bit in the internal array with the result of +doing a logical AND operation of that bit and the corresponding one in `x`. + +[horizontal] +Returns:;; `*this`; + +==== Combine with OR + +[listing,subs="+macros,+quotes"] +---- +filter& operator|=(const filter& x); +---- + +If `capacity() != x.capacity()`, throws an `std::invalid_argument` exception; +otherwise, changes the value of each bit in the internal array with the result of +doing a logical OR operation of that bit and the corresponding one in `x`. + +[horizontal] +Returns:;; `*this`; + +=== Observers + +==== get_allocator + +[listing,subs="+macros,+quotes"] +---- +allocator_type get_allocator() const noexcept; +---- + +[horizontal] +Returns:;; A copy of the internal allocator. + +==== hash_function + +[listing,subs="+macros,+quotes"] +---- +hasher hash_function() const; +---- + +[horizontal] +Returns:;; A copy of the internal hash function. + +=== Lookup + +==== may_contain + +[listing,subs="+macros,+quotes"] +---- +bool may_contain(const value_type& x) const; +template bool may_contain(const U& x) const; +---- + +[horizontal] +Returns:;; `true` iff all the bits selected by a hypothetical +`xref:filter_insert[insert](x)` operation are set to one. +Notes:;; The second overload only participates in overload resolution if +`hasher::is_transparent` is a valid member typedef. + +=== Comparison + +==== operator== + +[listing,subs="+macros,+quotes"] +---- +template< + typename T, std::size_t K, typename S, std::size_t B, typename H, typename A +> +bool operator==( + const filter& x, const filter& y); +---- + +[horizontal] +Returns:;; `true` iff `x.capacity() == y.capacity()` and +`x`++'++s and `y`++'++s internal arrays are bitwise identical. + +==== operator!= + +[listing,subs="+macros,+quotes"] +---- +template< + typename T, std::size_t K, typename S, std::size_t B, typename H, typename A +> +bool operator!=( + const filter& x, const filter& y); +---- + +[horizontal] +Returns:;; `!(x xref:filter_operator[==] y)`. + + +=== Swap + +[listing,subs="+macros,+quotes"] +---- +template< + typename T, std::size_t K, typename S, std::size_t B, typename H, typename A +> +void swap(filter& x, filter& y) + noexcept(noexcept(x.swap(y))); +---- + +Equivalent to `x.xref:filter_swap[swap](y)`. diff --git a/doc/bloom/reference/header_block.adoc b/doc/bloom/reference/header_block.adoc new file mode 100644 index 0000000..b307aa0 --- /dev/null +++ b/doc/bloom/reference/header_block.adoc @@ -0,0 +1,17 @@ +[#header_block] +== `` + +:idprefix: header_block_ + +[listing,subs="+macros,+quotes"] +----- +namespace boost{ +namespace bloom{ + +template +struct xref:block[block]; + +} // namespace bloom +} // namespace boost +----- + diff --git a/doc/bloom/reference/header_fast_multiblock32.adoc b/doc/bloom/reference/header_fast_multiblock32.adoc new file mode 100644 index 0000000..e271777 --- /dev/null +++ b/doc/bloom/reference/header_fast_multiblock32.adoc @@ -0,0 +1,17 @@ +[#header_fast_multiblock32] +== `` + +:idprefix: header_fast_multiblock32_ + +[listing,subs="+macros,+quotes"] +----- +namespace boost{ +namespace bloom{ + +template +struct xref:fast_multiblock32[fast_multiblock32]; + +} // namespace bloom +} // namespace boost +----- + diff --git a/doc/bloom/reference/header_fast_multiblock64.adoc b/doc/bloom/reference/header_fast_multiblock64.adoc new file mode 100644 index 0000000..e8ba7d4 --- /dev/null +++ b/doc/bloom/reference/header_fast_multiblock64.adoc @@ -0,0 +1,17 @@ +[#header_fast_multiblock64] +== `` + +:idprefix: header_fast_multiblock64_ + +[listing,subs="+macros,+quotes"] +----- +namespace boost{ +namespace bloom{ + +template +struct xref:fast_multiblock64[fast_multiblock64]; + +} // namespace bloom +} // namespace boost +----- + diff --git a/doc/bloom/reference/header_filter.adoc b/doc/bloom/reference/header_filter.adoc new file mode 100644 index 0000000..2a1b793 --- /dev/null +++ b/doc/bloom/reference/header_filter.adoc @@ -0,0 +1,42 @@ +[#header_filter] +== `` + +:idprefix: header_filter_ + +Defines `xref:filter[boost::bloom::filter]` +and associated functions. + +[listing,subs="+macros,+quotes"] +----- +namespace boost{ +namespace bloom{ + +template< + typename T, std::size_t K, + typename Subfilter = block, std::size_t BucketSize = 0, + typename Hash = boost::hash, typename Allocator = std::allocator +> +class xref:filter[filter]; + +template< + typename T, std::size_t K, typename S, std::size_t B, typename H, typename A +> +bool xref:filter_operator[operator+++==+++]( + const filter& x, const filter& y); + +template< + typename T, std::size_t K, typename S, std::size_t B, typename H, typename A +> +bool xref:filter_operator_2[operator!=]( + const filter& x, const filter& y); + +template< + typename T, std::size_t K, typename S, std::size_t B, typename H, typename A +> +void xref:filter_swap_2[swap](filter& x, filter& y) + noexcept(noexcept(x.swap(y))); + +} // namespace bloom +} // namespace boost +----- + diff --git a/doc/bloom/reference/header_multiblock.adoc b/doc/bloom/reference/header_multiblock.adoc new file mode 100644 index 0000000..dbf6a98 --- /dev/null +++ b/doc/bloom/reference/header_multiblock.adoc @@ -0,0 +1,17 @@ +[#header_multiblock] +== `` + +:idprefix: header_multiblock_ + +[listing,subs="+macros,+quotes"] +----- +namespace boost{ +namespace bloom{ + +template +struct xref:multiblock[multiblock]; + +} // namespace bloom +} // namespace boost +----- + diff --git a/doc/bloom/reference/multiblock.adoc b/doc/bloom/reference/multiblock.adoc new file mode 100644 index 0000000..cddd1d4 --- /dev/null +++ b/doc/bloom/reference/multiblock.adoc @@ -0,0 +1,45 @@ +[#multiblock] +== Class Template `multiblock` + +:idprefix: multiblock_ + +`boost::bloom::multiblock` -- A xref:subfilter[subfilter] over an array of an integral type. + +=== Synopsis + +[listing,subs="+macros,+quotes"] +----- +// #include + +namespace boost{ +namespace bloom{ + +template +struct multiblock +{ + static constexpr std::size_t k = K; + using value_type = Block[k]; + + // the rest of the interface is not public + +} // namespace bloom +} // namespace boost +----- + +=== Description + +*Template Parameters* + +[cols="1,4"] +|=== + +|`Block` +|An unsigned integral type. + +|`K` +| Number of bits set/checked per operation. Must be greater than zero. + +|=== + +Each of the `K` bits set/checked is located in a different element of the +`Block[K]` array. diff --git a/doc/bloom/reference/subfilters.adoc b/doc/bloom/reference/subfilters.adoc new file mode 100644 index 0000000..19662df --- /dev/null +++ b/doc/bloom/reference/subfilters.adoc @@ -0,0 +1,57 @@ +[#subfilter] +== Subfilters + +:idprefix: subfilters_ + +A _subfilter_ implements a specific algorithm for bit setting (insertion) and +bit checking (lookup) for `boost::bloom::filter`. Subfilters operate +on portions of the filter's internal array called _subarrays_. The +exact width of these subarrays is statically dependent on the subfilter type. + +The full interface of a conforming subfilter is not exposed publicly, hence +users can't provide their own subfilters and may only use those natively +provided by the library. What follows is the publicly available interface. + +[listing,subs="+macros,+quotes"] +----- +Subfilter::k +----- + +[horizontal] +Result:;; A compile-time `std::size_t` value indicating +the number of (not necessarily distinct) bits set/checked per operation. + +[listing,subs="+macros,+quotes"] +----- +typename Subfilter::value_type +----- + +[horizontal] +Result:;; A cv-unqualified, +https://en.cppreference.com/w/cpp/named_req/TriviallyCopyable[TriviallyCopyable^] +type to which the subfilter projects assigned subarrays. + +[listing,subs="+macros,+quotes"] +----- +Subfilter::used_value_size +----- + +[horizontal] +Result:;; A compile-time `std::size_t` value indicating +the size of the effective portion of `Subfilter::value_type` used +for bit setting/checking (assumed to begin at the lowest address in memory). +Postconditions:;; Greater than zero and not greater than `sizeof(Subfilter::value_type)`. +Notes:;; Optional. + +=== _used-value-size_ + +[listing,subs="+macros,+quotes"] +----- +template +constexpr std::size_t _used-value-size_; // exposition only +----- + +`_used-value-size_` is `Subfilter::used_value_size` if this nested +constant exists, or `sizeof(Subfilter::value_type)` otherwise. +The value is the effective size in bytes of the subarrays upon which a +given subfilter operates. diff --git a/doc/bloom/release_notes.adoc b/doc/bloom/release_notes.adoc new file mode 100644 index 0000000..7f5d630 --- /dev/null +++ b/doc/bloom/release_notes.adoc @@ -0,0 +1,9 @@ +[#release_notes] += Release Notes + +:idprefix: release_notes_ + +== Boost 1.xx + +* Initial release. + diff --git a/doc/bloom/tutorial.adoc b/doc/bloom/tutorial.adoc new file mode 100644 index 0000000..0302a64 --- /dev/null +++ b/doc/bloom/tutorial.adoc @@ -0,0 +1,204 @@ +[#tutorial] += Tutorial + +:idprefix: tutorial_ + +== Filter Definition + +A `boost::bloom::filter` can be regarded as a bit array divided into _buckets_ that +are selected pseudo-randomly (based on a hash function) upon insertion: +each of the buckets is passed to a _subfilter_ that marks several of its bits according +to some associated strategy. + +[listing,subs="+macros,+quotes"] +----- +template< + typename T, std::size_t K, + typename Subfilter = block, std::size_t BucketSize = 0, + typename Hash = boost::hash, typename Allocator = std::allocator +> +class filter; +----- + +* `T`: Type of the elements inserted. +* `K`: Number of buckets marked per insertion. +* `xref:tutorial_subfilter[Subfilter]`: Type of subfilter used. +* `xref:tutorial_bucketsize[BucketSize`]: Size in bytes of the buckets. +* `xref:tutorial_hash[Hash]`: A hash function for `T`. +* `Allocator`: An allocator for `T`. + +=== `Subfilter` + +The following subfilters can be selected, offering different compromises +between performance and _false positive rate_ (FPR). +See the xref:primer_variations_on_the_classical_filter[Bloom Filter Primer] +for a general explanation of block and multiblock filters. + +`block` + +[.indent] +Sets `K'` bits in an underlying value of the unsigned integral type `Block` +(e.g. `unsigned char`, `uint32_t`, `uint64_t`). So, +a `filter>` will set `K * K'` bits per element. +The tradeoff here is that insertion/lookup will be (much) faster than +with `filter` while the FPR will be worse (larger). +FPR is better the wider `Block` is. + +`multiblock` + +[.indent] +Instead of setting `K'` bits in a `Block` value, this subfilter sets +one bit on each of the elements of a `Block[K']` subarray. This improves FPR +but impacts performance with respect to `block`, among other +things because cacheline boundaries can be crossed when accessing the subarray. + +`fast_multiblock32` + +[.indent] +Statistically equivalent to `multiblock`, but uses +faster SIMD-based algorithms when SSE2, AVX2 or Neon are available. + +`fast_multiblock64` + +[.indent] +Statistically equivalent to `multiblock`, but uses a +faster SIMD-based algorithm when AVX2 is available. + +The default configuration with `block` corresponds to a +xref:primer[classical Bloom filter] setting `K` bits per element uniformly +distributed across the array. + +=== `BucketSize` + +When the default value 0 is used, buckets have the same size as +the _subarrays_ subfilters operate on (non-overlapping case). +Otherwise, bucket size is smaller and subarrays spill over adjacent buckets, +which results in an improved (lower) FPR in exchange for a possibly +worse performance due to memory unalignment. + +=== `Hash` + +By default, link:../../../container_hash/index.html[Boost.ContainerHash] is used. +Consult this library's link:../../../container_hash/doc/html/hash.html#user[dedicated section] +if you need to extend `boost::hash` for your own types. + +When the provided hash function is of sufficient quality, it is used +as is; otherwise, a bit-mixing post-process is applied to hash values that improves +their statistical properties so that the resulting FPR approaches its +theoretical limit. The hash function is determined to be of high quality +(more precisely, to have the so-called _avalanching_ property) via the +`link:../../../unordered/doc/html/unordered/reference/hash_traits.html#hash_traits_hash_is_avalanching[boost::unordered::hash_is_avalanching]` +trait. + +== Capacity + +The size of the filter's internal array is specified at construction time: + +[listing,subs="+macros,+quotes"] +----- +using filter = boost::bloom::filter; +filter f(1'000'000); // array of 1'000'000 **bits** +std::cout << f.capacity(); // >= 1'000'000 +----- + +Note that `boost::bloom::filter` default constructor specifies a capacity +of zero, which in general won't be of much use -- the assigned array +is null. + +Instead of specifying the array's capacity directly, we can let the library +figure it out based on the number of elements we plan to insert and the +desired FPR: + +[listing,subs="+macros,+quotes"] +----- +// we'll insert 100'000 elements and want a FPR ~ 1% +filter f(100'000, 0.01); + +// this is equivalent +filter f2(filter::capacity_for(100'000, 0.01)); +----- + +Once a filter is constructed, its array is fixed (for instance, it won't +grow dynamically as elements are inserted). The only way to change it is +by assignment/swapping from a different filter, or using `reset`: + +[listing,subs="+macros,+quotes"] +----- +f.reset(2'000'000); // change to 2'000'000 bits **and clears the filter** +f.reset(100'000, 0.005); // equivalent to reset(filter::capacity_for(100'000, 0.005)); +f.reset(); // null array (capacity == 0) +----- + +== Insertion and Lookup + +Insertion is done in much the same way as with a traditional container: + +[listing,subs="+macros,+quotes"] +----- +f.insert("hello"); +f.emplace(100, 'X'); // ~ insert(std::string(100, 'X')) +f.insert(data.begin(), data.end()); +----- + +Of course, in this context "insertion" does not involve any actual +storage of elements into the filter, but rather the setting of bits in the +internal array based on the hash values of those elements. +Lookup goes as follows: + +[listing,subs="+macros,+quotes"] +----- +bool b1 = f.may_contain("hello"); // b1 is true since we actually inserted "hello" +bool b2 = f.may_contain("bye"); // b2 is most likely false +----- + +As its name suggests, `may_contain` can return `true` even if the +element has not been previously inserted, that is, it may yield false +positives -- this is the essence of probabilistic data structures. +`fpr_for` provides an estimation of the false positive rate: + +[listing,subs="+macros,+quotes"] +----- +// we have inserted 100 elements so far, what's our FPR? +std::cout<< filter::fpr_for(100, f.capacity()); +----- + +Note that in the example we provided the number 100 externally: +`boost::bloom::filter` does not keep track of the number of elements +that have been inserted -- in other words, it does not have a `size` +operation. + +Once inserted, there is no way to remove a specific element from the filter. +We can only clear up the filter entirely: + +[listing,subs="+macros,+quotes"] +----- +f.clear(); // sets all the bits in the array to zero +----- + +== Filter Combination + +`boost::bloom::filter`+++s+++ can be combined by doing the OR logical operation +of the bits of their arrays: + +[listing,subs="+macros,+quotes"] +----- +filter f2=...; +... +f|=f2; // f and f2 must have exactly the same capacity +----- + +The result is equivalent to a filter "containing" both the elements +of `f` and `f2`. AND combination, on the other hand, results in a filter +holding the _intersection_ of the elements: + +[listing,subs="+macros,+quotes"] +----- +filter f3=...; +... +f&=f3; // f and f3 must have exactly the same capacity +----- + +For AND combination, be aware that the resulting FPR will be in general +worse (higher) than if the filter had been constructed from scratch +by inserting only the commom elements -- don't trust `fpr_for` in this +case. diff --git a/doc/img/block_insertion.png b/doc/img/block_insertion.png new file mode 100644 index 0000000000000000000000000000000000000000..0c159b0b3a72bcb01c5f7b5f50e00514a355f489 GIT binary patch literal 4711 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 KelF{r5}E)%9TGJF literal 0 HcmV?d00001 diff --git a/doc/img/block_multi_insertion.png b/doc/img/block_multi_insertion.png new file mode 100644 index 0000000000000000000000000000000000000000..148ddefac291b540ae77a3c4b3e9395d0f7fdbc4 GIT binary patch literal 4707 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 literal 0 HcmV?d00001 diff --git a/doc/img/bloom_insertion.png b/doc/img/bloom_insertion.png new file mode 100644 index 0000000000000000000000000000000000000000..f24afbe43f89bb6c9077828d77c024b6d6bd44e6 GIT binary patch literal 4915 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%0DUru|3X z*5hgX)is+9u55Ca&Q`S7;(>IfK?%Dn<{C!>70lyu;Qyzo_@ok(Z z^6l28y@h`-x^1_JLb2w~!`5YwZ=Kq{_O4Z_(qHj7gLg-&w%_}kD;Wunc^_VRiD#}S zPrdt~ba`X&|G4O~*Vp5sBP^=Fy>WWxeN6G%;brA-U!RZm7I&}tkWzLo@4Jr9nz!%g zow9na8v6E{>1QLemsVysPqVp29GiANjrZY2DT!;_C#9DE_~fLt`gs41hpx+#|I2L; zpJ!G2@zvek_vh~W=(*nXZpHgoDSvzY_3EwvAH8>P=l?)AkF@7orrTHEc{y)&`1$wS z>N%{`cdyDmExq>g$=;Q2amUq8%Sar&b?DUMm)9OYQINRUIPvD6d%wPwzBji&(>*7p z?8mXi*X~^H+pBkW?O|@8<9?Uk=hgl`6`W~$`S9oB&-(u+l+^xho%6nair}Q1mJfek zD9KLN%}|#$5}7+W^6IrKKhK~0&cCq@WRIN0L(O}qUUrLD`_FMJOW3pIXK?V_vftMh zzbs)i(=YlwiI4y8?O3DTPv=Z~MzP2lw53z4QIu+064(ckl$g z?Opow=hfbXOuM(OURKW=wjbZ8aiw^@UHjTs2htNGc#L*kGQ4PRq9u9QRbow&wDjuL zs}q^Nn^&sLir>dLz2jWh&h063yAJbPc+Z=jc;NLetNYuRvF*KF-$ebWW>`X-$*)o}W)w z*C=XzThGYgAnWPQ(1DJh{_pE6y7JC^UiKBslj|*}AGWs2TJli%&abddr^FZ8eD6y? zH^iUHa&mIg z)6=V}tp3J5zPh6LU+w#Q_UmJRU!QCJ|81hTpKKJwe zw{QNFCnvh7wb$>f|8H(=>^W1d``CxSzrUx?GBJ?4_vcfqs)mkBqNNw#|B}zo-uLZE z3;TBJ)sG)Pmd~%7HRbRQ!QQ`FS1j{;`v2E$UzKRl}Bk3IH7%K56>$79U;CiL&$9yR^4l7s2L zTUoK*-3Bssx3hjuWnK`Kza`2t@6u)d_;WS2rawbhzu*7g&a$WG&&T5rUo)21+|1s_ zZj zr|o6^SWv$B#;o~2@9qjw`*GA*EF|=OIZMNg60Dekfq}tdjn;oIO9mcwd)(@9>8HB5 c_|~ZZ>^HL_jn^a{Jq~i6r>mdKI;Vst0CN1vjQ{`u literal 0 HcmV?d00001 diff --git a/doc/img/db_speedup.png b/doc/img/db_speedup.png new file mode 100644 index 0000000000000000000000000000000000000000..245f0c3b64646f13c16e89ef28db89c7e13166a1 GIT binary patch literal 4718 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
  • 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

    OG-!*a4Nt+E1_^!># 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 E020E2z5oCK literal 0 HcmV?d00001 diff --git a/include/boost/bloom/detail/block_base.hpp b/include/boost/bloom/detail/block_base.hpp index 76d52fd..509ee38 100644 --- a/include/boost/bloom/detail/block_base.hpp +++ b/include/boost/bloom/detail/block_base.hpp @@ -32,6 +32,9 @@ struct block_base static constexpr std::size_t k=K; static constexpr std::size_t hash_width=sizeof(boost::uint64_t)*CHAR_BIT; static constexpr std::size_t block_width=sizeof(Block)*CHAR_BIT; + static_assert( + (block_width&(block_width-1))==0, + "Block's size in bits must be a power of two"); static constexpr std::size_t mask=block_width-1; static constexpr std::size_t shift=constexpr_bit_width(mask); static constexpr std::size_t rehash_k=(hash_width-shift)/shift; diff --git a/include/boost/bloom/detail/core.hpp b/include/boost/bloom/detail/core.hpp index 858ecd9..8a728b5 100644 --- a/include/boost/bloom/detail/core.hpp +++ b/include/boost/bloom/detail/core.hpp @@ -60,9 +60,9 @@ namespace detail{ #endif /* mcg_and_fastrange produces (pos,hash') from hash, where - * - x=mulx64(hash,range), mulx64 denotes extended multiplication - * - pos=high(x) - * - hash'=low(x) + * - m=mulx64(hash,range), mulx64 denotes extended multiplication + * - pos=high(m) + * - hash'=low(m) * pos is uniformly distributed in [0,range) (see * https://arxiv.org/pdf/1805.10941), whereas hash'<-hash is a multiplicative * congruential generator of the form hash'<-hash*rng mod 2^64. This MCG @@ -100,20 +100,20 @@ struct mcg_and_fastrange boost::uint64_t rng; }; -/* used_block_size::value is Subfilter::used_value_size if it +/* used_value_size::value is Subfilter::used_value_size if it * exists, or sizeof(Subfilter::value_type) otherwise. This covers the * case where a subfilter only operates on the first bytes of its entire * value_type (e.g. fast_multiblock32 with K<8). */ template -struct used_block_size +struct used_value_size { static constexpr std::size_t value=sizeof(typename Subfilter::value_type); }; template -struct used_block_size< +struct used_value_size< Subfilter, typename std::enable_if::type > @@ -187,14 +187,14 @@ private: static constexpr std::size_t k_total=k*kp; using block_type=typename subfilter::value_type; static constexpr std::size_t block_size=sizeof(block_type); - static constexpr std::size_t used_block_size= - detail::used_block_size::value; + static constexpr std::size_t used_value_size= + detail::used_value_size::value; public: static constexpr std::size_t bucket_size= - BucketSize?BucketSize:used_block_size; + BucketSize?BucketSize:used_value_size; static_assert( - bucket_size<=used_block_size,"BucketSize can't exceed the block size"); + bucket_size<=used_value_size,"BucketSize can't exceed the block size"); private: static constexpr std::size_t tail_size=sizeof(block_type)-bucket_size; @@ -356,7 +356,7 @@ public: static double fpr_for(std::size_t n,std::size_t m) { - return n==0?0.0:m==0?1.0:fpr_for_c((double)m/n); + return m==0?1.0:n==0?0.0:fpr_for_c((double)m/n); } BOOST_FORCEINLINE void insert(boost::uint64_t hash) @@ -410,6 +410,11 @@ public: clear_bytes(); } + void reset(std::size_t n,double fpr) + { + reset(capacity_for(n,fpr)); + } + filter_core& operator&=(const filter_core& x) { combine(x,[](unsigned char& a,unsigned char b){a&=b;}); @@ -459,9 +464,9 @@ private: static std::size_t requested_range(std::size_t m) { - if(m>(used_block_size-bucket_size)*CHAR_BIT){ + if(m>(used_value_size-bucket_size)*CHAR_BIT){ /* ensures filter_core{f.capacity()}.capacity()==f.capacity() */ - m-=(used_block_size-bucket_size)*CHAR_BIT; + m-=(used_value_size-bucket_size)*CHAR_BIT; } return (std::numeric_limits::max)()-m>=bucket_size*CHAR_BIT-1? @@ -530,7 +535,7 @@ private: static std::size_t used_array_size(std::size_t rng)noexcept { - return rng?rng*bucket_size+(used_block_size-bucket_size):0; + return rng?rng*bucket_size+(used_value_size-bucket_size):0; } static std::size_t unadjusted_capacity_for(std::size_t n,double fpr) @@ -539,7 +544,7 @@ private: using double_limits=std::numeric_limits; BOOST_ASSERT(fpr>=0.0&&fpr<=1.0); - if(n==0)return 0; + if(n==0)return fpr==1.0?0:1; constexpr double eps=1.0/(double)(size_t_limits::max)(); constexpr double max_size_t_as_double= @@ -593,7 +598,7 @@ private: static double fpr_for_c(double c) { - constexpr std::size_t w=(2*used_block_size-bucket_size)*CHAR_BIT; + constexpr std::size_t w=(2*used_value_size-bucket_size)*CHAR_BIT; const double lambda=w*k/c; const double loglambda=std::log(lambda); double res=0.0; diff --git a/include/boost/bloom/filter.hpp b/include/boost/bloom/filter.hpp index 8b21783..ee674c5 100644 --- a/include/boost/bloom/filter.hpp +++ b/include/boost/bloom/filter.hpp @@ -146,7 +146,7 @@ public: const allocator_type& al=allocator_type()): super{m,al},hash_base{empty_init,h}{} - explicit filter( + filter( std::size_t n,double fpr,const hasher& h=hasher(), const allocator_type& al=allocator_type()): super{n,fpr,al},hash_base{empty_init,h}{} diff --git a/test/test_capacity.cpp b/test/test_capacity.cpp index a83d457..2d8b41b 100644 --- a/test/test_capacity.cpp +++ b/test/test_capacity.cpp @@ -85,6 +85,14 @@ void test_capacity() BOOST_TEST_EQ(f.capacity(),0); BOOST_TEST(f==filter{}); } + { + filter f{{fac(),fac()},1000}; + num_allocations=0; + f.reset(0,1.0); + BOOST_TEST_EQ(num_allocations,0); + BOOST_TEST_EQ(f.capacity(),0); + BOOST_TEST(f==filter{}); + } { filter f{{fac(),fac()},1000}; std::size_t c=f.capacity(); @@ -94,6 +102,14 @@ void test_capacity() BOOST_TEST_GE(f.capacity(),c+1); BOOST_TEST(f==filter{f.capacity()}); } + { + filter f; + std::size_t c=filter::capacity_for(100,0.1); + num_allocations=0; + f.reset(100,0.1); + BOOST_TEST_EQ(num_allocations,1); + BOOST_TEST_EQ(f.capacity(),c); + } { filter f1{{fac(),fac()},1000},f2; std::size_t c=f1.capacity(); diff --git a/test/test_fpr.cpp b/test/test_fpr.cpp index 2a6d044..fff19bb 100644 --- a/test/test_fpr.cpp +++ b/test/test_fpr.cpp @@ -64,7 +64,9 @@ void test_fpr() boost::hash >; - BOOST_TEST_EQ(filter(0,0.01).capacity(),0); + BOOST_TEST_GT(filter(0,0.0).capacity(),0); + BOOST_TEST_GT(filter(0,0.5).capacity(),0); + BOOST_TEST_EQ(filter(0,1.0).capacity(),0); BOOST_TEST_THROWS((void)filter(1,0.0),std::bad_alloc); BOOST_TEST_EQ(filter(100,1.0).capacity(),0); @@ -82,7 +84,7 @@ void test_fpr() } BOOST_TEST_EQ(filter::fpr_for(0,1),0.0); - BOOST_TEST_EQ(filter::fpr_for(0,0),0.0); + BOOST_TEST_EQ(filter::fpr_for(0,0),1.0); BOOST_TEST_EQ(filter::fpr_for(1,0),1.0); {