GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/smallhash.h
Date: 2026-06-28 02:36:10
Exec Total Coverage
Lines: 260 263 98.9%
Branches: 87 110 79.1%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System.
3 */
4
5 #ifndef CVMFS_SMALLHASH_H_
6 #define CVMFS_SMALLHASH_H_
7
8 #include "duplex_testing.h"
9 #include <inttypes.h>
10 #include <pthread.h>
11 #include <stdint.h>
12
13 #include <algorithm>
14 #include <cassert>
15 #include <cstdlib>
16 #include <new>
17
18 #include "util/atomic.h"
19 #include "util/murmur.hxx"
20 #include "util/prng.h"
21 #include "util/smalloc.h"
22
23 /**
24 * Hash table with linear probing as collision resolution. Works only for
25 * a fixed (maximum) number of elements, i.e. no resizing. Load factor fixed
26 * to 0.7.
27 */
28 template<class Key, class Value, class Derived>
29 class SmallHashBase {
30 FRIEND_TEST(T_Smallhash, InsertAndCopyMd5Slow);
31
32 public:
33 static const double kLoadFactor; // mainly useless for the dynamic version
34 static const double kThresholdGrow; // only used for resizable version
35 static const double kThresholdShrink; // only used for resizable version
36
37 145527 SmallHashBase() {
38 145527 keys_ = NULL;
39 145527 values_ = NULL;
40 145527 hasher_ = NULL;
41 145527 bytes_allocated_ = 0;
42 145527 num_collisions_ = 0;
43 145527 max_collisions_ = 0;
44
45 // Properly initialized by Init()
46 145527 capacity_ = 0;
47 145527 initial_capacity_ = 0;
48 145527 size_ = 0;
49 145527 }
50
51 145389 ~SmallHashBase() { DeallocMemory(keys_, values_, capacity_); }
52
53 145457 void Init(uint32_t expected_size, Key empty,
54 uint32_t (*hasher)(const Key &key)) {
55 145457 hasher_ = hasher;
56 145457 empty_key_ = empty;
57 145457 capacity_ = static_cast<uint32_t>(static_cast<double>(expected_size)
58 145457 / kLoadFactor);
59 145457 initial_capacity_ = capacity_;
60 145457 static_cast<Derived *>(this)->SetThresholds(); // No-op for fixed size
61 145457 AllocMemory();
62 145457 this->DoClear(false);
63 145457 }
64
65 73350267 bool Lookup(const Key &key, Value *value) const {
66 uint32_t bucket;
67 uint32_t collisions;
68
1/2
✓ Branch 1 taken 73316060 times.
✗ Branch 2 not taken.
73350267 const bool found = DoLookup(key, &bucket, &collisions);
69
2/2
✓ Branch 0 taken 72636894 times.
✓ Branch 1 taken 679166 times.
73352957 if (found)
70
1/2
✓ Branch 1 taken 71400 times.
✗ Branch 2 not taken.
72649344 *value = values_[bucket];
71 73352957 return found;
72 }
73
74 /**
75 * Returns both the key and the value. That is useful if Key's equality
76 * operator implements an equivalence relation on Key. In this case, LookupEx
77 * returns the key representing the equivalence class that has been used
78 * during Insert().
79 * Used to return a glue::InodeEx element when looking for an inode.
80 */
81 500 bool LookupEx(Key *key, Value *value) const {
82 500 uint32_t bucket = ScaleHash(*key);
83
2/2
✓ Branch 1 taken 166 times.
✓ Branch 2 taken 84 times.
500 while (!(keys_[bucket] == empty_key_)) {
84
1/2
✓ Branch 1 taken 166 times.
✗ Branch 2 not taken.
332 if (keys_[bucket] == *key) {
85 332 *key = keys_[bucket];
86 332 *value = values_[bucket];
87 332 return true;
88 }
89 bucket = (bucket + 1) % capacity_;
90 }
91 168 return false;
92 }
93
94 44020028 bool Contains(const Key &key) const {
95 uint32_t bucket;
96 uint32_t collisions;
97
1/2
✓ Branch 1 taken 44019243 times.
✗ Branch 2 not taken.
44020028 const bool found = DoLookup(key, &bucket, &collisions);
98 44020028 return found;
99 }
100
101 2031779877 void Insert(const Key &key, const Value &value) {
102 2031779877 static_cast<Derived *>(this)->Grow(); // No-op if fixed-size
103 2024234275 const bool overwritten = DoInsert(key, value, true);
104 2024117356 size_ += !overwritten; // size + 1 if the key was not yet in the map
105 2024117356 }
106
107 97154168 bool Erase(const Key &key) {
108 uint32_t bucket;
109 uint32_t collisions;
110
1/2
✓ Branch 1 taken 94236725 times.
✗ Branch 2 not taken.
97154168 const bool found = DoLookup(key, &bucket, &collisions);
111
1/2
✓ Branch 0 taken 94250156 times.
✗ Branch 1 not taken.
94240276 if (found) {
112 94253352 keys_[bucket] = empty_key_;
113 94253352 size_--;
114 94253352 bucket = (bucket + 1) % capacity_;
115
3/3
✓ Branch 0 taken 200127314 times.
✓ Branch 1 taken 93159255 times.
✓ Branch 2 taken 377030 times.
293666795 while (!(keys_[bucket] == empty_key_)) {
116 200335623 Key rehash = keys_[bucket];
117 200335623 keys_[bucket] = empty_key_;
118
1/2
✓ Branch 1 taken 199413443 times.
✗ Branch 2 not taken.
200335623 DoInsert(rehash, values_[bucket], false);
119 199413443 bucket = (bucket + 1) % capacity_;
120 }
121
1/2
✓ Branch 1 taken 94953188 times.
✗ Branch 2 not taken.
93331172 static_cast<Derived *>(this)->Shrink(); // No-op if fixed-size
122 }
123 94980876 return found;
124 }
125
126 2372 void Clear() { DoClear(true); }
127
128 3596 uint64_t bytes_allocated() const { return bytes_allocated_; }
129 393 static double GetEntrySize() {
130 786 const double unit = sizeof(Key) + sizeof(Value);
131 786 return unit / kLoadFactor;
132 }
133
134 void GetCollisionStats(uint64_t *num_collisions,
135 uint32_t *max_collisions) const {
136 *num_collisions = num_collisions_;
137 *max_collisions = max_collisions_;
138 }
139
140 // Careful with the direct access TODO: iterator
141 uint32_t capacity() const { return capacity_; }
142 50318 Key empty_key() const { return empty_key_; }
143 156544 Key *keys() const { return keys_; }
144 687 Value *values() const { return values_; }
145
146 // Only needed by compat
147 void SetHasher(uint32_t (*hasher)(const Key &key)) { hasher_ = hasher; }
148
149 protected:
150 2784041850 uint32_t ScaleHash(const Key &key) const {
151 2784041850 const double bucket = (static_cast<double>(hasher_(key))
152 2784613986 * static_cast<double>(capacity_)
153 / static_cast<double>(static_cast<uint32_t>(-1)));
154 2784613986 return static_cast<uint32_t>(bucket) % capacity_;
155 }
156
157 301526 void AllocMemory() {
158 301526 keys_ = static_cast<Key *>(smmap(capacity_ * sizeof(Key)));
159 301526 values_ = static_cast<Value *>(smmap(capacity_ * sizeof(Value)));
160
2/2
✓ Branch 0 taken 2021887578 times.
✓ Branch 1 taken 152646 times.
4040260548 for (uint32_t i = 0; i < capacity_; ++i) {
161 4039959022 /*keys_[i] =*/new (keys_ + i) Key();
162 }
163
2/2
✓ Branch 0 taken 2028055348 times.
✓ Branch 1 taken 84501 times.
4052459798 for (uint32_t i = 0; i < capacity_; ++i) {
164 4052294562 /*values_[i] =*/new (values_ + i) Value();
165 }
166 165236 bytes_allocated_ = (sizeof(Key) + sizeof(Value)) * capacity_;
167 165236 }
168
169 301318 void DeallocMemory(Key *k, Value *v, uint32_t c) {
170
2/2
✓ Branch 0 taken 2032355070 times.
✓ Branch 1 taken 152541 times.
4061198054 for (uint32_t i = 0; i < c; ++i) {
171 4060896736 k[i].~Key();
172 }
173
2/2
✓ Branch 0 taken 2032301835 times.
✓ Branch 1 taken 152541 times.
4061091584 for (uint32_t i = 0; i < c; ++i) {
174 4060790266 v[i].~Value();
175 }
176
1/2
✓ Branch 0 taken 152576 times.
✗ Branch 1 not taken.
301318 if (k)
177 301388 smunmap(k);
178
1/2
✓ Branch 0 taken 152576 times.
✗ Branch 1 not taken.
301318 if (v)
179 301388 smunmap(v);
180 301318 k = NULL;
181 301318 v = NULL;
182 301318 }
183
184 // Returns true iff the key is overwritten
185 2388148222 bool DoInsert(const Key &key, const Value &value,
186 const bool count_collisions) {
187 uint32_t bucket;
188 uint32_t collisions;
189
1/2
✓ Branch 1 taken 1213785614 times.
✗ Branch 2 not taken.
2388148222 const bool overwritten = DoLookup(key, &bucket, &collisions);
190
2/2
✓ Branch 0 taken 1014235130 times.
✓ Branch 1 taken 199550484 times.
2390994550 if (count_collisions) {
191 2026287750 num_collisions_ += collisions;
192 2026287750 max_collisions_ = std::max(collisions, max_collisions_);
193 }
194 2388939070 keys_[bucket] = key;
195
1/2
✓ Branch 1 taken 34372210 times.
✗ Branch 2 not taken.
2388939070 values_[bucket] = value;
196 2388939070 return overwritten;
197 }
198
199 2785140542 bool DoLookup(const Key &key, uint32_t *bucket, uint32_t *collisions) const {
200 2785140542 *bucket = ScaleHash(key);
201 2783206031 *collisions = 0;
202
3/3
✓ Branch 0 taken 19102819006 times.
✓ Branch 1 taken 1190863632 times.
✓ Branch 2 taken 259058803 times.
23273463467 while (!(keys_[*bucket] == empty_key_)) {
203
3/3
✓ Branch 0 taken 166126385 times.
✓ Branch 1 taken 18983444000 times.
✓ Branch 2 taken 204252891 times.
20911313117 if (keys_[*bucket] == key)
204 421055681 return true;
205 20490257436 *bucket = (*bucket + 1) % capacity_;
206 20490257436 (*collisions)++;
207 }
208 2362150350 return false;
209 }
210
211 301526 void DoClear(const bool reset_capacity) {
212
2/2
✓ Branch 0 taken 2372 times.
✓ Branch 1 taken 150309 times.
301526 if (reset_capacity)
213 3926 static_cast<Derived *>(this)->ResetCapacity(); // No-op if fixed-size
214
2/2
✓ Branch 0 taken 2029003603 times.
✓ Branch 1 taken 152716 times.
4054397118 for (uint32_t i = 0; i < capacity_; ++i)
215 4054095522 keys_[i] = empty_key_;
216 301596 size_ = 0;
217 301596 }
218
219 // Methods for resizable version
220 void SetThresholds() { }
221 void Grow() { }
222 void Shrink() { }
223 void ResetCapacity() { }
224
225 // Separate key and value arrays for better locality
226 Key *keys_;
227 Value *values_;
228 uint32_t capacity_;
229 uint32_t initial_capacity_;
230 uint32_t size_;
231 uint32_t (*hasher_)(const Key &key);
232 uint64_t bytes_allocated_;
233 uint64_t num_collisions_;
234 uint32_t max_collisions_; /**< maximum collisions for a single insert */
235 Key empty_key_;
236 };
237
238
239 template<class Key, class Value>
240 class SmallHashFixed
241 : public SmallHashBase<Key, Value, SmallHashFixed<Key, Value> > {
242 friend class SmallHashBase<Key, Value, SmallHashFixed<Key, Value> >;
243
244 protected:
245 // No-ops
246 3526 void SetThresholds() { }
247 915830 void Grow() { }
248 37568 void Shrink() { }
249 70 void ResetCapacity() { }
250 };
251
252
253 template<class Key, class Value>
254 class SmallHashDynamic
255 : public SmallHashBase<Key, Value, SmallHashDynamic<Key, Value> > {
256 friend class SmallHashBase<Key, Value, SmallHashDynamic<Key, Value> >;
257
258 public:
259 typedef SmallHashBase<Key, Value, SmallHashDynamic<Key, Value> > Base;
260 static const double kThresholdGrow;
261 static const double kThresholdShrink;
262
263 142001 SmallHashDynamic() : Base() {
264 142001 num_migrates_ = 0;
265
266 // Properly set by Init
267 142001 threshold_grow_ = 0;
268 142001 threshold_shrink_ = 0;
269 142001 }
270
271 SmallHashDynamic(const SmallHashDynamic<Key, Value> &other) : Base() {
272 num_migrates_ = 0;
273 CopyFrom(other);
274 }
275
276 387 SmallHashDynamic<Key, Value> &operator=(
277 const SmallHashDynamic<Key, Value> &other) {
278
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 211 times.
387 if (&other == this)
279 return *this;
280
281 387 CopyFrom(other);
282 387 return *this;
283 }
284
285 3106914 uint32_t capacity() const { return Base::capacity_; }
286 1108667727 uint32_t size() const { return Base::size_; }
287 uint32_t num_migrates() const { return num_migrates_; }
288
289 protected:
290 298000 void SetThresholds() {
291 298000 threshold_grow_ = static_cast<uint32_t>(static_cast<double>(capacity())
292 298000 * kThresholdGrow);
293 298000 threshold_shrink_ = static_cast<uint32_t>(static_cast<double>(capacity())
294 298000 * kThresholdShrink);
295 298000 }
296
297 2028458721 void Grow() {
298
2/2
✓ Branch 1 taken 51971 times.
✓ Branch 2 taken 1013041000 times.
2028458721 if (size() > threshold_grow_)
299 103942 Migrate(capacity() * 2);
300 2024919497 }
301
302 94658995 void Shrink() {
303
2/2
✓ Branch 1 taken 1026317 times.
✓ Branch 2 taken 93783852 times.
94658995 if (size() < threshold_shrink_) {
304 1029513 const uint32_t target_capacity = capacity() / 2;
305
2/2
✓ Branch 0 taken 24080 times.
✓ Branch 1 taken 1002005 times.
1029281 if (target_capacity >= Base::initial_capacity_)
306 24080 Migrate(target_capacity);
307 }
308 94813133 }
309
310 3856 void ResetCapacity() {
311 3856 Base::DeallocMemory(Base::keys_, Base::values_, Base::capacity_);
312 3856 Base::capacity_ = Base::initial_capacity_;
313 3856 Base::AllocMemory();
314 3856 SetThresholds();
315 3856 }
316
317 private:
318 // Returns a random permutation of indices [0..N-1] that is allocated
319 // by smmap (Knuth's shuffle algorithm)
320 48582 uint32_t *ShuffleIndices(const uint32_t N) {
321 48582 uint32_t *shuffled = static_cast<uint32_t *>(smmap(N * sizeof(uint32_t)));
322 // Init with identity
323
2/2
✓ Branch 0 taken 392055146 times.
✓ Branch 1 taken 24291 times.
784158874 for (unsigned i = 0; i < N; ++i)
324 784110292 shuffled[i] = i;
325 // Shuffle (no shuffling for the last element)
326
1/2
✓ Branch 0 taken 375319440 times.
✗ Branch 1 not taken.
750534162 for (unsigned i = 0; i < N - 1; ++i) {
327 750638880 const uint32_t swap_idx = i + g_prng.Next(N - i);
328 750485580 const uint32_t tmp = shuffled[i];
329 750485580 shuffled[i] = shuffled[swap_idx];
330 750485580 shuffled[swap_idx] = tmp;
331 }
332 387 return shuffled;
333 }
334
335 152102 void Migrate(const uint32_t new_capacity) {
336 152102 Key *old_keys = Base::keys_;
337 152102 Value *old_values = Base::values_;
338 152102 const uint32_t old_capacity = capacity();
339 152102 const uint32_t old_size = size();
340
341 152172 Base::capacity_ = new_capacity;
342 152172 SetThresholds();
343 152172 Base::AllocMemory();
344 152172 Base::DoClear(false);
345
2/2
✓ Branch 0 taken 24080 times.
✓ Branch 1 taken 52006 times.
152172 if (new_capacity < old_capacity) {
346 48160 uint32_t *shuffled_indices = ShuffleIndices(old_capacity);
347
2/2
✓ Branch 0 taken 338803290 times.
✓ Branch 1 taken 840 times.
677608260 for (uint32_t i = 0; i < old_capacity; ++i) {
348
2/3
✓ Branch 0 taken 85028020 times.
✓ Branch 1 taken 253775270 times.
✗ Branch 2 not taken.
677606580 if (old_keys[shuffled_indices[i]] != Base::empty_key_) {
349 170056040 Base::Insert(old_keys[shuffled_indices[i]],
350 170056040 old_values[shuffled_indices[i]]);
351 }
352 }
353 1680 smunmap(shuffled_indices);
354 } else {
355
2/2
✓ Branch 0 taken 634013212 times.
✓ Branch 1 taken 29501 times.
1268085426 for (uint32_t i = 0; i < old_capacity; ++i) {
356
3/3
✓ Branch 0 taken 366732258 times.
✓ Branch 1 taken 230953080 times.
✓ Branch 2 taken 36327874 times.
1268026424 if (old_keys[i] != Base::empty_key_)
357 951446920 Base::Insert(old_keys[i], old_values[i]);
358 }
359 }
360
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 76016 times.
107162 assert(size() == old_size);
361
362 152032 Base::DeallocMemory(old_keys, old_values, old_capacity);
363 152172 num_migrates_++;
364 152172 }
365
366 387 void CopyFrom(const SmallHashDynamic<Key, Value> &other) {
367 387 uint32_t *shuffled_indices = ShuffleIndices(other.capacity_);
368
2/2
✓ Branch 0 taken 48172656 times.
✓ Branch 1 taken 211 times.
48176739 for (uint32_t i = 0; i < other.capacity_; ++i) {
369
2/3
✗ Branch 0 not taken.
✓ Branch 1 taken 35001848 times.
✓ Branch 2 taken 13170808 times.
48176352 if (other.keys_[shuffled_indices[i]] != other.empty_key_) {
370 35000000 this->Insert(other.keys_[shuffled_indices[i]],
371 35000000 other.values_[shuffled_indices[i]]);
372 }
373 }
374 387 smunmap(shuffled_indices);
375 387 }
376
377 uint32_t num_migrates_;
378 uint32_t threshold_grow_;
379 uint32_t threshold_shrink_;
380 static Prng g_prng;
381 };
382
383
384 /**
385 * Distributes the key-value pairs over $n$ dynamic hash maps with individual
386 * mutexes. Hence low mutex contention, and benefits from multiple processors.
387 */
388 template<class Key, class Value>
389 class MultiHash {
390 public:
391 350 MultiHash() {
392 350 num_hashmaps_ = 0;
393 350 hashmaps_ = NULL;
394 350 locks_ = NULL;
395 350 }
396
397 350 void Init(const uint8_t num_hashmaps, const Key &empty_key,
398 uint32_t (*hasher)(const Key &key)) {
399
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 350 times.
350 assert(num_hashmaps > 0);
400 350 const uint8_t N = num_hashmaps;
401 350 num_hashmaps_ = N;
402
3/4
✓ Branch 2 taken 14700 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 14700 times.
✓ Branch 5 taken 350 times.
15050 hashmaps_ = new SmallHashDynamic<Key, Value>[N]();
403 350 locks_ = static_cast<pthread_mutex_t *>(
404 350 smalloc(N * sizeof(pthread_mutex_t)));
405
2/2
✓ Branch 0 taken 14700 times.
✓ Branch 1 taken 350 times.
15050 for (uint8_t i = 0; i < N; ++i) {
406 14700 int retval = pthread_mutex_init(&locks_[i], NULL);
407
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14700 times.
14700 assert(retval == 0);
408 14700 hashmaps_[i].Init(128, empty_key, hasher);
409 }
410 350 }
411
412 350 ~MultiHash() {
413
2/2
✓ Branch 0 taken 14700 times.
✓ Branch 1 taken 350 times.
15050 for (uint8_t i = 0; i < num_hashmaps_; ++i) {
414 14700 pthread_mutex_destroy(&locks_[i]);
415 }
416 350 free(locks_);
417
3/4
✓ Branch 0 taken 350 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14700 times.
✓ Branch 3 taken 350 times.
15050 delete[] hashmaps_;
418 350 }
419
420 35000000 bool Lookup(const Key &key, Value *value) {
421 35000000 uint8_t target = SelectHashmap(key);
422 35000000 Lock(target);
423 35000000 const bool result = hashmaps_[target].Lookup(key, value);
424 35000000 Unlock(target);
425 35000000 return result;
426 }
427
428 130621260 void Insert(const Key &key, const Value &value) {
429 130621260 uint8_t target = SelectHashmap(key);
430 127159970 Lock(target);
431 132243965 hashmaps_[target].Insert(key, value);
432 125813590 Unlock(target);
433 136046260 }
434
435 62942985 void Erase(const Key &key) {
436 62942985 uint8_t target = SelectHashmap(key);
437 60211515 Lock(target);
438 64414350 hashmaps_[target].Erase(key);
439 58925475 Unlock(target);
440 65300410 }
441
442 35 void Clear() {
443
2/2
✓ Branch 0 taken 1470 times.
✓ Branch 1 taken 35 times.
1505 for (uint8_t i = 0; i < num_hashmaps_; ++i)
444 1470 Lock(i);
445
2/2
✓ Branch 0 taken 1470 times.
✓ Branch 1 taken 35 times.
1505 for (uint8_t i = 0; i < num_hashmaps_; ++i)
446 1470 hashmaps_[i].Clear();
447
2/2
✓ Branch 0 taken 1470 times.
✓ Branch 1 taken 35 times.
1505 for (uint8_t i = 0; i < num_hashmaps_; ++i)
448 1470 Unlock(i);
449 35 }
450
451 350 uint8_t num_hashmaps() const { return num_hashmaps_; }
452
453 280 void GetSizes(uint32_t *sizes) {
454
2/2
✓ Branch 0 taken 11760 times.
✓ Branch 1 taken 280 times.
12040 for (uint8_t i = 0; i < num_hashmaps_; ++i) {
455 11760 Lock(i);
456 11760 sizes[i] = hashmaps_[i].size();
457 11760 Unlock(i);
458 }
459 280 }
460
461 void GetCollisionStats(uint64_t *num_collisions, uint32_t *max_collisions) {
462 for (uint8_t i = 0; i < num_hashmaps_; ++i) {
463 Lock(i);
464 hashmaps_[i].GetCollisionStats(&num_collisions[i], &max_collisions[i]);
465 Unlock(i);
466 }
467 }
468
469 private:
470 227137680 inline uint8_t SelectHashmap(const Key &key) {
471 227137680 uint32_t hash = MurmurHash2(&key, sizeof(key), 0x37);
472 223423795 double bucket = static_cast<double>(hash)
473 223423795 * static_cast<double>(num_hashmaps_)
474 / static_cast<double>(static_cast<uint32_t>(-1));
475 223423795 return static_cast<uint32_t>(bucket) % num_hashmaps_;
476 }
477
478 221566905 inline void Lock(const uint8_t target) {
479 221566905 int retval = pthread_mutex_lock(&locks_[target]);
480
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 239782585 times.
239782585 assert(retval == 0);
481 239782585 }
482
483 219776200 inline void Unlock(const uint8_t target) {
484 219776200 int retval = pthread_mutex_unlock(&locks_[target]);
485
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 240968035 times.
240968035 assert(retval == 0);
486 240968035 }
487
488 uint8_t num_hashmaps_;
489 SmallHashDynamic<Key, Value> *hashmaps_;
490 pthread_mutex_t *locks_;
491 };
492
493
494 // initialize the static fields
495 template<class Key, class Value>
496 Prng SmallHashDynamic<Key, Value>::g_prng;
497
498 template<class Key, class Value, class Derived>
499 const double SmallHashBase<Key, Value, Derived>::kLoadFactor = 0.75;
500
501 template<class Key, class Value>
502 const double SmallHashDynamic<Key, Value>::kThresholdGrow = 0.75;
503
504 template<class Key, class Value>
505 const double SmallHashDynamic<Key, Value>::kThresholdShrink = 0.25;
506
507 #endif // CVMFS_SMALLHASH_H_
508