Bitcoin Core  27.99.0
P2P Digital Currency
cuckoocache_tests.cpp
Go to the documentation of this file.
1 // Copyright (c) 2012-2021 The Bitcoin Core developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4 
5 #include <cuckoocache.h>
6 #include <random.h>
7 #include <script/sigcache.h>
8 #include <test/util/random.h>
10 
11 #include <boost/test/unit_test.hpp>
12 
13 #include <deque>
14 #include <mutex>
15 #include <shared_mutex>
16 #include <thread>
17 #include <vector>
18 
32 BOOST_AUTO_TEST_SUITE(cuckoocache_tests);
33 
34 /* Test that no values not inserted into the cache are read out of it.
35  *
36  * There are no repeats in the first 200000 insecure_GetRandHash calls
37  */
38 BOOST_AUTO_TEST_CASE(test_cuckoocache_no_fakes)
39 {
42  size_t megabytes = 4;
43  cc.setup_bytes(megabytes << 20);
44  for (int x = 0; x < 100000; ++x) {
45  cc.insert(InsecureRand256());
46  }
47  for (int x = 0; x < 100000; ++x) {
48  BOOST_CHECK(!cc.contains(InsecureRand256(), false));
49  }
50 };
51 
55 template <typename Cache>
56 static double test_cache(size_t megabytes, double load)
57 {
59  std::vector<uint256> hashes;
60  Cache set{};
61  size_t bytes = megabytes * (1 << 20);
62  set.setup_bytes(bytes);
63  uint32_t n_insert = static_cast<uint32_t>(load * (bytes / sizeof(uint256)));
64  hashes.resize(n_insert);
65  for (uint32_t i = 0; i < n_insert; ++i) {
66  uint32_t* ptr = (uint32_t*)hashes[i].begin();
67  for (uint8_t j = 0; j < 8; ++j)
68  *(ptr++) = InsecureRand32();
69  }
74  std::vector<uint256> hashes_insert_copy = hashes;
76  for (const uint256& h : hashes_insert_copy)
77  set.insert(h);
79  uint32_t count = 0;
80  for (const uint256& h : hashes)
81  count += set.contains(h, false);
82  double hit_rate = ((double)count) / ((double)n_insert);
83  return hit_rate;
84 }
85 
103 static double normalize_hit_rate(double hits, double load)
104 {
105  return hits * std::max(load, 1.0);
106 }
107 
109 BOOST_AUTO_TEST_CASE(cuckoocache_hit_rate_ok)
110 {
114  double HitRateThresh = 0.98;
115  size_t megabytes = 4;
116  for (double load = 0.1; load < 2; load *= 2) {
117  double hits = test_cache<CuckooCache::cache<uint256, SignatureCacheHasher>>(megabytes, load);
118  BOOST_CHECK(normalize_hit_rate(hits, load) > HitRateThresh);
119  }
120 }
121 
122 
125 template <typename Cache>
126 static void test_cache_erase(size_t megabytes)
127 {
128  double load = 1;
130  std::vector<uint256> hashes;
131  Cache set{};
132  size_t bytes = megabytes * (1 << 20);
133  set.setup_bytes(bytes);
134  uint32_t n_insert = static_cast<uint32_t>(load * (bytes / sizeof(uint256)));
135  hashes.resize(n_insert);
136  for (uint32_t i = 0; i < n_insert; ++i) {
137  uint32_t* ptr = (uint32_t*)hashes[i].begin();
138  for (uint8_t j = 0; j < 8; ++j)
139  *(ptr++) = InsecureRand32();
140  }
145  std::vector<uint256> hashes_insert_copy = hashes;
146 
148  for (uint32_t i = 0; i < (n_insert / 2); ++i)
149  set.insert(hashes_insert_copy[i]);
151  for (uint32_t i = 0; i < (n_insert / 4); ++i)
152  BOOST_CHECK(set.contains(hashes[i], true));
154  for (uint32_t i = (n_insert / 2); i < n_insert; ++i)
155  set.insert(hashes_insert_copy[i]);
156 
158  size_t count_erased_but_contained = 0;
160  size_t count_stale = 0;
162  size_t count_fresh = 0;
163 
164  for (uint32_t i = 0; i < (n_insert / 4); ++i)
165  count_erased_but_contained += set.contains(hashes[i], false);
166  for (uint32_t i = (n_insert / 4); i < (n_insert / 2); ++i)
167  count_stale += set.contains(hashes[i], false);
168  for (uint32_t i = (n_insert / 2); i < n_insert; ++i)
169  count_fresh += set.contains(hashes[i], false);
170 
171  double hit_rate_erased_but_contained = double(count_erased_but_contained) / (double(n_insert) / 4.0);
172  double hit_rate_stale = double(count_stale) / (double(n_insert) / 4.0);
173  double hit_rate_fresh = double(count_fresh) / (double(n_insert) / 2.0);
174 
175  // Check that our hit_rate_fresh is perfect
176  BOOST_CHECK_EQUAL(hit_rate_fresh, 1.0);
177  // Check that we have a more than 2x better hit rate on stale elements than
178  // erased elements.
179  BOOST_CHECK(hit_rate_stale > 2 * hit_rate_erased_but_contained);
180 }
181 
182 BOOST_AUTO_TEST_CASE(cuckoocache_erase_ok)
183 {
184  size_t megabytes = 4;
185  test_cache_erase<CuckooCache::cache<uint256, SignatureCacheHasher>>(megabytes);
186 }
187 
188 template <typename Cache>
189 static void test_cache_erase_parallel(size_t megabytes)
190 {
191  double load = 1;
193  std::vector<uint256> hashes;
194  Cache set{};
195  size_t bytes = megabytes * (1 << 20);
196  set.setup_bytes(bytes);
197  uint32_t n_insert = static_cast<uint32_t>(load * (bytes / sizeof(uint256)));
198  hashes.resize(n_insert);
199  for (uint32_t i = 0; i < n_insert; ++i) {
200  uint32_t* ptr = (uint32_t*)hashes[i].begin();
201  for (uint8_t j = 0; j < 8; ++j)
202  *(ptr++) = InsecureRand32();
203  }
208  std::vector<uint256> hashes_insert_copy = hashes;
209  std::shared_mutex mtx;
210 
211  {
213  std::unique_lock<std::shared_mutex> l(mtx);
215  for (uint32_t i = 0; i < (n_insert / 2); ++i)
216  set.insert(hashes_insert_copy[i]);
217  }
218 
221  std::vector<std::thread> threads;
223  for (uint32_t x = 0; x < 3; ++x)
226  threads.emplace_back([&, x] {
227  std::shared_lock<std::shared_mutex> l(mtx);
228  size_t ntodo = (n_insert/4)/3;
229  size_t start = ntodo*x;
230  size_t end = ntodo*(x+1);
231  for (uint32_t i = start; i < end; ++i) {
232  bool contains = set.contains(hashes[i], true);
233  assert(contains);
234  }
235  });
236 
239  for (std::thread& t : threads)
240  t.join();
242  std::unique_lock<std::shared_mutex> l(mtx);
244  for (uint32_t i = (n_insert / 2); i < n_insert; ++i)
245  set.insert(hashes_insert_copy[i]);
246 
248  size_t count_erased_but_contained = 0;
250  size_t count_stale = 0;
252  size_t count_fresh = 0;
253 
254  for (uint32_t i = 0; i < (n_insert / 4); ++i)
255  count_erased_but_contained += set.contains(hashes[i], false);
256  for (uint32_t i = (n_insert / 4); i < (n_insert / 2); ++i)
257  count_stale += set.contains(hashes[i], false);
258  for (uint32_t i = (n_insert / 2); i < n_insert; ++i)
259  count_fresh += set.contains(hashes[i], false);
260 
261  double hit_rate_erased_but_contained = double(count_erased_but_contained) / (double(n_insert) / 4.0);
262  double hit_rate_stale = double(count_stale) / (double(n_insert) / 4.0);
263  double hit_rate_fresh = double(count_fresh) / (double(n_insert) / 2.0);
264 
265  // Check that our hit_rate_fresh is perfect
266  BOOST_CHECK_EQUAL(hit_rate_fresh, 1.0);
267  // Check that we have a more than 2x better hit rate on stale elements than
268  // erased elements.
269  BOOST_CHECK(hit_rate_stale > 2 * hit_rate_erased_but_contained);
270 }
271 BOOST_AUTO_TEST_CASE(cuckoocache_erase_parallel_ok)
272 {
273  size_t megabytes = 4;
274  test_cache_erase_parallel<CuckooCache::cache<uint256, SignatureCacheHasher>>(megabytes);
275 }
276 
277 
278 template <typename Cache>
280 {
281  // This test checks that for a simulation of network activity, the fresh hit
282  // rate is never below 99%, and the number of times that it is worse than
283  // 99.9% are less than 1% of the time.
284  double min_hit_rate = 0.99;
285  double tight_hit_rate = 0.999;
286  double max_rate_less_than_tight_hit_rate = 0.01;
287  // A cache that meets this specification is therefore shown to have a hit
288  // rate of at least tight_hit_rate * (1 - max_rate_less_than_tight_hit_rate) +
289  // min_hit_rate*max_rate_less_than_tight_hit_rate = 0.999*99%+0.99*1% == 99.89%
290  // hit rate with low variance.
291 
292  // We use deterministic values, but this test has also passed on many
293  // iterations with non-deterministic values, so it isn't "overfit" to the
294  // specific entropy in FastRandomContext(true) and implementation of the
295  // cache.
297 
298  // block_activity models a chunk of network activity. n_insert elements are
299  // added to the cache. The first and last n/4 are stored for removal later
300  // and the middle n/2 are not stored. This models a network which uses half
301  // the signatures of recently (since the last block) added transactions
302  // immediately and never uses the other half.
303  struct block_activity {
304  std::vector<uint256> reads;
305  block_activity(uint32_t n_insert, Cache& c) : reads()
306  {
307  std::vector<uint256> inserts;
308  inserts.resize(n_insert);
309  reads.reserve(n_insert / 2);
310  for (uint32_t i = 0; i < n_insert; ++i) {
311  uint32_t* ptr = (uint32_t*)inserts[i].begin();
312  for (uint8_t j = 0; j < 8; ++j)
313  *(ptr++) = InsecureRand32();
314  }
315  for (uint32_t i = 0; i < n_insert / 4; ++i)
316  reads.push_back(inserts[i]);
317  for (uint32_t i = n_insert - (n_insert / 4); i < n_insert; ++i)
318  reads.push_back(inserts[i]);
319  for (const auto& h : inserts)
320  c.insert(h);
321  }
322  };
323 
324  const uint32_t BLOCK_SIZE = 1000;
325  // We expect window size 60 to perform reasonably given that each epoch
326  // stores 45% of the cache size (~472k).
327  const uint32_t WINDOW_SIZE = 60;
328  const uint32_t POP_AMOUNT = (BLOCK_SIZE / WINDOW_SIZE) / 2;
329  const double load = 10;
330  const size_t megabytes = 4;
331  const size_t bytes = megabytes * (1 << 20);
332  const uint32_t n_insert = static_cast<uint32_t>(load * (bytes / sizeof(uint256)));
333 
334  std::vector<block_activity> hashes;
335  Cache set{};
336  set.setup_bytes(bytes);
337  hashes.reserve(n_insert / BLOCK_SIZE);
338  std::deque<block_activity> last_few;
339  uint32_t out_of_tight_tolerance = 0;
340  uint32_t total = n_insert / BLOCK_SIZE;
341  // we use the deque last_few to model a sliding window of blocks. at each
342  // step, each of the last WINDOW_SIZE block_activities checks the cache for
343  // POP_AMOUNT of the hashes that they inserted, and marks these erased.
344  for (uint32_t i = 0; i < total; ++i) {
345  if (last_few.size() == WINDOW_SIZE)
346  last_few.pop_front();
347  last_few.emplace_back(BLOCK_SIZE, set);
348  uint32_t count = 0;
349  for (auto& act : last_few)
350  for (uint32_t k = 0; k < POP_AMOUNT; ++k) {
351  count += set.contains(act.reads.back(), true);
352  act.reads.pop_back();
353  }
354  // We use last_few.size() rather than WINDOW_SIZE for the correct
355  // behavior on the first WINDOW_SIZE iterations where the deque is not
356  // full yet.
357  double hit = (double(count)) / (last_few.size() * POP_AMOUNT);
358  // Loose Check that hit rate is above min_hit_rate
359  BOOST_CHECK(hit > min_hit_rate);
360  // Tighter check, count number of times we are less than tight_hit_rate
361  // (and implicitly, greater than min_hit_rate)
362  out_of_tight_tolerance += hit < tight_hit_rate;
363  }
364  // Check that being out of tolerance happens less than
365  // max_rate_less_than_tight_hit_rate of the time
366  BOOST_CHECK(double(out_of_tight_tolerance) / double(total) < max_rate_less_than_tight_hit_rate);
367 }
368 BOOST_AUTO_TEST_CASE(cuckoocache_generations)
369 {
370  test_cache_generations<CuckooCache::cache<uint256, SignatureCacheHasher>>();
371 }
372 
cache implements a cache with properties similar to a cuckoo-set.
Definition: cuckoocache.h:163
std::optional< std::pair< uint32_t, size_t > > setup_bytes(size_t bytes)
setup_bytes is a convenience function which accounts for internal memory usage when deciding how many...
Definition: cuckoocache.h:365
256-bit opaque blob.
Definition: uint256.h:106
BOOST_AUTO_TEST_CASE(test_cuckoocache_no_fakes)
static void test_cache_erase_parallel(size_t megabytes)
static void test_cache_erase(size_t megabytes)
This helper checks that erased elements are preferentially inserted onto and that the hit rate of "fr...
static void test_cache_generations()
BOOST_AUTO_TEST_SUITE(cuckoocache_tests)
Test Suite for CuckooCache.
BOOST_AUTO_TEST_SUITE_END()
static double test_cache(size_t megabytes, double load)
This helper returns the hit rate when megabytes*load worth of entries are inserted into a megabytes s...
static double normalize_hit_rate(double hits, double load)
The normalized hit rate for a given load.
#define BOOST_CHECK_EQUAL(v1, v2)
Definition: object.cpp:18
#define BOOST_CHECK(expr)
Definition: object.cpp:17
@ ZEROS
Seed with a compile time constant of zeros.
static uint256 InsecureRand256()
Definition: random.h:50
static void SeedInsecureRand(SeedRand seed=SeedRand::SEED)
Definition: random.h:36
static uint32_t InsecureRand32()
Definition: random.h:45
static int count
assert(!tx.IsCoinBase())