Coverage Report

Created: 2026-08-14 08:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/rocksdb/cache/clock_cache.cc
Line
Count
Source
1
//  Copyright (c) 2011-present, Facebook, Inc.  All rights reserved.
2
//  This source code is licensed under both the GPLv2 (found in the
3
//  COPYING file in the root directory) and Apache 2.0 License
4
//  (found in the LICENSE.Apache file in the root directory).
5
//
6
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
7
// Use of this source code is governed by a BSD-style license that can be
8
// found in the LICENSE file. See the AUTHORS file for names of contributors.
9
10
#include "cache/clock_cache.h"
11
12
#include <algorithm>
13
#include <bitset>
14
#include <cassert>
15
#include <cinttypes>
16
#include <cstddef>
17
#include <cstdint>
18
#include <cstdio>
19
#include <exception>
20
#include <functional>
21
#include <numeric>
22
#include <string>
23
#include <thread>
24
#include <type_traits>
25
26
#include "cache/cache_key.h"
27
#include "cache/secondary_cache_adapter.h"
28
#include "logging/logging.h"
29
#include "port/likely.h"
30
#include "rocksdb/env.h"
31
#include "util/autovector.h"
32
#include "util/hash.h"
33
#include "util/math.h"
34
#include "util/random.h"
35
36
namespace ROCKSDB_NAMESPACE {
37
38
namespace clock_cache {
39
40
namespace {
41
using SlotMeta = ClockHandle::SlotMeta;
42
using AcquireCounter = SlotMeta::AcquireCounter;
43
using ReleaseCounter = SlotMeta::ReleaseCounter;
44
45
107k
inline uint32_t GetInitialCountdown(Cache::Priority priority) {
46
  // Set initial clock data from priority
47
  // TODO: configuration parameters for priority handling and clock cycle
48
  // count?
49
107k
  switch (priority) {
50
70.1k
    case Cache::Priority::HIGH:
51
70.1k
      return ClockHandle::kHighCountdown;
52
36.9k
    case Cache::Priority::LOW:
53
36.9k
      return ClockHandle::kLowCountdown;
54
0
    case Cache::Priority::BOTTOM:
55
0
      return ClockHandle::kBottomCountdown;
56
107k
  }
57
  // Switch should have been exhaustive.
58
107k
  assert(false);
59
  // For release build, fall back on something reasonable.
60
0
  return ClockHandle::kLowCountdown;
61
107k
}
62
63
17.3k
inline void MarkEmpty(ClockHandle& h) {
64
#ifndef NDEBUG
65
  // Mark slot as empty, with assertion
66
  auto old_meta = h.meta.Exchange({});
67
  assert(old_meta.IsUnderConstruction());
68
#else
69
  // Mark slot as empty
70
17.3k
  h.meta.Store({});
71
17.3k
#endif
72
17.3k
}
73
74
0
inline void FreeDataMarkEmpty(ClockHandle& h, MemoryAllocator* allocator) {
75
  // NOTE: in theory there's more room for parallelism if we copy the handle
76
  // data and delay actions like this until after marking the entry as empty,
77
  // but performance tests only show a regression by copying the few words
78
  // of data.
79
0
  h.FreeData(allocator);
80
81
0
  MarkEmpty(h);
82
0
}
83
84
// Called to undo the effect of referencing an entry for internal purposes,
85
// so it should not be marked as having been used.
86
12.3k
inline void Unref(const ClockHandle& h, uint32_t count = 1) {
87
  // Pretend we never took the reference
88
  // WART: there's a tiny chance we release last ref to invisible
89
  // entry here. If that happens, we let eviction take care of it.
90
12.3k
  SlotMeta old_meta;
91
12.3k
  h.meta.Apply(AcquireCounter::MinusTransformPromiseNoUnderflow(count),
92
12.3k
               &old_meta);
93
12.3k
  assert(old_meta.GetRefcount() != 0);
94
12.3k
  (void)old_meta;
95
12.3k
}
96
97
inline bool ClockUpdate(ClockHandle& h, BaseClockTable::EvictionData* data,
98
0
                        bool* purgeable = nullptr) {
99
0
  SlotMeta meta;
100
0
  if (purgeable) {
101
0
    assert(*purgeable == false);
102
    // In AutoHCC, our eviction process follows the chain structure, so we
103
    // should ensure that we see the latest state of each entry, at least for
104
    // assertion checking.
105
0
    meta = h.meta.Load();
106
0
  } else {
107
    // In FixedHCC, our eviction process is a simple iteration without regard
108
    // to probing order, displacements, etc., so it doesn't matter if we see
109
    // somewhat stale data.
110
0
    meta = h.meta.LoadRelaxed();
111
0
  }
112
113
0
  if (!meta.IsShareable()) {
114
    // Only clock update Shareable entries
115
0
    if (purgeable) {
116
0
      *purgeable = true;
117
      // AutoHCC only: make sure we only attempt to update non-empty slots
118
0
      assert(!meta.IsEmpty());
119
0
    }
120
0
    return false;
121
0
  }
122
0
  uint32_t acquire_count = meta.GetAcquireCounter();
123
0
  uint32_t release_count = meta.GetReleaseCounter();
124
0
  if (acquire_count != release_count) {
125
    // Only clock update entries with no outstanding refs
126
0
    data->seen_pinned_count++;
127
0
    return false;
128
0
  }
129
0
  if (meta.IsVisible() && acquire_count > 0) {
130
    // Decrement clock
131
0
    uint32_t new_count =
132
0
        std::min(acquire_count - 1, uint32_t{ClockHandle::kMaxCountdown} - 1);
133
    // Compare-exchange in the decremented clock info, but
134
    // not aggressively
135
0
    SlotMeta new_meta = meta;
136
0
    new_meta.SetReleaseCounter(new_count);
137
0
    new_meta.SetAcquireCounter(new_count);
138
0
    h.meta.CasStrongRelaxed(meta, new_meta);
139
0
    return false;
140
0
  }
141
  // Otherwise, remove entry (either unreferenced invisible or
142
  // unreferenced and expired visible).
143
0
  SlotMeta construction_meta;
144
0
  construction_meta.SetUnderConstruction();
145
0
  construction_meta.SetHit(meta.GetHit());
146
0
  if (h.meta.CasStrong(meta, construction_meta)) {
147
    // Took ownership.
148
0
    data->freed_charge += h.GetTotalCharge();
149
0
    data->freed_count += 1;
150
0
    return true;
151
0
  } else {
152
    // Compare-exchange failing probably
153
    // indicates the entry was used, so skip it in that case.
154
0
    return false;
155
0
  }
156
0
}
157
158
// If an entry doesn't receive clock updates but is repeatedly referenced &
159
// released, the acquire and release counters could overflow without some
160
// intervention. This is that intervention, which should be inexpensive
161
// because it only incurs a simple, very predictable check. (Applying a bit
162
// mask in addition to an increment to every Release likely would be
163
// relatively expensive, because it's an extra atomic update.)
164
//
165
// We do have to assume that we never have many millions of simultaneous
166
// references to a cache handle, because we cannot represent so many
167
// references with the difference in counters, masked to the number of
168
// counter bits. Similarly, we assume there aren't millions of threads
169
// holding transient references (which might be "undone" rather than
170
// released by the way).
171
//
172
// Consider these possible states for each counter:
173
// low: less than kMaxCountdown
174
// medium: kMaxCountdown to half way to overflow + kMaxCountdown
175
// high: half way to overflow + kMaxCountdown, or greater
176
//
177
// And these possible states for the combination of counters:
178
// acquire / release
179
// -------   -------
180
// low       low       - Normal / common, with caveats (see below)
181
// medium    low       - Can happen while holding some refs
182
// high      low       - Violates assumptions (too many refs)
183
// low       medium    - Violates assumptions (refs underflow, etc.)
184
// medium    medium    - Normal (very read heavy cache)
185
// high      medium    - Can happen while holding some refs
186
// low       high      - This function is supposed to prevent
187
// medium    high      - Violates assumptions (refs underflow, etc.)
188
// high      high      - Needs CorrectNearOverflow
189
//
190
// Basically, this function detects (high, high) state (inferred from
191
// release alone being high) and bumps it back down to (medium, medium)
192
// state with the same refcount and the same logical countdown counter
193
// (everything > kMaxCountdown is logically the same). Note that bumping
194
// down to (low, low) would modify the countdown counter, so is "reserved"
195
// in a sense.
196
//
197
// If near-overflow correction is triggered here, there's no guarantee
198
// that another thread hasn't freed the entry and replaced it with another.
199
// Therefore, it must be the case that the correction does not affect
200
// entries unless they are very old (many millions of acquire-release cycles).
201
// (Our bit manipulation is indeed idempotent and only affects entries in
202
// exceptional cases.) We assume a pre-empted thread will not stall that long.
203
// If it did, the state could be corrupted in the (unlikely) case that the top
204
// bit of the acquire counter is set but not the release counter, and thus
205
// we only clear the top bit of the acquire counter on resumption. It would
206
// then appear that there are too many refs and the entry would be permanently
207
// pinned (which is not terrible for an exceptionally rare occurrence), unless
208
// it is referenced enough (at least kMaxCountdown more times) for the release
209
// counter to reach "high" state again and bumped back to "medium." (This
210
// motivates only checking for release counter in high state, not both in high
211
// state.)
212
inline void CorrectNearOverflow(SlotMeta old_meta,
213
164k
                                BitFieldsAtomic<SlotMeta>& meta) {
214
  // We clear both top-most counter bits at the same time.
215
164k
  constexpr uint32_t kCounterTopBit = uint32_t{1}
216
164k
                                      << (SlotMeta::kCounterNumBits - 1);
217
  // The threshold for correcting "near overflow" is to ensure
218
  // (a) the value has a top bit set that can be cleared
219
  // (b) when we clear the top bit, the eviction state will be preserved
220
  //     (everything >= kMaxCountdown is treated equivalently)
221
  // As mentioned above, we only check the release count.
222
164k
  constexpr uint32_t kThreshold = kCounterTopBit + ClockHandle::kMaxCountdown;
223
224
164k
  if (UNLIKELY(old_meta.GetReleaseCounter() > kThreshold)) {
225
0
    auto clear_transform = AcquireCounter::AndTransform(kCounterTopBit - 1) +
226
0
                           ReleaseCounter::AndTransform(kCounterTopBit - 1);
227
0
    meta.ApplyRelaxed(clear_transform);
228
0
  }
229
164k
}
230
231
inline bool BeginSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
232
114k
                            uint32_t initial_countdown, bool* already_matches) {
233
114k
  assert(*already_matches == false);
234
  // Optimistically transition the slot from "empty" to
235
  // "under construction" (no effect on other states)
236
114k
  auto set_occupied = SlotMeta::OccupiedFlag::SetTransform();
237
114k
  SlotMeta old_meta;
238
114k
  h.meta.Apply(set_occupied, &old_meta);
239
240
114k
  if (old_meta.IsEmpty()) {
241
    // We've started inserting into an available slot, and taken
242
    // ownership.
243
107k
    return true;
244
107k
  } else if (!old_meta.IsVisible()) {
245
    // Slot not usable / touchable now
246
0
    return false;
247
0
  }
248
  // Existing, visible entry, which might be a match.
249
  // But first, we need to acquire a ref to read it. In fact, number of
250
  // refs for initial countdown, so that we boost the clock state if
251
  // this is a match.
252
7.31k
  auto add_acquire =
253
7.31k
      AcquireCounter::PlusTransformPromiseNoOverflow(initial_countdown);
254
7.31k
  h.meta.Apply(add_acquire, &old_meta);
255
  // Like Lookup
256
7.31k
  if (old_meta.IsVisible()) {
257
    // Acquired a read reference
258
7.31k
    if (h.hashed_key == proto.hashed_key) {
259
      // Match. Release in a way that boosts the clock state
260
0
      auto add_release =
261
0
          ReleaseCounter::PlusTransformPromiseNoOverflow(initial_countdown);
262
0
      h.meta.Apply(add_release, &old_meta);
263
      // Correct for possible (but rare) overflow
264
0
      CorrectNearOverflow(old_meta, h.meta);
265
      // Insert detached instead (only if return handle needed)
266
0
      *already_matches = true;
267
0
      return false;
268
7.31k
    } else {
269
      // Mismatch.
270
7.31k
      Unref(h, initial_countdown);
271
7.31k
    }
272
18.4E
  } else if (UNLIKELY(old_meta.IsInvisible())) {
273
    // Pretend we never took the reference
274
0
    Unref(h, initial_countdown);
275
18.4E
  } else {
276
    // For other states, incrementing the acquire counter has no effect
277
    // so we don't need to undo it.
278
    // Slot not usable / touchable now.
279
18.4E
  }
280
7.31k
  return false;
281
7.31k
}
282
283
inline void FinishSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
284
107k
                             uint32_t initial_countdown, bool keep_ref) {
285
  // Save data fields
286
107k
  ClockHandleBasicData* h_alias = &h;
287
107k
  *h_alias = proto;
288
289
  // Transition from "under construction" state to "visible" state
290
107k
  SlotMeta new_meta;
291
107k
  new_meta.SetVisible();
292
293
  // Maybe with an outstanding reference
294
107k
  new_meta.SetAcquireCounter(initial_countdown);
295
107k
  new_meta.SetReleaseCounter(initial_countdown - (keep_ref ? 1 : 0));
296
297
#ifndef NDEBUG
298
  // Save the state transition, with assertion
299
  auto old_meta = h.meta.Exchange(new_meta);
300
  assert(old_meta.IsUnderConstruction());
301
#else
302
  // Save the state transition
303
107k
  h.meta.Store(new_meta);
304
107k
#endif
305
107k
}
306
307
bool TryInsert(const ClockHandleBasicData& proto, ClockHandle& h,
308
               uint32_t initial_countdown, bool keep_ref,
309
114k
               bool* already_matches) {
310
114k
  bool b = BeginSlotInsert(proto, h, initial_countdown, already_matches);
311
114k
  if (b) {
312
107k
    FinishSlotInsert(proto, h, initial_countdown, keep_ref);
313
107k
  }
314
114k
  return b;
315
114k
}
316
317
// Func must be const HandleImpl& -> void callable
318
template <class HandleImpl, class Func>
319
void ConstApplyToEntriesRange(const Func& func, const HandleImpl* begin,
320
                              const HandleImpl* end,
321
28.2k
                              bool apply_if_will_be_deleted) {
322
56.5k
  for (const HandleImpl* h = begin; h < end; ++h) {
323
    // Note: to avoid using compare_exchange, we have to be extra careful.
324
28.2k
    SlotMeta old_meta = h->meta.LoadRelaxed();
325
    // Check if it's an entry visible to lookups
326
28.2k
    if (apply_if_will_be_deleted || old_meta.IsVisible()) {
327
516
      if (old_meta.IsShareable()) {
328
        // Increment acquire counter. Note: it's possible that the entry has
329
        // completely changed since we loaded old_meta, but incrementing acquire
330
        // count is always safe. (Similar to optimistic Lookup here.)
331
516
        auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
332
516
        h->meta.Apply(add_acquire, &old_meta);
333
        // Check whether we actually acquired a reference.
334
516
        if (old_meta.IsShareable()) {
335
          // Apply func if appropriate
336
516
          if (apply_if_will_be_deleted || old_meta.IsVisible()) {
337
516
            func(*h);
338
516
          }
339
          // Pretend we never took the reference
340
516
          Unref(*h);
341
          // No net change, so don't need to check for overflow
342
516
        } else {
343
          // For other states, incrementing the acquire counter has no effect
344
          // so we don't need to undo it. Furthermore, we cannot safely undo
345
          // it because we did not acquire a read reference to lock the
346
          // entry in a Shareable state.
347
0
        }
348
516
      }
349
516
    }
350
28.2k
  }
351
28.2k
}
Unexecuted instantiation: clock_cache.cc:void rocksdb::clock_cache::(anonymous namespace)::ConstApplyToEntriesRange<rocksdb::clock_cache::FixedHyperClockTable::HandleImpl, rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::GetPinnedUsage() const::{lambda(rocksdb::clock_cache::FixedHyperClockTable::HandleImpl const&)#1}>(rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::GetPinnedUsage() const::{lambda(rocksdb::clock_cache::FixedHyperClockTable::HandleImpl const&)#1} const&, rocksdb::clock_cache::FixedHyperClockTable::HandleImpl const*, rocksdb::clock_cache::FixedHyperClockTable::HandleImpl const, bool)
Unexecuted instantiation: clock_cache.cc:void rocksdb::clock_cache::(anonymous namespace)::ConstApplyToEntriesRange<rocksdb::clock_cache::FixedHyperClockTable::HandleImpl, rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::ApplyToSomeEntries(std::__1::function<void (rocksdb::Slice const&, void*, unsigned long, rocksdb::Cache::CacheItemHelper const*)> const&, unsigned long, unsigned long*)::{lambda(rocksdb::clock_cache::FixedHyperClockTable::HandleImpl const&)#1}>(rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::ApplyToSomeEntries(std::__1::function<void (rocksdb::Slice const&, void*, unsigned long, rocksdb::Cache::CacheItemHelper const*)> const&, unsigned long, unsigned long*)::{lambda(rocksdb::clock_cache::FixedHyperClockTable::HandleImpl const&)#1} const&, rocksdb::clock_cache::FixedHyperClockTable::HandleImpl const*, rocksdb::clock_cache::FixedHyperClockTable::HandleImpl const, bool)
Unexecuted instantiation: clock_cache.cc:void rocksdb::clock_cache::(anonymous namespace)::ConstApplyToEntriesRange<rocksdb::clock_cache::AutoHyperClockTable::HandleImpl, rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::GetPinnedUsage() const::{lambda(rocksdb::clock_cache::AutoHyperClockTable::HandleImpl const&)#1}>(rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::GetPinnedUsage() const::{lambda(rocksdb::clock_cache::AutoHyperClockTable::HandleImpl const&)#1} const&, rocksdb::clock_cache::AutoHyperClockTable::HandleImpl const*, rocksdb::clock_cache::AutoHyperClockTable::HandleImpl const, bool)
clock_cache.cc:void rocksdb::clock_cache::(anonymous namespace)::ConstApplyToEntriesRange<rocksdb::clock_cache::AutoHyperClockTable::HandleImpl, rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::ApplyToSomeEntries(std::__1::function<void (rocksdb::Slice const&, void*, unsigned long, rocksdb::Cache::CacheItemHelper const*)> const&, unsigned long, unsigned long*)::{lambda(rocksdb::clock_cache::AutoHyperClockTable::HandleImpl const&)#1}>(rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::ApplyToSomeEntries(std::__1::function<void (rocksdb::Slice const&, void*, unsigned long, rocksdb::Cache::CacheItemHelper const*)> const&, unsigned long, unsigned long*)::{lambda(rocksdb::clock_cache::AutoHyperClockTable::HandleImpl const&)#1} const&, rocksdb::clock_cache::AutoHyperClockTable::HandleImpl const*, rocksdb::clock_cache::AutoHyperClockTable::HandleImpl const, bool)
Line
Count
Source
321
28.2k
                              bool apply_if_will_be_deleted) {
322
56.5k
  for (const HandleImpl* h = begin; h < end; ++h) {
323
    // Note: to avoid using compare_exchange, we have to be extra careful.
324
28.2k
    SlotMeta old_meta = h->meta.LoadRelaxed();
325
    // Check if it's an entry visible to lookups
326
28.2k
    if (apply_if_will_be_deleted || old_meta.IsVisible()) {
327
516
      if (old_meta.IsShareable()) {
328
        // Increment acquire counter. Note: it's possible that the entry has
329
        // completely changed since we loaded old_meta, but incrementing acquire
330
        // count is always safe. (Similar to optimistic Lookup here.)
331
516
        auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
332
516
        h->meta.Apply(add_acquire, &old_meta);
333
        // Check whether we actually acquired a reference.
334
516
        if (old_meta.IsShareable()) {
335
          // Apply func if appropriate
336
516
          if (apply_if_will_be_deleted || old_meta.IsVisible()) {
337
516
            func(*h);
338
516
          }
339
          // Pretend we never took the reference
340
516
          Unref(*h);
341
          // No net change, so don't need to check for overflow
342
516
        } else {
343
          // For other states, incrementing the acquire counter has no effect
344
          // so we don't need to undo it. Furthermore, we cannot safely undo
345
          // it because we did not acquire a read reference to lock the
346
          // entry in a Shareable state.
347
0
        }
348
516
      }
349
516
    }
350
28.2k
  }
351
28.2k
}
352
353
646k
uint32_t SanitizeEvictionEffortCap(int eviction_effort_cap) {
354
646k
  eviction_effort_cap = std::max(int{1}, eviction_effort_cap);
355
646k
  return static_cast<uint32_t>(eviction_effort_cap);
356
646k
}
357
358
}  // namespace
359
360
107k
void ClockHandleBasicData::FreeData(MemoryAllocator* allocator) const {
361
107k
  if (helper->del_cb) {
362
89.7k
    helper->del_cb(value, allocator);
363
89.7k
  }
364
107k
}
365
366
BaseClockTable::BaseClockTable(size_t capacity, bool strict_capacity_limit,
367
                               int eviction_effort_cap,
368
                               CacheMetadataChargePolicy metadata_charge_policy,
369
                               MemoryAllocator* allocator,
370
                               const Cache::EvictionCallback* eviction_callback,
371
                               const uint32_t* hash_seed)
372
646k
    : capacity_(capacity),
373
646k
      eec_and_scl_(EecAndScl{}
374
646k
                       .With<EvictionEffortCap>(
375
646k
                           SanitizeEvictionEffortCap(eviction_effort_cap))
376
646k
                       .With<StrictCapacityLimit>(strict_capacity_limit)),
377
646k
      metadata_charge_policy_(metadata_charge_policy),
378
646k
      allocator_(allocator),
379
646k
      eviction_callback_(*eviction_callback),
380
646k
      hash_seed_(*hash_seed) {}
381
382
template <class HandleImpl>
383
HandleImpl* BaseClockTable::StandaloneInsert(
384
0
    const ClockHandleBasicData& proto) {
385
  // Heap allocated separate from table
386
0
  HandleImpl* h = new HandleImpl();
387
0
  ClockHandleBasicData* h_alias = h;
388
0
  *h_alias = proto;
389
0
  h->SetStandalone();
390
  // Single reference (standalone entries only created if returning a refed
391
  // Handle back to user)
392
0
  SlotMeta meta;
393
0
  meta.SetInvisible();
394
0
  meta.SetAcquireCounter(1);
395
0
  h->meta.Store(meta);
396
  // Keep track of how much of usage is standalone
397
0
  standalone_usage_.FetchAddRelaxed(proto.GetTotalCharge());
398
0
  return h;
399
0
}
Unexecuted instantiation: rocksdb::clock_cache::FixedHyperClockTable::HandleImpl* rocksdb::clock_cache::BaseClockTable::StandaloneInsert<rocksdb::clock_cache::FixedHyperClockTable::HandleImpl>(rocksdb::clock_cache::ClockHandleBasicData const&)
Unexecuted instantiation: rocksdb::clock_cache::AutoHyperClockTable::HandleImpl* rocksdb::clock_cache::BaseClockTable::StandaloneInsert<rocksdb::clock_cache::AutoHyperClockTable::HandleImpl>(rocksdb::clock_cache::ClockHandleBasicData const&)
400
401
template <class Table>
402
typename Table::HandleImpl* BaseClockTable::CreateStandalone(
403
0
    ClockHandleBasicData& proto, bool allow_uncharged) {
404
0
  Table& derived = static_cast<Table&>(*this);
405
0
  typename Table::InsertState state;
406
0
  derived.StartInsert(state);
407
408
0
  const size_t total_charge = proto.GetTotalCharge();
409
  // NOTE: we can use eec_and_scl as eviction_effort_cap below because
410
  // strict_capacity_limit=true is supposed to disable the limit on eviction
411
  // effort, and a large value effectively does that.
412
0
  if (eec_and_scl_.LoadRelaxed().Get<StrictCapacityLimit>()) {
413
0
    Status s = ChargeUsageMaybeEvictStrict<Table>(
414
0
        total_charge,
415
0
        /*need_evict_for_occupancy=*/false, state);
416
0
    if (!s.ok()) {
417
0
      if (allow_uncharged) {
418
0
        proto.total_charge = 0;
419
0
      } else {
420
0
        return nullptr;
421
0
      }
422
0
    }
423
0
  } else {
424
    // Case strict_capacity_limit == false
425
0
    bool success = ChargeUsageMaybeEvictNonStrict<Table>(
426
0
        total_charge,
427
0
        /*need_evict_for_occupancy=*/false, state);
428
0
    if (!success) {
429
      // Force the issue
430
0
      usage_.FetchAddRelaxed(total_charge);
431
0
    }
432
0
  }
433
434
0
  return StandaloneInsert<typename Table::HandleImpl>(proto);
435
0
}
Unexecuted instantiation: rocksdb::clock_cache::FixedHyperClockTable::HandleImpl* rocksdb::clock_cache::BaseClockTable::CreateStandalone<rocksdb::clock_cache::FixedHyperClockTable>(rocksdb::clock_cache::ClockHandleBasicData&, bool)
Unexecuted instantiation: rocksdb::clock_cache::AutoHyperClockTable::HandleImpl* rocksdb::clock_cache::BaseClockTable::CreateStandalone<rocksdb::clock_cache::AutoHyperClockTable>(rocksdb::clock_cache::ClockHandleBasicData&, bool)
436
437
template <class Table>
438
Status BaseClockTable::ChargeUsageMaybeEvictStrict(
439
    size_t total_charge, bool need_evict_for_occupancy,
440
0
    typename Table::InsertState& state) {
441
0
  const size_t capacity = capacity_.LoadRelaxed();
442
0
  if (total_charge > capacity) {
443
0
    return Status::MemoryLimit(
444
0
        "Cache entry too large for a single cache shard: " +
445
0
        std::to_string(total_charge) + " > " + std::to_string(capacity));
446
0
  }
447
  // Grab any available capacity, and free up any more required.
448
0
  size_t old_usage = usage_.LoadRelaxed();
449
0
  size_t new_usage;
450
0
  do {
451
0
    new_usage = std::min(capacity, old_usage + total_charge);
452
0
    if (new_usage == old_usage) {
453
      // No change needed
454
0
      break;
455
0
    }
456
0
  } while (!usage_.CasWeakRelaxed(old_usage, new_usage));
457
  // How much do we need to evict then?
458
0
  size_t need_evict_charge = old_usage + total_charge - new_usage;
459
0
  size_t request_evict_charge = need_evict_charge;
460
0
  if (UNLIKELY(need_evict_for_occupancy) && request_evict_charge == 0) {
461
    // Require at least 1 eviction.
462
0
    request_evict_charge = 1;
463
0
  }
464
0
  if (request_evict_charge > 0) {
465
0
    EvictionData data;
466
0
    static_cast<Table*>(this)->Evict(request_evict_charge, state, &data);
467
0
    occupancy_.FetchSub(data.freed_count);
468
0
    if (LIKELY(data.freed_charge > need_evict_charge)) {
469
0
      assert(data.freed_count > 0);
470
      // Evicted more than enough
471
0
      usage_.FetchSubRelaxed(data.freed_charge - need_evict_charge);
472
0
    } else if (data.freed_charge < need_evict_charge ||
473
0
               (UNLIKELY(need_evict_for_occupancy) && data.freed_count == 0)) {
474
      // Roll back to old usage minus evicted
475
0
      usage_.FetchSubRelaxed(data.freed_charge + (new_usage - old_usage));
476
0
      if (data.freed_charge < need_evict_charge) {
477
0
        return Status::MemoryLimit(
478
0
            "Insert failed because unable to evict entries to stay within "
479
0
            "capacity limit.");
480
0
      } else {
481
0
        return Status::MemoryLimit(
482
0
            "Insert failed because unable to evict entries to stay within "
483
0
            "table occupancy limit.");
484
0
      }
485
0
    }
486
    // If we needed to evict something and we are proceeding, we must have
487
    // evicted something.
488
0
    assert(data.freed_count > 0);
489
0
  }
490
0
  return Status::OK();
491
0
}
Unexecuted instantiation: rocksdb::Status rocksdb::clock_cache::BaseClockTable::ChargeUsageMaybeEvictStrict<rocksdb::clock_cache::FixedHyperClockTable>(unsigned long, bool, rocksdb::clock_cache::FixedHyperClockTable::InsertState&)
Unexecuted instantiation: rocksdb::Status rocksdb::clock_cache::BaseClockTable::ChargeUsageMaybeEvictStrict<rocksdb::clock_cache::AutoHyperClockTable>(unsigned long, bool, rocksdb::clock_cache::AutoHyperClockTable::InsertState&)
492
493
template <class Table>
494
inline bool BaseClockTable::ChargeUsageMaybeEvictNonStrict(
495
    size_t total_charge, bool need_evict_for_occupancy,
496
107k
    typename Table::InsertState& state) {
497
  // For simplicity, we consider that either the cache can accept the insert
498
  // with no evictions, or we must evict enough to make (at least) enough
499
  // space. It could lead to unnecessary failures or excessive evictions in
500
  // some extreme cases, but allows a fast, simple protocol. If we allow a
501
  // race to get us over capacity, then we might never get back to capacity
502
  // limit if the sizes of entries allow each insertion to evict the minimum
503
  // charge. Thus, we should evict some extra if it's not a signifcant
504
  // portion of the shard capacity. This can have the side benefit of
505
  // involving fewer threads in eviction.
506
107k
  const size_t old_usage = usage_.LoadRelaxed();
507
107k
  const size_t capacity = capacity_.LoadRelaxed();
508
107k
  size_t need_evict_charge;
509
  // NOTE: if total_charge > old_usage, there isn't yet enough to evict
510
  // `total_charge` amount. Even if we only try to evict `old_usage` amount,
511
  // there's likely something referenced and we would eat CPU looking for
512
  // enough to evict.
513
107k
  if (old_usage + total_charge <= capacity || total_charge > old_usage) {
514
    // Good enough for me (might run over with a race)
515
107k
    need_evict_charge = 0;
516
107k
  } else {
517
    // Try to evict enough space, and maybe some extra
518
1
    need_evict_charge = total_charge;
519
1
    if (old_usage > capacity) {
520
      // Not too much to avoid thundering herd while avoiding strict
521
      // synchronization, such as the compare_exchange used with strict
522
      // capacity limit.
523
0
      need_evict_charge += std::min(capacity / 1024, total_charge) + 1;
524
0
    }
525
1
  }
526
107k
  if (UNLIKELY(need_evict_for_occupancy) && need_evict_charge == 0) {
527
    // Special case: require at least 1 eviction if we only have to
528
    // deal with occupancy
529
0
    need_evict_charge = 1;
530
0
  }
531
107k
  EvictionData data;
532
107k
  if (need_evict_charge > 0) {
533
0
    static_cast<Table*>(this)->Evict(need_evict_charge, state, &data);
534
    // Deal with potential occupancy deficit
535
0
    if (UNLIKELY(need_evict_for_occupancy) && data.freed_count == 0) {
536
0
      assert(data.freed_charge == 0);
537
      // Can't meet occupancy requirement
538
0
      return false;
539
0
    } else {
540
      // Update occupancy for evictions
541
0
      occupancy_.FetchSub(data.freed_count);
542
0
    }
543
0
  }
544
  // Track new usage even if we weren't able to evict enough
545
107k
  usage_.FetchAddRelaxed(total_charge - data.freed_charge);
546
  // No underflow
547
107k
  assert(usage_.LoadRelaxed() < SIZE_MAX / 2);
548
  // Success
549
107k
  return true;
550
107k
}
Unexecuted instantiation: bool rocksdb::clock_cache::BaseClockTable::ChargeUsageMaybeEvictNonStrict<rocksdb::clock_cache::FixedHyperClockTable>(unsigned long, bool, rocksdb::clock_cache::FixedHyperClockTable::InsertState&)
bool rocksdb::clock_cache::BaseClockTable::ChargeUsageMaybeEvictNonStrict<rocksdb::clock_cache::AutoHyperClockTable>(unsigned long, bool, rocksdb::clock_cache::AutoHyperClockTable::InsertState&)
Line
Count
Source
496
107k
    typename Table::InsertState& state) {
497
  // For simplicity, we consider that either the cache can accept the insert
498
  // with no evictions, or we must evict enough to make (at least) enough
499
  // space. It could lead to unnecessary failures or excessive evictions in
500
  // some extreme cases, but allows a fast, simple protocol. If we allow a
501
  // race to get us over capacity, then we might never get back to capacity
502
  // limit if the sizes of entries allow each insertion to evict the minimum
503
  // charge. Thus, we should evict some extra if it's not a signifcant
504
  // portion of the shard capacity. This can have the side benefit of
505
  // involving fewer threads in eviction.
506
107k
  const size_t old_usage = usage_.LoadRelaxed();
507
107k
  const size_t capacity = capacity_.LoadRelaxed();
508
107k
  size_t need_evict_charge;
509
  // NOTE: if total_charge > old_usage, there isn't yet enough to evict
510
  // `total_charge` amount. Even if we only try to evict `old_usage` amount,
511
  // there's likely something referenced and we would eat CPU looking for
512
  // enough to evict.
513
107k
  if (old_usage + total_charge <= capacity || total_charge > old_usage) {
514
    // Good enough for me (might run over with a race)
515
107k
    need_evict_charge = 0;
516
107k
  } else {
517
    // Try to evict enough space, and maybe some extra
518
1
    need_evict_charge = total_charge;
519
1
    if (old_usage > capacity) {
520
      // Not too much to avoid thundering herd while avoiding strict
521
      // synchronization, such as the compare_exchange used with strict
522
      // capacity limit.
523
0
      need_evict_charge += std::min(capacity / 1024, total_charge) + 1;
524
0
    }
525
1
  }
526
107k
  if (UNLIKELY(need_evict_for_occupancy) && need_evict_charge == 0) {
527
    // Special case: require at least 1 eviction if we only have to
528
    // deal with occupancy
529
0
    need_evict_charge = 1;
530
0
  }
531
107k
  EvictionData data;
532
107k
  if (need_evict_charge > 0) {
533
0
    static_cast<Table*>(this)->Evict(need_evict_charge, state, &data);
534
    // Deal with potential occupancy deficit
535
0
    if (UNLIKELY(need_evict_for_occupancy) && data.freed_count == 0) {
536
0
      assert(data.freed_charge == 0);
537
      // Can't meet occupancy requirement
538
0
      return false;
539
0
    } else {
540
      // Update occupancy for evictions
541
0
      occupancy_.FetchSub(data.freed_count);
542
0
    }
543
0
  }
544
  // Track new usage even if we weren't able to evict enough
545
107k
  usage_.FetchAddRelaxed(total_charge - data.freed_charge);
546
  // No underflow
547
107k
  assert(usage_.LoadRelaxed() < SIZE_MAX / 2);
548
  // Success
549
107k
  return true;
550
107k
}
551
552
0
void BaseClockTable::TrackAndReleaseEvictedEntry(ClockHandle* h) {
553
0
  bool took_value_ownership = false;
554
0
  if (eviction_callback_) {
555
    // For key reconstructed from hash
556
0
    UniqueId64x2 unhashed;
557
0
    took_value_ownership = eviction_callback_(
558
0
        ClockCacheShard<FixedHyperClockTable>::ReverseHash(
559
0
            h->GetHash(), &unhashed, hash_seed_),
560
0
        static_cast<Cache::Handle*>(h), h->meta.LoadRelaxed().GetHit());
561
0
  }
562
0
  if (!took_value_ownership) {
563
0
    h->FreeData(allocator_);
564
0
  }
565
0
  MarkEmpty(*h);
566
0
}
567
568
bool BaseClockTable::IsEvictionEffortExceeded(
569
0
    const BaseClockTable::EvictionData& data) const {
570
0
  auto eviction_effort_cap =
571
0
      eec_and_scl_.LoadRelaxed().GetEffectiveEvictionEffortCap();
572
  // Basically checks whether the ratio of useful effort to wasted effort is
573
  // too low, with a start-up allowance for wasted effort before any useful
574
  // effort.
575
0
  return (data.freed_count + 1U) * uint64_t{eviction_effort_cap} <=
576
0
         data.seen_pinned_count;
577
0
}
578
579
template <class Table>
580
Status BaseClockTable::Insert(const ClockHandleBasicData& proto,
581
                              typename Table::HandleImpl** handle,
582
107k
                              Cache::Priority priority) {
583
107k
  using HandleImpl = typename Table::HandleImpl;
584
107k
  Table& derived = static_cast<Table&>(*this);
585
586
107k
  typename Table::InsertState state;
587
107k
  derived.StartInsert(state);
588
589
  // Do we have the available occupancy? Optimistically assume we do
590
  // and deal with it if we don't.
591
107k
  size_t old_occupancy = occupancy_.FetchAdd(1);
592
  // Whether we over-committed and need an eviction to make up for it
593
107k
  bool need_evict_for_occupancy =
594
107k
      !derived.GrowIfNeeded(old_occupancy + 1, state);
595
596
  // Usage/capacity handling is somewhat different depending on
597
  // strict_capacity_limit, but mostly pessimistic.
598
107k
  bool use_standalone_insert = false;
599
107k
  const size_t total_charge = proto.GetTotalCharge();
600
  // NOTE: we can use eec_and_scl as eviction_effort_cap below because
601
  // strict_capacity_limit=true is supposed to disable the limit on eviction
602
  // effort, and a large value effectively does that.
603
107k
  if (eec_and_scl_.LoadRelaxed().Get<StrictCapacityLimit>()) {
604
0
    Status s = ChargeUsageMaybeEvictStrict<Table>(
605
0
        total_charge, need_evict_for_occupancy, state);
606
0
    if (!s.ok()) {
607
      // Revert occupancy
608
0
      occupancy_.FetchSubRelaxed(1);
609
0
      return s;
610
0
    }
611
107k
  } else {
612
    // Case strict_capacity_limit == false
613
107k
    bool success = ChargeUsageMaybeEvictNonStrict<Table>(
614
107k
        total_charge, need_evict_for_occupancy, state);
615
107k
    if (!success) {
616
      // Revert occupancy
617
0
      occupancy_.FetchSubRelaxed(1);
618
0
      if (handle == nullptr) {
619
        // Don't insert the entry but still return ok, as if the entry
620
        // inserted into cache and evicted immediately.
621
0
        proto.FreeData(allocator_);
622
0
        return Status::OK();
623
0
      } else {
624
        // Need to track usage of fallback standalone insert
625
0
        usage_.FetchAddRelaxed(total_charge);
626
0
        use_standalone_insert = true;
627
0
      }
628
0
    }
629
107k
  }
630
631
107k
  if (!use_standalone_insert) {
632
    // Attempt a table insert, but abort if we find an existing entry for the
633
    // key. If we were to overwrite old entries, we would either
634
    // * Have to gain ownership over an existing entry to overwrite it, which
635
    // would only work if there are no outstanding (read) references and would
636
    // create a small gap in availability of the entry (old or new) to lookups.
637
    // * Have to insert into a suboptimal location (more probes) so that the
638
    // old entry can be kept around as well.
639
640
107k
    uint32_t initial_countdown = GetInitialCountdown(priority);
641
107k
    assert(initial_countdown > 0);
642
643
107k
    HandleImpl* e =
644
107k
        derived.DoInsert(proto, initial_countdown, handle != nullptr, state);
645
646
107k
    if (e) {
647
      // Successfully inserted
648
107k
      if (handle) {
649
107k
        *handle = e;
650
107k
      }
651
107k
      return Status::OK();
652
107k
    }
653
    // Not inserted
654
    // Revert occupancy
655
5
    occupancy_.FetchSubRelaxed(1);
656
    // Maybe fall back on standalone insert
657
5
    if (handle == nullptr) {
658
      // Revert usage
659
0
      usage_.FetchSubRelaxed(total_charge);
660
      // No underflow
661
0
      assert(usage_.LoadRelaxed() < SIZE_MAX / 2);
662
      // As if unrefed entry immdiately evicted
663
0
      proto.FreeData(allocator_);
664
0
      return Status::OK();
665
0
    }
666
667
5
    use_standalone_insert = true;
668
5
  }
669
670
  // Run standalone insert
671
107k
  assert(use_standalone_insert);
672
673
2
  *handle = StandaloneInsert<HandleImpl>(proto);
674
675
  // The OkOverwritten status is used to count "redundant" insertions into
676
  // block cache. This implementation doesn't strictly check for redundant
677
  // insertions, but we instead are probably interested in how many insertions
678
  // didn't go into the table (instead "standalone"), which could be redundant
679
  // Insert or some other reason (use_standalone_insert reasons above).
680
2
  return Status::OkOverwritten();
681
107k
}
Unexecuted instantiation: rocksdb::Status rocksdb::clock_cache::BaseClockTable::Insert<rocksdb::clock_cache::FixedHyperClockTable>(rocksdb::clock_cache::ClockHandleBasicData const&, rocksdb::clock_cache::FixedHyperClockTable::HandleImpl**, rocksdb::Cache::Priority)
rocksdb::Status rocksdb::clock_cache::BaseClockTable::Insert<rocksdb::clock_cache::AutoHyperClockTable>(rocksdb::clock_cache::ClockHandleBasicData const&, rocksdb::clock_cache::AutoHyperClockTable::HandleImpl**, rocksdb::Cache::Priority)
Line
Count
Source
582
107k
                              Cache::Priority priority) {
583
107k
  using HandleImpl = typename Table::HandleImpl;
584
107k
  Table& derived = static_cast<Table&>(*this);
585
586
107k
  typename Table::InsertState state;
587
107k
  derived.StartInsert(state);
588
589
  // Do we have the available occupancy? Optimistically assume we do
590
  // and deal with it if we don't.
591
107k
  size_t old_occupancy = occupancy_.FetchAdd(1);
592
  // Whether we over-committed and need an eviction to make up for it
593
107k
  bool need_evict_for_occupancy =
594
107k
      !derived.GrowIfNeeded(old_occupancy + 1, state);
595
596
  // Usage/capacity handling is somewhat different depending on
597
  // strict_capacity_limit, but mostly pessimistic.
598
107k
  bool use_standalone_insert = false;
599
107k
  const size_t total_charge = proto.GetTotalCharge();
600
  // NOTE: we can use eec_and_scl as eviction_effort_cap below because
601
  // strict_capacity_limit=true is supposed to disable the limit on eviction
602
  // effort, and a large value effectively does that.
603
107k
  if (eec_and_scl_.LoadRelaxed().Get<StrictCapacityLimit>()) {
604
0
    Status s = ChargeUsageMaybeEvictStrict<Table>(
605
0
        total_charge, need_evict_for_occupancy, state);
606
0
    if (!s.ok()) {
607
      // Revert occupancy
608
0
      occupancy_.FetchSubRelaxed(1);
609
0
      return s;
610
0
    }
611
107k
  } else {
612
    // Case strict_capacity_limit == false
613
107k
    bool success = ChargeUsageMaybeEvictNonStrict<Table>(
614
107k
        total_charge, need_evict_for_occupancy, state);
615
107k
    if (!success) {
616
      // Revert occupancy
617
0
      occupancy_.FetchSubRelaxed(1);
618
0
      if (handle == nullptr) {
619
        // Don't insert the entry but still return ok, as if the entry
620
        // inserted into cache and evicted immediately.
621
0
        proto.FreeData(allocator_);
622
0
        return Status::OK();
623
0
      } else {
624
        // Need to track usage of fallback standalone insert
625
0
        usage_.FetchAddRelaxed(total_charge);
626
0
        use_standalone_insert = true;
627
0
      }
628
0
    }
629
107k
  }
630
631
107k
  if (!use_standalone_insert) {
632
    // Attempt a table insert, but abort if we find an existing entry for the
633
    // key. If we were to overwrite old entries, we would either
634
    // * Have to gain ownership over an existing entry to overwrite it, which
635
    // would only work if there are no outstanding (read) references and would
636
    // create a small gap in availability of the entry (old or new) to lookups.
637
    // * Have to insert into a suboptimal location (more probes) so that the
638
    // old entry can be kept around as well.
639
640
107k
    uint32_t initial_countdown = GetInitialCountdown(priority);
641
107k
    assert(initial_countdown > 0);
642
643
107k
    HandleImpl* e =
644
107k
        derived.DoInsert(proto, initial_countdown, handle != nullptr, state);
645
646
107k
    if (e) {
647
      // Successfully inserted
648
107k
      if (handle) {
649
107k
        *handle = e;
650
107k
      }
651
107k
      return Status::OK();
652
107k
    }
653
    // Not inserted
654
    // Revert occupancy
655
5
    occupancy_.FetchSubRelaxed(1);
656
    // Maybe fall back on standalone insert
657
5
    if (handle == nullptr) {
658
      // Revert usage
659
0
      usage_.FetchSubRelaxed(total_charge);
660
      // No underflow
661
0
      assert(usage_.LoadRelaxed() < SIZE_MAX / 2);
662
      // As if unrefed entry immdiately evicted
663
0
      proto.FreeData(allocator_);
664
0
      return Status::OK();
665
0
    }
666
667
5
    use_standalone_insert = true;
668
5
  }
669
670
  // Run standalone insert
671
107k
  assert(use_standalone_insert);
672
673
2
  *handle = StandaloneInsert<HandleImpl>(proto);
674
675
  // The OkOverwritten status is used to count "redundant" insertions into
676
  // block cache. This implementation doesn't strictly check for redundant
677
  // insertions, but we instead are probably interested in how many insertions
678
  // didn't go into the table (instead "standalone"), which could be redundant
679
  // Insert or some other reason (use_standalone_insert reasons above).
680
2
  return Status::OkOverwritten();
681
107k
}
682
683
0
void BaseClockTable::Ref(ClockHandle& h) {
684
  // Increment acquire counter
685
0
  SlotMeta old_meta;
686
0
  h.meta.Apply(AcquireCounter::PlusTransformPromiseNoOverflow(1), &old_meta);
687
688
0
  assert(old_meta.IsShareable());
689
  // Must have already had a reference
690
0
  assert(old_meta.GetRefcount() > 0);
691
0
  (void)old_meta;
692
0
}
693
694
#ifndef NDEBUG
695
void BaseClockTable::TEST_RefN(ClockHandle& h, uint32_t n) {
696
  // Increment acquire counter
697
  SlotMeta old_meta;
698
  h.meta.Apply(AcquireCounter::PlusTransformPromiseNoOverflow(n), &old_meta);
699
700
  assert(old_meta.IsShareable());
701
  (void)old_meta;
702
}
703
704
void BaseClockTable::TEST_ReleaseNMinus1(ClockHandle* h, uint32_t n) {
705
  assert(n > 0);
706
707
  // Like n-1 Releases, but assumes one more will happen in the caller to take
708
  // care of anything like erasing an unreferenced, invisible entry.
709
  SlotMeta old_meta;
710
  h->meta.Apply(ReleaseCounter::PlusTransformPromiseNoOverflow(n - 1),
711
                &old_meta);
712
  assert(old_meta.IsShareable());
713
  (void)old_meta;
714
}
715
#endif
716
717
FixedHyperClockTable::FixedHyperClockTable(
718
    size_t capacity, bool strict_capacity_limit,
719
    CacheMetadataChargePolicy metadata_charge_policy,
720
    MemoryAllocator* allocator,
721
    const Cache::EvictionCallback* eviction_callback, const uint32_t* hash_seed,
722
    const Opts& opts)
723
0
    : BaseClockTable(capacity, strict_capacity_limit, opts.eviction_effort_cap,
724
0
                     metadata_charge_policy, allocator, eviction_callback,
725
0
                     hash_seed),
726
0
      length_bits_(CalcHashBits(capacity, opts.estimated_value_size,
727
0
                                metadata_charge_policy)),
728
0
      length_bits_mask_((size_t{1} << length_bits_) - 1),
729
0
      occupancy_limit_(static_cast<size_t>((uint64_t{1} << length_bits_) *
730
0
                                           kStrictLoadFactor)),
731
0
      array_(new HandleImpl[size_t{1} << length_bits_]) {
732
0
  if (metadata_charge_policy ==
733
0
      CacheMetadataChargePolicy::kFullChargeCacheMetadata) {
734
0
    usage_.FetchAddRelaxed(size_t{GetTableSize()} * sizeof(HandleImpl));
735
0
  }
736
737
0
  static_assert(sizeof(HandleImpl) == 64U,
738
0
                "Expecting size / alignment with common cache line size");
739
0
}
740
741
0
FixedHyperClockTable::~FixedHyperClockTable() {
742
  // Assumes there are no references or active operations on any slot/element
743
  // in the table.
744
0
  for (size_t i = 0; i < GetTableSize(); i++) {
745
0
    HandleImpl& h = array_[i];
746
0
    SlotMeta meta = h.meta.LoadRelaxed();
747
0
    if (meta.IsShareable()) {
748
      // NOTE: Reaching here invisible is rare but possible
749
0
      assert(meta.GetRefcount() == 0);
750
0
      h.FreeData(allocator_);
751
#ifndef NDEBUG
752
      Rollback(h.hashed_key, &h);
753
      ReclaimEntryUsage(h.GetTotalCharge());
754
#endif
755
0
    } else {
756
      // Should be no transient "under construction" states unless a thread
757
      // was killed or we are being destructed while another thread is still
758
      // operating on the structure
759
0
      assert(meta.IsEmpty());
760
0
    }
761
0
  }
762
763
#ifndef NDEBUG
764
  for (size_t i = 0; i < GetTableSize(); i++) {
765
    assert(array_[i].displacements.LoadRelaxed() == 0);
766
  }
767
#endif
768
769
0
  assert(usage_.LoadRelaxed() == 0 ||
770
0
         usage_.LoadRelaxed() == size_t{GetTableSize()} * sizeof(HandleImpl));
771
0
  assert(occupancy_.LoadRelaxed() == 0);
772
0
}
773
774
0
void FixedHyperClockTable::StartInsert(InsertState&) {}
775
776
0
bool FixedHyperClockTable::GrowIfNeeded(size_t new_occupancy, InsertState&) {
777
0
  return new_occupancy <= occupancy_limit_;
778
0
}
779
780
FixedHyperClockTable::HandleImpl* FixedHyperClockTable::DoInsert(
781
    const ClockHandleBasicData& proto, uint32_t initial_countdown,
782
0
    bool keep_ref, InsertState&) {
783
0
  bool already_matches = false;
784
0
  HandleImpl* e = FindSlot(
785
0
      proto.hashed_key,
786
0
      [&](HandleImpl* h) {
787
0
        return TryInsert(proto, *h, initial_countdown, keep_ref,
788
0
                         &already_matches);
789
0
      },
790
0
      [&](HandleImpl* h) {
791
0
        if (already_matches) {
792
          // Stop searching & roll back displacements
793
0
          Rollback(proto.hashed_key, h);
794
0
          return true;
795
0
        } else {
796
          // Keep going
797
0
          return false;
798
0
        }
799
0
      },
800
0
      [&](HandleImpl* h, bool is_last) {
801
0
        if (is_last) {
802
          // Search is ending. Roll back displacements
803
0
          Rollback(proto.hashed_key, h);
804
0
        } else {
805
0
          h->displacements.FetchAddRelaxed(1);
806
0
        }
807
0
      });
808
0
  if (already_matches) {
809
    // Insertion skipped
810
0
    return nullptr;
811
0
  }
812
0
  if (e != nullptr) {
813
    // Successfully inserted
814
0
    return e;
815
0
  }
816
  // Else, no available slot found. Occupancy check should generally prevent
817
  // this, except it's theoretically possible for other threads to evict and
818
  // replace entries in the right order to hit every slot when it is populated.
819
  // Assuming random hashing, the chance of that should be no higher than
820
  // pow(kStrictLoadFactor, n) for n slots. That should be infeasible for
821
  // roughly n >= 256, so if this assertion fails, that suggests something is
822
  // going wrong.
823
0
  assert(GetTableSize() < 256);
824
0
  return nullptr;
825
0
}
826
827
FixedHyperClockTable::HandleImpl* FixedHyperClockTable::Lookup(
828
0
    const UniqueId64x2& hashed_key) {
829
0
  HandleImpl* e = FindSlot(
830
0
      hashed_key,
831
0
      [&](HandleImpl* h) {
832
0
        SlotMeta old_meta;
833
        // Mostly branch-free version (similar performance)
834
        /*
835
        h->meta.Apply(AcquireCounter::PlusTransformPromiseNoOverflow(1),
836
                      &old_meta);
837
        bool shareable = old_meta.IsShareable();
838
        bool visible = old_meta.IsVisible();
839
        bool match = (h->hashed_key == hashed_key) & visible;
840
        h->meta.Apply(AcquireCounter::MinusTransformPromiseNoUnderflow(
841
            uint32_t{shareable} & uint32_t{!match}));
842
        h->meta.Apply(SlotMeta::HitFlag::Or(match));
843
        return match;
844
        */
845
        // Optimistic lookup should pay off when the table is relatively
846
        // sparse.
847
0
        constexpr bool kOptimisticLookup = true;
848
0
        if (!kOptimisticLookup) {
849
0
          old_meta = h->meta.Load();
850
0
          if (!old_meta.IsVisible()) {
851
0
            return false;
852
0
          }
853
0
        }
854
        // (Optimistically) increment acquire counter
855
0
        h->meta.Apply(AcquireCounter::PlusTransformPromiseNoOverflow(1),
856
0
                      &old_meta);
857
        // Check if it's an entry visible to lookups
858
0
        if (old_meta.IsVisible()) {
859
          // Acquired a read reference
860
0
          if (h->hashed_key == hashed_key) {
861
            // Match
862
            // Update the hit bit
863
0
            if (eviction_callback_) {
864
0
              h->meta.ApplyRelaxed(SlotMeta::HitFlag::SetTransform());
865
0
            }
866
0
            return true;
867
0
          } else {
868
            // Mismatch. Pretend we never took the reference
869
0
            Unref(*h);
870
0
          }
871
0
        } else if (UNLIKELY(old_meta.IsInvisible())) {
872
          // Pretend we never took the reference
873
0
          Unref(*h);
874
0
        } else {
875
          // For other states, incrementing the acquire counter has no effect
876
          // so we don't need to undo it. Furthermore, we cannot safely undo
877
          // it because we did not acquire a read reference to lock the
878
          // entry in a Shareable state.
879
0
        }
880
0
        return false;
881
0
      },
882
0
      [&](HandleImpl* h) { return h->displacements.LoadRelaxed() == 0; },
883
0
      [&](HandleImpl* /*h*/, bool /*is_last*/) {});
884
885
0
  return e;
886
0
}
887
888
bool FixedHyperClockTable::Release(HandleImpl* h, bool useful,
889
0
                                   bool erase_if_last_ref) {
890
  // In contrast with LRUCache's Release, this function won't delete the handle
891
  // when the cache is above capacity and the reference is the last one. Space
892
  // is only freed up by EvictFromClock (called by Insert when space is needed)
893
  // and Erase. We do this to avoid an extra atomic read of the variable usage_.
894
895
0
  SlotMeta old_meta;
896
0
  if (useful) {
897
    // Increment release counter to indicate was used
898
0
    auto add_release = ReleaseCounter::PlusTransformPromiseNoOverflow(1);
899
0
    h->meta.Apply(add_release, &old_meta);
900
0
  } else {
901
    // Decrement acquire counter to pretend it never happened
902
0
    auto sub_acquire = AcquireCounter::MinusTransformPromiseNoUnderflow(1);
903
0
    h->meta.Apply(sub_acquire, &old_meta);
904
0
  }
905
906
0
  assert(old_meta.IsShareable());
907
  // No underflow
908
0
  assert(old_meta.GetAcquireCounter() != old_meta.GetReleaseCounter());
909
910
0
  if (erase_if_last_ref || UNLIKELY(old_meta.IsInvisible())) {
911
    // FIXME: There's a chance here that another thread could replace this
912
    // entry and we end up erasing the wrong one.
913
914
    // Update for last Apply op
915
0
    if (useful) {
916
0
      old_meta.SetReleaseCounter(old_meta.GetReleaseCounter() + 1);
917
0
    } else {
918
0
      old_meta.SetAcquireCounter(old_meta.GetAcquireCounter() - 1);
919
0
    }
920
    // Take ownership if no refs
921
0
    SlotMeta construction_meta;
922
0
    construction_meta.SetUnderConstruction();
923
0
    do {
924
0
      if (old_meta.GetRefcount() != 0) {
925
        // Not last ref at some point in time during this Release call
926
        // Correct for possible (but rare) overflow
927
0
        CorrectNearOverflow(old_meta, h->meta);
928
0
        return false;
929
0
      }
930
0
      if (!old_meta.IsShareable()) {
931
        // Someone else took ownership
932
0
        return false;
933
0
      }
934
      // Note that there's a small chance that we release, another thread
935
      // replaces this entry with another, reaches zero refs, and then we end
936
      // up erasing that other entry. That's an acceptable risk / imprecision.
937
0
    } while (!h->meta.CasWeak(old_meta, construction_meta));
938
    // Took ownership
939
0
    size_t total_charge = h->GetTotalCharge();
940
0
    if (UNLIKELY(h->IsStandalone())) {
941
0
      h->FreeData(allocator_);
942
      // Delete standalone handle
943
0
      delete h;
944
0
      standalone_usage_.FetchSubRelaxed(total_charge);
945
0
      usage_.FetchSubRelaxed(total_charge);
946
0
    } else {
947
0
      Rollback(h->hashed_key, h);
948
0
      FreeDataMarkEmpty(*h, allocator_);
949
0
      ReclaimEntryUsage(total_charge);
950
0
    }
951
0
    return true;
952
0
  } else {
953
    // Correct for possible (but rare) overflow
954
0
    CorrectNearOverflow(old_meta, h->meta);
955
0
    return false;
956
0
  }
957
0
}
958
959
#ifndef NDEBUG
960
void FixedHyperClockTable::TEST_ReleaseN(HandleImpl* h, uint32_t n) {
961
  if (n > 0) {
962
    // Do n-1 simple releases first
963
    TEST_ReleaseNMinus1(h, n);
964
965
    // Then the last release might be more involved
966
    Release(h, /*useful*/ true, /*erase_if_last_ref*/ false);
967
  }
968
}
969
#endif
970
971
0
void FixedHyperClockTable::Erase(const UniqueId64x2& hashed_key) {
972
0
  (void)FindSlot(
973
0
      hashed_key,
974
0
      [&](HandleImpl* h) {
975
        // Could be multiple entries in rare cases. Erase them all.
976
        // Optimistically increment acquire counter
977
0
        auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
978
0
        SlotMeta old_meta, meta;
979
0
        h->meta.Apply(add_acquire, &old_meta, &meta);
980
        // Check if it's an entry visible to lookups
981
0
        if (meta.IsVisible()) {
982
          // Acquired a read reference
983
0
          if (h->hashed_key == hashed_key) {
984
            // Match. Take ownership if no other refs, or set invisible other
985
            // refs exist.
986
0
            for (;;) {
987
0
              uint32_t refcount = meta.GetRefcount();
988
0
              assert(refcount > 0);
989
0
              if (refcount > 1) {
990
                // Not last ref at some point in time during this Erase call
991
                // Set invisible
992
0
                h->meta.Apply(SlotMeta::VisibleFlag::ClearTransform());
993
                // And pretend we never took the reference
994
0
                Unref(*h);
995
0
                break;
996
0
              }
997
0
              SlotMeta construction_meta;
998
0
              construction_meta.SetUnderConstruction();
999
0
              if (h->meta.CasWeak(meta, construction_meta)) {
1000
                // Took ownership
1001
0
                assert(hashed_key == h->hashed_key);
1002
0
                size_t total_charge = h->GetTotalCharge();
1003
0
                FreeDataMarkEmpty(*h, allocator_);
1004
0
                ReclaimEntryUsage(total_charge);
1005
                // We already have a copy of hashed_key in this case, so OK to
1006
                // delay Rollback until after releasing the entry
1007
0
                Rollback(hashed_key, h);
1008
0
                break;
1009
0
              }
1010
0
            }
1011
0
          } else {
1012
            // Mismatch. Pretend we never took the reference
1013
0
            Unref(*h);
1014
0
          }
1015
0
        } else if (UNLIKELY(old_meta.IsInvisible())) {
1016
          // Pretend we never took the reference
1017
0
          Unref(*h);
1018
0
        } else {
1019
          // For other states, incrementing the acquire counter has no effect
1020
          // so we don't need to undo it.
1021
0
        }
1022
0
        return false;
1023
0
      },
1024
0
      [&](HandleImpl* h) { return h->displacements.LoadRelaxed() == 0; },
1025
0
      [&](HandleImpl* /*h*/, bool /*is_last*/) {});
1026
0
}
1027
1028
0
void FixedHyperClockTable::EraseUnRefEntries() {
1029
0
  for (size_t i = 0; i <= this->length_bits_mask_; i++) {
1030
0
    HandleImpl& h = array_[i];
1031
1032
0
    SlotMeta old_meta = h.meta.LoadRelaxed();
1033
0
    if (old_meta.IsShareable() && old_meta.GetRefcount() == 0) {
1034
0
      SlotMeta construction_meta;
1035
0
      construction_meta.SetUnderConstruction();
1036
0
      if (h.meta.CasStrong(old_meta, construction_meta)) {
1037
        // Took ownership
1038
0
        size_t total_charge = h.GetTotalCharge();
1039
0
        Rollback(h.hashed_key, &h);
1040
0
        FreeDataMarkEmpty(h, allocator_);
1041
0
        ReclaimEntryUsage(total_charge);
1042
0
      }
1043
0
    }
1044
0
  }
1045
0
}
1046
1047
template <typename MatchFn, typename AbortFn, typename UpdateFn>
1048
inline FixedHyperClockTable::HandleImpl* FixedHyperClockTable::FindSlot(
1049
    const UniqueId64x2& hashed_key, const MatchFn& match_fn,
1050
0
    const AbortFn& abort_fn, const UpdateFn& update_fn) {
1051
  // NOTE: upper 32 bits of hashed_key[0] is used for sharding
1052
  //
1053
  // We use double-hashing probing. Every probe in the sequence is a
1054
  // pseudorandom integer, computed as a linear function of two random hashes,
1055
  // which we call base and increment. Specifically, the i-th probe is base + i
1056
  // * increment modulo the table size.
1057
0
  size_t base = static_cast<size_t>(hashed_key[1]);
1058
  // We use an odd increment, which is relatively prime with the power-of-two
1059
  // table size. This implies that we cycle back to the first probe only
1060
  // after probing every slot exactly once.
1061
  // TODO: we could also reconsider linear probing, though locality benefits
1062
  // are limited because each slot is a full cache line
1063
0
  size_t increment = static_cast<size_t>(hashed_key[0]) | 1U;
1064
0
  size_t first = ModTableSize(base);
1065
0
  size_t current = first;
1066
0
  bool is_last;
1067
0
  do {
1068
0
    HandleImpl* h = &array_[current];
1069
0
    if (match_fn(h)) {
1070
0
      return h;
1071
0
    }
1072
0
    if (abort_fn(h)) {
1073
0
      return nullptr;
1074
0
    }
1075
0
    current = ModTableSize(current + increment);
1076
0
    is_last = current == first;
1077
0
    update_fn(h, is_last);
1078
0
  } while (!is_last);
1079
  // We looped back.
1080
0
  return nullptr;
1081
0
}
Unexecuted instantiation: clock_cache.cc:rocksdb::clock_cache::FixedHyperClockTable::HandleImpl* rocksdb::clock_cache::FixedHyperClockTable::FindSlot<rocksdb::clock_cache::FixedHyperClockTable::DoInsert(rocksdb::clock_cache::ClockHandleBasicData const&, unsigned int, bool, rocksdb::clock_cache::FixedHyperClockTable::InsertState&)::$_0, rocksdb::clock_cache::FixedHyperClockTable::DoInsert(rocksdb::clock_cache::ClockHandleBasicData const&, unsigned int, bool, rocksdb::clock_cache::FixedHyperClockTable::InsertState&)::$_1, rocksdb::clock_cache::FixedHyperClockTable::DoInsert(rocksdb::clock_cache::ClockHandleBasicData const&, unsigned int, bool, rocksdb::clock_cache::FixedHyperClockTable::InsertState&)::$_2>(std::__1::array<unsigned long, 2ul> const&, rocksdb::clock_cache::FixedHyperClockTable::DoInsert(rocksdb::clock_cache::ClockHandleBasicData const&, unsigned int, bool, rocksdb::clock_cache::FixedHyperClockTable::InsertState&)::$_0 const&, rocksdb::clock_cache::FixedHyperClockTable::DoInsert(rocksdb::clock_cache::ClockHandleBasicData const&, unsigned int, bool, rocksdb::clock_cache::FixedHyperClockTable::InsertState&)::$_1 const&, rocksdb::clock_cache::FixedHyperClockTable::DoInsert(rocksdb::clock_cache::ClockHandleBasicData const&, unsigned int, bool, rocksdb::clock_cache::FixedHyperClockTable::InsertState&)::$_2 const&)
Unexecuted instantiation: clock_cache.cc:rocksdb::clock_cache::FixedHyperClockTable::HandleImpl* rocksdb::clock_cache::FixedHyperClockTable::FindSlot<rocksdb::clock_cache::FixedHyperClockTable::Lookup(std::__1::array<unsigned long, 2ul> const&)::$_0, rocksdb::clock_cache::FixedHyperClockTable::Lookup(std::__1::array<unsigned long, 2ul> const&)::$_1, rocksdb::clock_cache::FixedHyperClockTable::Lookup(std::__1::array<unsigned long, 2ul> const&)::$_2>(std::__1::array<unsigned long, 2ul> const&, rocksdb::clock_cache::FixedHyperClockTable::Lookup(std::__1::array<unsigned long, 2ul> const&)::$_0 const&, rocksdb::clock_cache::FixedHyperClockTable::Lookup(std::__1::array<unsigned long, 2ul> const&)::$_1 const&, rocksdb::clock_cache::FixedHyperClockTable::Lookup(std::__1::array<unsigned long, 2ul> const&)::$_2 const&)
Unexecuted instantiation: clock_cache.cc:rocksdb::clock_cache::FixedHyperClockTable::HandleImpl* rocksdb::clock_cache::FixedHyperClockTable::FindSlot<rocksdb::clock_cache::FixedHyperClockTable::Erase(std::__1::array<unsigned long, 2ul> const&)::$_0, rocksdb::clock_cache::FixedHyperClockTable::Erase(std::__1::array<unsigned long, 2ul> const&)::$_1, rocksdb::clock_cache::FixedHyperClockTable::Erase(std::__1::array<unsigned long, 2ul> const&)::$_2>(std::__1::array<unsigned long, 2ul> const&, rocksdb::clock_cache::FixedHyperClockTable::Erase(std::__1::array<unsigned long, 2ul> const&)::$_0 const&, rocksdb::clock_cache::FixedHyperClockTable::Erase(std::__1::array<unsigned long, 2ul> const&)::$_1 const&, rocksdb::clock_cache::FixedHyperClockTable::Erase(std::__1::array<unsigned long, 2ul> const&)::$_2 const&)
1082
1083
inline void FixedHyperClockTable::Rollback(const UniqueId64x2& hashed_key,
1084
0
                                           const HandleImpl* h) {
1085
0
  size_t current = ModTableSize(hashed_key[1]);
1086
0
  size_t increment = static_cast<size_t>(hashed_key[0]) | 1U;
1087
0
  while (&array_[current] != h) {
1088
0
    array_[current].displacements.FetchSubRelaxed(1);
1089
0
    current = ModTableSize(current + increment);
1090
0
  }
1091
0
}
1092
1093
0
inline void FixedHyperClockTable::ReclaimEntryUsage(size_t total_charge) {
1094
0
  auto old_occupancy = occupancy_.FetchSub(1U);
1095
0
  (void)old_occupancy;
1096
  // No underflow
1097
0
  assert(old_occupancy > 0);
1098
0
  auto old_usage = usage_.FetchSubRelaxed(total_charge);
1099
0
  (void)old_usage;
1100
  // No underflow
1101
0
  assert(old_usage >= total_charge);
1102
0
}
1103
1104
inline void FixedHyperClockTable::Evict(size_t requested_charge, InsertState&,
1105
0
                                        EvictionData* data) {
1106
  // precondition
1107
0
  assert(requested_charge > 0);
1108
1109
  // TODO: make a tuning parameter?
1110
0
  constexpr size_t step_size = 4;
1111
1112
  // First (concurrent) increment clock pointer
1113
0
  uint64_t old_clock_pointer = clock_pointer_.FetchAddRelaxed(step_size);
1114
1115
  // Cap the eviction effort at this thread (along with those operating in
1116
  // parallel) circling through the whole structure kMaxCountdown times.
1117
  // In other words, this eviction run must find something/anything that is
1118
  // unreferenced at start of and during the eviction run that isn't reclaimed
1119
  // by a concurrent eviction run.
1120
0
  uint64_t max_clock_pointer =
1121
0
      old_clock_pointer + (ClockHandle::kMaxCountdown << length_bits_);
1122
1123
0
  for (;;) {
1124
0
    for (size_t i = 0; i < step_size; i++) {
1125
0
      HandleImpl& h = array_[ModTableSize(Lower32of64(old_clock_pointer + i))];
1126
0
      bool evicting = ClockUpdate(h, data);
1127
0
      if (evicting) {
1128
0
        Rollback(h.hashed_key, &h);
1129
0
        TrackAndReleaseEvictedEntry(&h);
1130
0
      }
1131
0
    }
1132
1133
    // Loop exit condition
1134
0
    if (data->freed_charge >= requested_charge) {
1135
0
      return;
1136
0
    }
1137
0
    if (old_clock_pointer >= max_clock_pointer) {
1138
0
      return;
1139
0
    }
1140
0
    if (IsEvictionEffortExceeded(*data)) {
1141
0
      eviction_effort_exceeded_count_.FetchAddRelaxed(1);
1142
0
      return;
1143
0
    }
1144
1145
    // Advance clock pointer (concurrently)
1146
0
    old_clock_pointer = clock_pointer_.FetchAddRelaxed(step_size);
1147
0
  }
1148
0
}
1149
1150
template <class Table>
1151
ClockCacheShard<Table>::ClockCacheShard(
1152
    size_t capacity, bool strict_capacity_limit,
1153
    CacheMetadataChargePolicy metadata_charge_policy,
1154
    MemoryAllocator* allocator,
1155
    const Cache::EvictionCallback* eviction_callback, const uint32_t* hash_seed,
1156
    const typename Table::Opts& opts)
1157
646k
    : CacheShardBase(metadata_charge_policy),
1158
646k
      table_(capacity, strict_capacity_limit, metadata_charge_policy, allocator,
1159
646k
             eviction_callback, hash_seed, opts) {
1160
  // Initial charge metadata should not exceed capacity
1161
646k
  assert(table_.GetUsage() <= table_.GetCapacity() ||
1162
646k
         table_.GetCapacity() < sizeof(HandleImpl));
1163
646k
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::ClockCacheShard(unsigned long, bool, rocksdb::CacheMetadataChargePolicy, rocksdb::MemoryAllocator*, std::__1::function<bool (rocksdb::Slice const&, rocksdb::Cache::Handle*, bool)> const*, unsigned int const*, rocksdb::clock_cache::FixedHyperClockTable::Opts const&)
rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::ClockCacheShard(unsigned long, bool, rocksdb::CacheMetadataChargePolicy, rocksdb::MemoryAllocator*, std::__1::function<bool (rocksdb::Slice const&, rocksdb::Cache::Handle*, bool)> const*, unsigned int const*, rocksdb::clock_cache::AutoHyperClockTable::Opts const&)
Line
Count
Source
1157
646k
    : CacheShardBase(metadata_charge_policy),
1158
646k
      table_(capacity, strict_capacity_limit, metadata_charge_policy, allocator,
1159
646k
             eviction_callback, hash_seed, opts) {
1160
  // Initial charge metadata should not exceed capacity
1161
  assert(table_.GetUsage() <= table_.GetCapacity() ||
1162
646k
         table_.GetCapacity() < sizeof(HandleImpl));
1163
646k
}
1164
1165
template <class Table>
1166
0
void ClockCacheShard<Table>::EraseUnRefEntries() {
1167
0
  table_.EraseUnRefEntries();
1168
0
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::EraseUnRefEntries()
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::EraseUnRefEntries()
1169
1170
template <class Table>
1171
void ClockCacheShard<Table>::ApplyToSomeEntries(
1172
    const std::function<void(const Slice& key, Cache::ObjectPtr value,
1173
                             size_t charge,
1174
                             const Cache::CacheItemHelper* helper)>& callback,
1175
28.2k
    size_t average_entries_per_lock, size_t* state) {
1176
  // The state will be a simple index into the table. Even with a dynamic
1177
  // hyper clock cache, entries will generally stay in their existing
1178
  // slots, so we don't need to be aware of the high-level organization
1179
  // that makes lookup efficient.
1180
28.2k
  size_t length = table_.GetTableSize();
1181
1182
28.2k
  assert(average_entries_per_lock > 0);
1183
1184
28.2k
  size_t index_begin = *state;
1185
28.2k
  size_t index_end = index_begin + average_entries_per_lock;
1186
28.2k
  if (index_end >= length) {
1187
    // Going to end.
1188
442
    index_end = length;
1189
442
    *state = SIZE_MAX;
1190
27.8k
  } else {
1191
27.8k
    *state = index_end;
1192
27.8k
  }
1193
1194
28.2k
  auto hash_seed = table_.GetHashSeed();
1195
28.2k
  ConstApplyToEntriesRange(
1196
28.2k
      [callback, hash_seed](const HandleImpl& h) {
1197
516
        UniqueId64x2 unhashed;
1198
516
        callback(ReverseHash(h.hashed_key, &unhashed, hash_seed), h.value,
1199
516
                 h.GetTotalCharge(), h.helper);
1200
516
      },
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::ApplyToSomeEntries(std::__1::function<void (rocksdb::Slice const&, void*, unsigned long, rocksdb::Cache::CacheItemHelper const*)> const&, unsigned long, unsigned long*)::{lambda(rocksdb::clock_cache::FixedHyperClockTable::HandleImpl const&)#1}::operator()(rocksdb::clock_cache::FixedHyperClockTable::HandleImpl const&) const
rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::ApplyToSomeEntries(std::__1::function<void (rocksdb::Slice const&, void*, unsigned long, rocksdb::Cache::CacheItemHelper const*)> const&, unsigned long, unsigned long*)::{lambda(rocksdb::clock_cache::AutoHyperClockTable::HandleImpl const&)#1}::operator()(rocksdb::clock_cache::AutoHyperClockTable::HandleImpl const&) const
Line
Count
Source
1196
516
      [callback, hash_seed](const HandleImpl& h) {
1197
516
        UniqueId64x2 unhashed;
1198
516
        callback(ReverseHash(h.hashed_key, &unhashed, hash_seed), h.value,
1199
516
                 h.GetTotalCharge(), h.helper);
1200
516
      },
1201
28.2k
      table_.HandlePtr(index_begin), table_.HandlePtr(index_end), false);
1202
28.2k
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::ApplyToSomeEntries(std::__1::function<void (rocksdb::Slice const&, void*, unsigned long, rocksdb::Cache::CacheItemHelper const*)> const&, unsigned long, unsigned long*)
rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::ApplyToSomeEntries(std::__1::function<void (rocksdb::Slice const&, void*, unsigned long, rocksdb::Cache::CacheItemHelper const*)> const&, unsigned long, unsigned long*)
Line
Count
Source
1175
28.2k
    size_t average_entries_per_lock, size_t* state) {
1176
  // The state will be a simple index into the table. Even with a dynamic
1177
  // hyper clock cache, entries will generally stay in their existing
1178
  // slots, so we don't need to be aware of the high-level organization
1179
  // that makes lookup efficient.
1180
28.2k
  size_t length = table_.GetTableSize();
1181
1182
28.2k
  assert(average_entries_per_lock > 0);
1183
1184
28.2k
  size_t index_begin = *state;
1185
28.2k
  size_t index_end = index_begin + average_entries_per_lock;
1186
28.2k
  if (index_end >= length) {
1187
    // Going to end.
1188
442
    index_end = length;
1189
442
    *state = SIZE_MAX;
1190
27.8k
  } else {
1191
27.8k
    *state = index_end;
1192
27.8k
  }
1193
1194
28.2k
  auto hash_seed = table_.GetHashSeed();
1195
28.2k
  ConstApplyToEntriesRange(
1196
28.2k
      [callback, hash_seed](const HandleImpl& h) {
1197
28.2k
        UniqueId64x2 unhashed;
1198
28.2k
        callback(ReverseHash(h.hashed_key, &unhashed, hash_seed), h.value,
1199
28.2k
                 h.GetTotalCharge(), h.helper);
1200
28.2k
      },
1201
28.2k
      table_.HandlePtr(index_begin), table_.HandlePtr(index_end), false);
1202
28.2k
}
1203
1204
int FixedHyperClockTable::CalcHashBits(
1205
    size_t capacity, size_t estimated_value_size,
1206
0
    CacheMetadataChargePolicy metadata_charge_policy) {
1207
0
  double average_slot_charge = estimated_value_size * kLoadFactor;
1208
0
  if (metadata_charge_policy == kFullChargeCacheMetadata) {
1209
0
    average_slot_charge += sizeof(HandleImpl);
1210
0
  }
1211
0
  assert(average_slot_charge > 0.0);
1212
0
  uint64_t num_slots =
1213
0
      static_cast<uint64_t>(capacity / average_slot_charge + 0.999999);
1214
1215
0
  int hash_bits = FloorLog2((num_slots << 1) - 1);
1216
0
  if (metadata_charge_policy == kFullChargeCacheMetadata) {
1217
    // For very small estimated value sizes, it's possible to overshoot
1218
0
    while (hash_bits > 0 &&
1219
0
           uint64_t{sizeof(HandleImpl)} << hash_bits > capacity) {
1220
0
      hash_bits--;
1221
0
    }
1222
0
  }
1223
0
  return hash_bits;
1224
0
}
1225
1226
template <class Table>
1227
0
void ClockCacheShard<Table>::SetCapacity(size_t capacity) {
1228
0
  table_.SetCapacity(capacity);
1229
  // next Insert will take care of any necessary evictions
1230
0
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::SetCapacity(unsigned long)
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::SetCapacity(unsigned long)
1231
1232
template <class Table>
1233
void ClockCacheShard<Table>::SetStrictCapacityLimit(
1234
0
    bool strict_capacity_limit) {
1235
0
  table_.SetStrictCapacityLimit(strict_capacity_limit);
1236
  // next Insert will take care of any necessary evictions
1237
0
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::SetStrictCapacityLimit(bool)
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::SetStrictCapacityLimit(bool)
1238
1239
template <class Table>
1240
Status ClockCacheShard<Table>::Insert(const Slice& key,
1241
                                      const UniqueId64x2& hashed_key,
1242
                                      Cache::ObjectPtr value,
1243
                                      const Cache::CacheItemHelper* helper,
1244
                                      size_t charge, HandleImpl** handle,
1245
107k
                                      Cache::Priority priority) {
1246
107k
  if (UNLIKELY(key.size() != kCacheKeySize)) {
1247
0
    return Status::NotSupported("ClockCache only supports key size " +
1248
0
                                std::to_string(kCacheKeySize) + "B");
1249
0
  }
1250
107k
  ClockHandleBasicData proto;
1251
107k
  proto.hashed_key = hashed_key;
1252
107k
  proto.value = value;
1253
107k
  proto.helper = helper;
1254
107k
  proto.total_charge = charge;
1255
107k
  return table_.template Insert<Table>(proto, handle, priority);
1256
107k
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::Insert(rocksdb::Slice const&, std::__1::array<unsigned long, 2ul> const&, void*, rocksdb::Cache::CacheItemHelper const*, unsigned long, rocksdb::clock_cache::FixedHyperClockTable::HandleImpl**, rocksdb::Cache::Priority)
rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::Insert(rocksdb::Slice const&, std::__1::array<unsigned long, 2ul> const&, void*, rocksdb::Cache::CacheItemHelper const*, unsigned long, rocksdb::clock_cache::AutoHyperClockTable::HandleImpl**, rocksdb::Cache::Priority)
Line
Count
Source
1245
107k
                                      Cache::Priority priority) {
1246
107k
  if (UNLIKELY(key.size() != kCacheKeySize)) {
1247
0
    return Status::NotSupported("ClockCache only supports key size " +
1248
0
                                std::to_string(kCacheKeySize) + "B");
1249
0
  }
1250
107k
  ClockHandleBasicData proto;
1251
107k
  proto.hashed_key = hashed_key;
1252
107k
  proto.value = value;
1253
107k
  proto.helper = helper;
1254
107k
  proto.total_charge = charge;
1255
107k
  return table_.template Insert<Table>(proto, handle, priority);
1256
107k
}
1257
1258
template <class Table>
1259
typename Table::HandleImpl* ClockCacheShard<Table>::CreateStandalone(
1260
    const Slice& key, const UniqueId64x2& hashed_key, Cache::ObjectPtr obj,
1261
0
    const Cache::CacheItemHelper* helper, size_t charge, bool allow_uncharged) {
1262
0
  if (UNLIKELY(key.size() != kCacheKeySize)) {
1263
0
    return nullptr;
1264
0
  }
1265
0
  ClockHandleBasicData proto;
1266
0
  proto.hashed_key = hashed_key;
1267
0
  proto.value = obj;
1268
0
  proto.helper = helper;
1269
0
  proto.total_charge = charge;
1270
0
  return table_.template CreateStandalone<Table>(proto, allow_uncharged);
1271
0
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::CreateStandalone(rocksdb::Slice const&, std::__1::array<unsigned long, 2ul> const&, void*, rocksdb::Cache::CacheItemHelper const*, unsigned long, bool)
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::CreateStandalone(rocksdb::Slice const&, std::__1::array<unsigned long, 2ul> const&, void*, rocksdb::Cache::CacheItemHelper const*, unsigned long, bool)
1272
1273
template <class Table>
1274
typename ClockCacheShard<Table>::HandleImpl* ClockCacheShard<Table>::Lookup(
1275
232k
    const Slice& key, const UniqueId64x2& hashed_key) {
1276
232k
  if (UNLIKELY(key.size() != kCacheKeySize)) {
1277
0
    return nullptr;
1278
0
  }
1279
232k
  return table_.Lookup(hashed_key);
1280
232k
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::Lookup(rocksdb::Slice const&, std::__1::array<unsigned long, 2ul> const&)
rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::Lookup(rocksdb::Slice const&, std::__1::array<unsigned long, 2ul> const&)
Line
Count
Source
1275
232k
    const Slice& key, const UniqueId64x2& hashed_key) {
1276
232k
  if (UNLIKELY(key.size() != kCacheKeySize)) {
1277
0
    return nullptr;
1278
0
  }
1279
232k
  return table_.Lookup(hashed_key);
1280
232k
}
1281
1282
template <class Table>
1283
0
bool ClockCacheShard<Table>::Ref(HandleImpl* h) {
1284
0
  if (h == nullptr) {
1285
0
    return false;
1286
0
  }
1287
0
  table_.Ref(*h);
1288
0
  return true;
1289
0
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::Ref(rocksdb::clock_cache::FixedHyperClockTable::HandleImpl*)
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::Ref(rocksdb::clock_cache::AutoHyperClockTable::HandleImpl*)
1290
1291
template <class Table>
1292
bool ClockCacheShard<Table>::Release(HandleImpl* handle, bool useful,
1293
164k
                                     bool erase_if_last_ref) {
1294
164k
  if (handle == nullptr) {
1295
0
    return false;
1296
0
  }
1297
164k
  return table_.Release(handle, useful, erase_if_last_ref);
1298
164k
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::Release(rocksdb::clock_cache::FixedHyperClockTable::HandleImpl*, bool, bool)
rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::Release(rocksdb::clock_cache::AutoHyperClockTable::HandleImpl*, bool, bool)
Line
Count
Source
1293
164k
                                     bool erase_if_last_ref) {
1294
164k
  if (handle == nullptr) {
1295
0
    return false;
1296
0
  }
1297
164k
  return table_.Release(handle, useful, erase_if_last_ref);
1298
164k
}
1299
1300
#ifndef NDEBUG
1301
template <class Table>
1302
void ClockCacheShard<Table>::TEST_RefN(HandleImpl* h, uint32_t n) {
1303
  table_.TEST_RefN(*h, n);
1304
}
1305
1306
template <class Table>
1307
void ClockCacheShard<Table>::TEST_ReleaseN(HandleImpl* h, uint32_t n) {
1308
  table_.TEST_ReleaseN(h, n);
1309
}
1310
#endif
1311
1312
template <class Table>
1313
bool ClockCacheShard<Table>::Release(HandleImpl* handle,
1314
0
                                     bool erase_if_last_ref) {
1315
0
  return Release(handle, /*useful=*/true, erase_if_last_ref);
1316
0
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::Release(rocksdb::clock_cache::FixedHyperClockTable::HandleImpl*, bool)
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::Release(rocksdb::clock_cache::AutoHyperClockTable::HandleImpl*, bool)
1317
1318
template <class Table>
1319
void ClockCacheShard<Table>::Erase(const Slice& key,
1320
0
                                   const UniqueId64x2& hashed_key) {
1321
0
  if (UNLIKELY(key.size() != kCacheKeySize)) {
1322
0
    return;
1323
0
  }
1324
0
  table_.Erase(hashed_key);
1325
0
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::Erase(rocksdb::Slice const&, std::__1::array<unsigned long, 2ul> const&)
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::Erase(rocksdb::Slice const&, std::__1::array<unsigned long, 2ul> const&)
1326
1327
template <class Table>
1328
442
size_t ClockCacheShard<Table>::GetUsage() const {
1329
442
  return table_.GetUsage();
1330
442
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::GetUsage() const
rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::GetUsage() const
Line
Count
Source
1328
442
size_t ClockCacheShard<Table>::GetUsage() const {
1329
442
  return table_.GetUsage();
1330
442
}
1331
1332
template <class Table>
1333
0
size_t ClockCacheShard<Table>::GetStandaloneUsage() const {
1334
0
  return table_.GetStandaloneUsage();
1335
0
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::GetStandaloneUsage() const
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::GetStandaloneUsage() const
1336
1337
template <class Table>
1338
0
size_t ClockCacheShard<Table>::GetCapacity() const {
1339
0
  return table_.GetCapacity();
1340
0
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::GetCapacity() const
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::GetCapacity() const
1341
1342
template <class Table>
1343
0
size_t ClockCacheShard<Table>::GetPinnedUsage() const {
1344
  // Computes the pinned usage by scanning the whole hash table. This
1345
  // is slow, but avoids keeping an exact counter on the clock usage,
1346
  // i.e., the number of not externally referenced elements.
1347
  // Why avoid this counter? Because Lookup removes elements from the clock
1348
  // list, so it would need to update the pinned usage every time,
1349
  // which creates additional synchronization costs.
1350
0
  size_t table_pinned_usage = 0;
1351
0
  const bool charge_metadata =
1352
0
      metadata_charge_policy_ == kFullChargeCacheMetadata;
1353
0
  ConstApplyToEntriesRange(
1354
0
      [&table_pinned_usage, charge_metadata](const HandleImpl& h) {
1355
0
        SlotMeta meta = h.meta.LoadRelaxed();
1356
0
        uint32_t refcount = meta.GetRefcount();
1357
        // Holding one ref for ConstApplyToEntriesRange
1358
0
        assert(refcount > 0);
1359
0
        if (refcount > 1) {
1360
0
          table_pinned_usage += h.GetTotalCharge();
1361
0
          if (charge_metadata) {
1362
0
            table_pinned_usage += sizeof(HandleImpl);
1363
0
          }
1364
0
        }
1365
0
      },
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::GetPinnedUsage() const::{lambda(rocksdb::clock_cache::FixedHyperClockTable::HandleImpl const&)#1}::operator()(rocksdb::clock_cache::FixedHyperClockTable::HandleImpl const&) const
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::GetPinnedUsage() const::{lambda(rocksdb::clock_cache::AutoHyperClockTable::HandleImpl const&)#1}::operator()(rocksdb::clock_cache::AutoHyperClockTable::HandleImpl const&) const
1366
0
      table_.HandlePtr(0), table_.HandlePtr(table_.GetTableSize()), true);
1367
1368
0
  return table_pinned_usage + table_.GetStandaloneUsage();
1369
0
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::GetPinnedUsage() const
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::GetPinnedUsage() const
1370
1371
template <class Table>
1372
442
size_t ClockCacheShard<Table>::GetOccupancyCount() const {
1373
442
  return table_.GetOccupancy();
1374
442
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::GetOccupancyCount() const
rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::GetOccupancyCount() const
Line
Count
Source
1372
442
size_t ClockCacheShard<Table>::GetOccupancyCount() const {
1373
442
  return table_.GetOccupancy();
1374
442
}
1375
1376
template <class Table>
1377
0
size_t ClockCacheShard<Table>::GetOccupancyLimit() const {
1378
0
  return table_.GetOccupancyLimit();
1379
0
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::GetOccupancyLimit() const
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::GetOccupancyLimit() const
1380
1381
template <class Table>
1382
442
size_t ClockCacheShard<Table>::GetTableAddressCount() const {
1383
442
  return table_.GetTableSize();
1384
442
}
Unexecuted instantiation: rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>::GetTableAddressCount() const
rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>::GetTableAddressCount() const
Line
Count
Source
1382
442
size_t ClockCacheShard<Table>::GetTableAddressCount() const {
1383
442
  return table_.GetTableSize();
1384
442
}
1385
1386
// Explicit instantiation
1387
template class ClockCacheShard<FixedHyperClockTable>;
1388
template class ClockCacheShard<AutoHyperClockTable>;
1389
1390
template <class Table>
1391
BaseHyperClockCache<Table>::BaseHyperClockCache(
1392
    const HyperClockCacheOptions& opts)
1393
646k
    : ShardedCache<ClockCacheShard<Table>>(opts) {
1394
  // TODO: should not need to go through two levels of pointer indirection to
1395
  // get to table entries
1396
646k
  size_t per_shard = this->GetPerShardCapacity();
1397
646k
  MemoryAllocator* alloc = this->memory_allocator();
1398
646k
  this->InitShards([&](Shard* cs) {
1399
646k
    typename Table::Opts table_opts{opts};
1400
646k
    new (cs) Shard(per_shard, opts.strict_capacity_limit,
1401
646k
                   opts.metadata_charge_policy, alloc,
1402
646k
                   &this->eviction_callback_, &this->hash_seed_, table_opts);
1403
646k
  });
rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::AutoHyperClockTable>::BaseHyperClockCache(rocksdb::HyperClockCacheOptions const&)::{lambda(rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>*)#1}::operator()(rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable>*) const
Line
Count
Source
1398
646k
  this->InitShards([&](Shard* cs) {
1399
646k
    typename Table::Opts table_opts{opts};
1400
646k
    new (cs) Shard(per_shard, opts.strict_capacity_limit,
1401
646k
                   opts.metadata_charge_policy, alloc,
1402
646k
                   &this->eviction_callback_, &this->hash_seed_, table_opts);
1403
646k
  });
Unexecuted instantiation: rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::FixedHyperClockTable>::BaseHyperClockCache(rocksdb::HyperClockCacheOptions const&)::{lambda(rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>*)#1}::operator()(rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable>*) const
1404
646k
}
rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::AutoHyperClockTable>::BaseHyperClockCache(rocksdb::HyperClockCacheOptions const&)
Line
Count
Source
1393
646k
    : ShardedCache<ClockCacheShard<Table>>(opts) {
1394
  // TODO: should not need to go through two levels of pointer indirection to
1395
  // get to table entries
1396
646k
  size_t per_shard = this->GetPerShardCapacity();
1397
646k
  MemoryAllocator* alloc = this->memory_allocator();
1398
646k
  this->InitShards([&](Shard* cs) {
1399
646k
    typename Table::Opts table_opts{opts};
1400
646k
    new (cs) Shard(per_shard, opts.strict_capacity_limit,
1401
646k
                   opts.metadata_charge_policy, alloc,
1402
646k
                   &this->eviction_callback_, &this->hash_seed_, table_opts);
1403
646k
  });
1404
646k
}
Unexecuted instantiation: rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::FixedHyperClockTable>::BaseHyperClockCache(rocksdb::HyperClockCacheOptions const&)
1405
1406
template <class Table>
1407
232k
Cache::ObjectPtr BaseHyperClockCache<Table>::Value(Handle* handle) {
1408
232k
  return static_cast<const typename Table::HandleImpl*>(handle)->value;
1409
232k
}
Unexecuted instantiation: rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::FixedHyperClockTable>::Value(rocksdb::Cache::Handle*)
rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::AutoHyperClockTable>::Value(rocksdb::Cache::Handle*)
Line
Count
Source
1407
232k
Cache::ObjectPtr BaseHyperClockCache<Table>::Value(Handle* handle) {
1408
232k
  return static_cast<const typename Table::HandleImpl*>(handle)->value;
1409
232k
}
1410
1411
template <class Table>
1412
17.8k
size_t BaseHyperClockCache<Table>::GetCharge(Handle* handle) const {
1413
17.8k
  return static_cast<const typename Table::HandleImpl*>(handle)
1414
17.8k
      ->GetTotalCharge();
1415
17.8k
}
Unexecuted instantiation: rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::FixedHyperClockTable>::GetCharge(rocksdb::Cache::Handle*) const
rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::AutoHyperClockTable>::GetCharge(rocksdb::Cache::Handle*) const
Line
Count
Source
1412
17.8k
size_t BaseHyperClockCache<Table>::GetCharge(Handle* handle) const {
1413
17.8k
  return static_cast<const typename Table::HandleImpl*>(handle)
1414
17.8k
      ->GetTotalCharge();
1415
17.8k
}
1416
1417
template <class Table>
1418
const Cache::CacheItemHelper* BaseHyperClockCache<Table>::GetCacheItemHelper(
1419
0
    Handle* handle) const {
1420
0
  auto h = static_cast<const typename Table::HandleImpl*>(handle);
1421
0
  return h->helper;
1422
0
}
Unexecuted instantiation: rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::FixedHyperClockTable>::GetCacheItemHelper(rocksdb::Cache::Handle*) const
Unexecuted instantiation: rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::AutoHyperClockTable>::GetCacheItemHelper(rocksdb::Cache::Handle*) const
1423
1424
template <class Table>
1425
void BaseHyperClockCache<Table>::ApplyToHandle(
1426
    Cache* cache, Handle* handle,
1427
    const std::function<void(const Slice& key, Cache::ObjectPtr value,
1428
                             size_t charge, const CacheItemHelper* helper)>&
1429
0
        callback) {
1430
0
  BaseHyperClockCache<Table>* cache_ptr =
1431
0
      static_cast<BaseHyperClockCache<Table>*>(cache);
1432
0
  auto h = static_cast<const typename Table::HandleImpl*>(handle);
1433
0
  UniqueId64x2 unhashed;
1434
0
  auto hash_seed = cache_ptr->GetShard(h->GetHash()).GetTable().GetHashSeed();
1435
0
  callback(
1436
0
      ClockCacheShard<Table>::ReverseHash(h->hashed_key, &unhashed, hash_seed),
1437
0
      h->value, h->GetTotalCharge(), h->helper);
1438
0
}
Unexecuted instantiation: rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::FixedHyperClockTable>::ApplyToHandle(rocksdb::Cache*, rocksdb::Cache::Handle*, std::__1::function<void (rocksdb::Slice const&, void*, unsigned long, rocksdb::Cache::CacheItemHelper const*)> const&)
Unexecuted instantiation: rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::AutoHyperClockTable>::ApplyToHandle(rocksdb::Cache*, rocksdb::Cache::Handle*, std::__1::function<void (rocksdb::Slice const&, void*, unsigned long, rocksdb::Cache::CacheItemHelper const*)> const&)
1439
1440
namespace {
1441
1442
// For each cache shard, estimate what the table load factor would be if
1443
// cache filled to capacity with average entries. This is considered
1444
// indicative of a potential problem if the shard is essentially operating
1445
// "at limit", which we define as high actual usage (>80% of capacity)
1446
// or actual occupancy very close to limit (>95% of limit).
1447
// Also, for each shard compute the recommended estimated_entry_charge,
1448
// and keep the minimum one for use as overall recommendation.
1449
void AddShardEvaluation(const FixedHyperClockCache::Shard& shard,
1450
                        std::vector<double>& predicted_load_factors,
1451
0
                        size_t& min_recommendation) {
1452
0
  size_t usage = shard.GetUsage() - shard.GetStandaloneUsage();
1453
0
  size_t capacity = shard.GetCapacity();
1454
0
  double usage_ratio = 1.0 * usage / capacity;
1455
1456
0
  size_t occupancy = shard.GetOccupancyCount();
1457
0
  size_t occ_limit = shard.GetOccupancyLimit();
1458
0
  double occ_ratio = 1.0 * occupancy / occ_limit;
1459
0
  if (usage == 0 || occupancy == 0 || (usage_ratio < 0.8 && occ_ratio < 0.95)) {
1460
    // Skip as described above
1461
0
    return;
1462
0
  }
1463
1464
  // If filled to capacity, what would the occupancy ratio be?
1465
0
  double ratio = occ_ratio / usage_ratio;
1466
  // Given max load factor, what that load factor be?
1467
0
  double lf = ratio * FixedHyperClockTable::kStrictLoadFactor;
1468
0
  predicted_load_factors.push_back(lf);
1469
1470
  // Update min_recommendation also
1471
0
  size_t recommendation = usage / occupancy;
1472
0
  min_recommendation = std::min(min_recommendation, recommendation);
1473
0
}
1474
1475
0
bool IsSlotOccupied(const ClockHandle& h) {
1476
0
  return !h.meta.LoadRelaxed().IsEmpty();
1477
0
}
1478
}  // namespace
1479
1480
// NOTE: GCC might warn about subobject linkage if this is in anon namespace
1481
template <size_t N = 500>
1482
class LoadVarianceStats {
1483
 public:
1484
0
  std::string Report() const {
1485
0
    return "Overall " + PercentStr(positive_count_, samples_) + " (" +
1486
0
           std::to_string(positive_count_) + "/" + std::to_string(samples_) +
1487
0
           "), Min/Max/Window = " + PercentStr(min_, N) + "/" +
1488
0
           PercentStr(max_, N) + "/" + std::to_string(N) +
1489
0
           ", MaxRun{Pos/Neg} = " + std::to_string(max_pos_run_) + "/" +
1490
0
           std::to_string(max_neg_run_);
1491
0
  }
1492
1493
0
  void Add(bool positive) {
1494
0
    recent_[samples_ % N] = positive;
1495
0
    if (positive) {
1496
0
      ++positive_count_;
1497
0
      ++cur_pos_run_;
1498
0
      max_pos_run_ = std::max(max_pos_run_, cur_pos_run_);
1499
0
      cur_neg_run_ = 0;
1500
0
    } else {
1501
0
      ++cur_neg_run_;
1502
0
      max_neg_run_ = std::max(max_neg_run_, cur_neg_run_);
1503
0
      cur_pos_run_ = 0;
1504
0
    }
1505
0
    ++samples_;
1506
0
    if (samples_ >= N) {
1507
0
      size_t count_set = recent_.count();
1508
0
      max_ = std::max(max_, count_set);
1509
0
      min_ = std::min(min_, count_set);
1510
0
    }
1511
0
  }
1512
1513
 private:
1514
  size_t max_ = 0;
1515
  size_t min_ = N;
1516
  size_t positive_count_ = 0;
1517
  size_t samples_ = 0;
1518
  size_t max_pos_run_ = 0;
1519
  size_t cur_pos_run_ = 0;
1520
  size_t max_neg_run_ = 0;
1521
  size_t cur_neg_run_ = 0;
1522
  std::bitset<N> recent_;
1523
1524
0
  static std::string PercentStr(size_t a, size_t b) {
1525
0
    if (b == 0) {
1526
0
      return "??%";
1527
0
    } else {
1528
0
      return std::to_string(uint64_t{100} * a / b) + "%";
1529
0
    }
1530
0
  }
1531
};
1532
1533
template <class Table>
1534
void BaseHyperClockCache<Table>::ReportProblems(
1535
442
    const std::shared_ptr<Logger>& info_log) const {
1536
442
  if (info_log->GetInfoLogLevel() <= InfoLogLevel::DEBUG_LEVEL) {
1537
0
    LoadVarianceStats slot_stats;
1538
0
    uint64_t eviction_effort_exceeded_count = 0;
1539
0
    this->ForEachShard([&](const BaseHyperClockCache<Table>::Shard* shard) {
1540
0
      size_t count = shard->GetTableAddressCount();
1541
0
      for (size_t i = 0; i < count; ++i) {
1542
0
        slot_stats.Add(IsSlotOccupied(*shard->GetTable().HandlePtr(i)));
1543
0
      }
1544
0
      eviction_effort_exceeded_count +=
1545
0
          shard->GetTable().GetEvictionEffortExceededCount();
1546
0
    });
Unexecuted instantiation: rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::FixedHyperClockTable>::ReportProblems(std::__1::shared_ptr<rocksdb::Logger> const&) const::{lambda(rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable> const*)#1}::operator()(rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::FixedHyperClockTable> const*) const
Unexecuted instantiation: rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::AutoHyperClockTable>::ReportProblems(std::__1::shared_ptr<rocksdb::Logger> const&) const::{lambda(rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable> const*)#1}::operator()(rocksdb::clock_cache::ClockCacheShard<rocksdb::clock_cache::AutoHyperClockTable> const*) const
1547
0
    ROCKS_LOG_AT_LEVEL(info_log, InfoLogLevel::DEBUG_LEVEL,
1548
0
                       "Slot occupancy stats: %s", slot_stats.Report().c_str());
1549
0
    ROCKS_LOG_AT_LEVEL(info_log, InfoLogLevel::DEBUG_LEVEL,
1550
0
                       "Eviction effort exceeded: %" PRIu64,
1551
0
                       eviction_effort_exceeded_count);
1552
0
  }
1553
442
}
Unexecuted instantiation: rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::FixedHyperClockTable>::ReportProblems(std::__1::shared_ptr<rocksdb::Logger> const&) const
rocksdb::clock_cache::BaseHyperClockCache<rocksdb::clock_cache::AutoHyperClockTable>::ReportProblems(std::__1::shared_ptr<rocksdb::Logger> const&) const
Line
Count
Source
1535
442
    const std::shared_ptr<Logger>& info_log) const {
1536
442
  if (info_log->GetInfoLogLevel() <= InfoLogLevel::DEBUG_LEVEL) {
1537
0
    LoadVarianceStats slot_stats;
1538
0
    uint64_t eviction_effort_exceeded_count = 0;
1539
0
    this->ForEachShard([&](const BaseHyperClockCache<Table>::Shard* shard) {
1540
0
      size_t count = shard->GetTableAddressCount();
1541
0
      for (size_t i = 0; i < count; ++i) {
1542
0
        slot_stats.Add(IsSlotOccupied(*shard->GetTable().HandlePtr(i)));
1543
0
      }
1544
0
      eviction_effort_exceeded_count +=
1545
0
          shard->GetTable().GetEvictionEffortExceededCount();
1546
0
    });
1547
0
    ROCKS_LOG_AT_LEVEL(info_log, InfoLogLevel::DEBUG_LEVEL,
1548
0
                       "Slot occupancy stats: %s", slot_stats.Report().c_str());
1549
0
    ROCKS_LOG_AT_LEVEL(info_log, InfoLogLevel::DEBUG_LEVEL,
1550
0
                       "Eviction effort exceeded: %" PRIu64,
1551
0
                       eviction_effort_exceeded_count);
1552
0
  }
1553
442
}
1554
1555
void FixedHyperClockCache::ReportProblems(
1556
0
    const std::shared_ptr<Logger>& info_log) const {
1557
0
  BaseHyperClockCache::ReportProblems(info_log);
1558
1559
0
  uint32_t shard_count = GetNumShards();
1560
0
  std::vector<double> predicted_load_factors;
1561
0
  size_t min_recommendation = SIZE_MAX;
1562
0
  ForEachShard([&](const FixedHyperClockCache::Shard* shard) {
1563
0
    AddShardEvaluation(*shard, predicted_load_factors, min_recommendation);
1564
0
  });
1565
1566
0
  if (predicted_load_factors.empty()) {
1567
    // None operating "at limit" -> nothing to report
1568
0
    return;
1569
0
  }
1570
0
  std::sort(predicted_load_factors.begin(), predicted_load_factors.end());
1571
1572
  // First, if the average load factor is within spec, we aren't going to
1573
  // complain about a few shards being out of spec.
1574
  // NOTE: this is only the average among cache shards operating "at limit,"
1575
  // which should be representative of what we care about. It it normal, even
1576
  // desirable, for a cache to operate "at limit" so this should not create
1577
  // selection bias. See AddShardEvaluation().
1578
  // TODO: Consider detecting cases where decreasing the number of shards
1579
  // would be good, e.g. serious imbalance among shards.
1580
0
  double average_load_factor =
1581
0
      std::accumulate(predicted_load_factors.begin(),
1582
0
                      predicted_load_factors.end(), 0.0) /
1583
0
      shard_count;
1584
1585
0
  constexpr double kLowSpecLoadFactor = FixedHyperClockTable::kLoadFactor / 2;
1586
0
  constexpr double kMidSpecLoadFactor =
1587
0
      FixedHyperClockTable::kLoadFactor / 1.414;
1588
0
  if (average_load_factor > FixedHyperClockTable::kLoadFactor) {
1589
    // Out of spec => Consider reporting load factor too high
1590
    // Estimate effective overall capacity loss due to enforcing occupancy limit
1591
0
    double lost_portion = 0.0;
1592
0
    int over_count = 0;
1593
0
    for (double lf : predicted_load_factors) {
1594
0
      if (lf > FixedHyperClockTable::kStrictLoadFactor) {
1595
0
        ++over_count;
1596
0
        lost_portion +=
1597
0
            (lf - FixedHyperClockTable::kStrictLoadFactor) / lf / shard_count;
1598
0
      }
1599
0
    }
1600
    // >= 20% loss -> error
1601
    // >= 10% loss -> consistent warning
1602
    // >= 1% loss -> intermittent warning
1603
0
    InfoLogLevel level = InfoLogLevel::INFO_LEVEL;
1604
0
    bool report = true;
1605
0
    if (lost_portion > 0.2) {
1606
0
      level = InfoLogLevel::ERROR_LEVEL;
1607
0
    } else if (lost_portion > 0.1) {
1608
0
      level = InfoLogLevel::WARN_LEVEL;
1609
0
    } else if (lost_portion > 0.01) {
1610
0
      int report_percent = static_cast<int>(lost_portion * 100.0);
1611
0
      if (Random::GetTLSInstance()->PercentTrue(report_percent)) {
1612
0
        level = InfoLogLevel::WARN_LEVEL;
1613
0
      }
1614
0
    } else {
1615
      // don't report
1616
0
      report = false;
1617
0
    }
1618
0
    if (report) {
1619
0
      ROCKS_LOG_AT_LEVEL(
1620
0
          info_log, level,
1621
0
          "FixedHyperClockCache@%p unable to use estimated %.1f%% capacity "
1622
0
          "because of full occupancy in %d/%u cache shards "
1623
0
          "(estimated_entry_charge too high). "
1624
0
          "Recommend estimated_entry_charge=%zu",
1625
0
          this, lost_portion * 100.0, over_count, (unsigned)shard_count,
1626
0
          min_recommendation);
1627
0
    }
1628
0
  } else if (average_load_factor < kLowSpecLoadFactor) {
1629
    // Out of spec => Consider reporting load factor too low
1630
    // But cautiously because low is not as big of a problem.
1631
1632
    // Only report if highest occupancy shard is also below
1633
    // spec and only if average is substantially out of spec
1634
0
    if (predicted_load_factors.back() < kLowSpecLoadFactor &&
1635
0
        average_load_factor < kLowSpecLoadFactor / 1.414) {
1636
0
      InfoLogLevel level = InfoLogLevel::INFO_LEVEL;
1637
0
      if (average_load_factor < kLowSpecLoadFactor / 2) {
1638
0
        level = InfoLogLevel::WARN_LEVEL;
1639
0
      }
1640
0
      ROCKS_LOG_AT_LEVEL(
1641
0
          info_log, level,
1642
0
          "FixedHyperClockCache@%p table has low occupancy at full capacity. "
1643
0
          "Higher estimated_entry_charge (about %.1fx) would likely improve "
1644
0
          "performance. Recommend estimated_entry_charge=%zu",
1645
0
          this, kMidSpecLoadFactor / average_load_factor, min_recommendation);
1646
0
    }
1647
0
  }
1648
0
}
1649
1650
// =======================================================================
1651
//                             AutoHyperClockCache
1652
// =======================================================================
1653
1654
// See AutoHyperClockTable::length_info_ etc. for how the linear hashing
1655
// metadata is encoded. Here are some example values:
1656
//
1657
// Used length  | min shift  | threshold  | max shift
1658
// 2            | 1          | 0          | 1
1659
// 3            | 1          | 1          | 2
1660
// 4            | 2          | 0          | 2
1661
// 5            | 2          | 1          | 3
1662
// 6            | 2          | 2          | 3
1663
// 7            | 2          | 3          | 3
1664
// 8            | 3          | 0          | 3
1665
// 9            | 3          | 1          | 4
1666
// ...
1667
// Note:
1668
// * min shift = floor(log2(used length))
1669
// * max shift = ceil(log2(used length))
1670
// * used length == (1 << shift) + threshold
1671
// Also, shift=0 is never used in practice, so is reserved for "unset"
1672
1673
namespace {
1674
1675
5.02M
inline int LengthInfoToMinShift(uint64_t length_info) {
1676
5.02M
  int mask_shift = BitwiseAnd(length_info, int{255});
1677
5.02M
  assert(mask_shift <= 63);
1678
5.02M
  assert(mask_shift > 0);
1679
5.02M
  return mask_shift;
1680
5.02M
}
1681
1682
3.73M
inline size_t LengthInfoToThreshold(uint64_t length_info) {
1683
3.73M
  return static_cast<size_t>(length_info >> 8);
1684
3.73M
}
1685
1686
3.37M
inline size_t LengthInfoToUsedLength(uint64_t length_info) {
1687
3.37M
  size_t threshold = LengthInfoToThreshold(length_info);
1688
3.37M
  int shift = LengthInfoToMinShift(length_info);
1689
3.37M
  assert(threshold < (size_t{1} << shift));
1690
3.37M
  size_t used_length = (size_t{1} << shift) + threshold;
1691
3.37M
  assert(used_length >= 2);
1692
3.37M
  return used_length;
1693
3.37M
}
1694
1695
648k
inline uint64_t UsedLengthToLengthInfo(size_t used_length) {
1696
648k
  assert(used_length >= 2);
1697
648k
  int shift = FloorLog2(used_length);
1698
648k
  uint64_t threshold = BottomNBits(used_length, shift);
1699
648k
  uint64_t length_info =
1700
648k
      (uint64_t{threshold} << 8) + static_cast<uint64_t>(shift);
1701
648k
  assert(LengthInfoToUsedLength(length_info) == used_length);
1702
648k
  assert(LengthInfoToMinShift(length_info) == shift);
1703
648k
  assert(LengthInfoToThreshold(length_info) == threshold);
1704
648k
  return length_info;
1705
648k
}
1706
1707
// Avoid potential initialization order race with port::kPageSize
1708
constexpr size_t kPresumedPageSize = 4096;
1709
1710
646k
inline size_t GetStartingLength(size_t capacity) {
1711
646k
  if (capacity > kPresumedPageSize) {
1712
    // Start with one memory page
1713
646k
    return kPresumedPageSize / sizeof(AutoHyperClockTable::HandleImpl);
1714
646k
  } else {
1715
    // Mostly to make unit tests happy
1716
0
    return 4;
1717
0
  }
1718
646k
}
1719
1720
712k
inline size_t GetHomeIndex(uint64_t hash, int shift) {
1721
712k
  return static_cast<size_t>(BottomNBits(hash, shift));
1722
712k
}
1723
1724
inline void GetHomeIndexAndShift(uint64_t length_info, uint64_t hash,
1725
356k
                                 size_t* home, int* shift) {
1726
356k
  int min_shift = LengthInfoToMinShift(length_info);
1727
356k
  size_t threshold = LengthInfoToThreshold(length_info);
1728
356k
  bool extra_shift = GetHomeIndex(hash, min_shift) < threshold;
1729
356k
  *home = GetHomeIndex(hash, min_shift + extra_shift);
1730
356k
  *shift = min_shift + extra_shift;
1731
356k
  assert(*home < LengthInfoToUsedLength(length_info));
1732
356k
}
1733
1734
// Helper function for Lookup
1735
inline bool MatchAndRef(const UniqueId64x2* hashed_key, const ClockHandle& h,
1736
                        int shift = 0, size_t home = 0,
1737
4.47k
                        bool* full_match_or_unknown = nullptr) {
1738
  // Must be at least something to match
1739
4.47k
  assert(hashed_key || shift > 0);
1740
1741
4.47k
  SlotMeta old_meta, new_meta;
1742
  // (Optimistically) increment acquire counter.
1743
4.47k
  auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
1744
4.47k
  h.meta.Apply(add_acquire, &old_meta, &new_meta);
1745
  // Check if it's a referencable (sharable) entry
1746
4.47k
  if (!old_meta.IsShareable()) {
1747
    // For non-sharable states, incrementing the acquire counter has no effect
1748
    // so we don't need to undo it. Furthermore, we cannot safely undo
1749
    // it because we did not acquire a read reference to lock the
1750
    // entry in a Shareable state.
1751
0
    if (full_match_or_unknown) {
1752
0
      *full_match_or_unknown = true;
1753
0
    }
1754
0
    return false;
1755
0
  }
1756
  // Else acquired a read reference
1757
4.47k
  assert(new_meta.GetRefcount() > 0);
1758
4.47k
  if (hashed_key && h.hashed_key == *hashed_key &&
1759
0
      LIKELY(old_meta.IsVisible())) {
1760
    // Match on full key, visible
1761
0
    if (full_match_or_unknown) {
1762
0
      *full_match_or_unknown = true;
1763
0
    }
1764
0
    return true;
1765
4.47k
  } else if (shift > 0 && home == BottomNBits(h.hashed_key[1], shift)) {
1766
    // NOTE: upper 32 bits of hashed_key[0] is used for sharding
1767
    // Match on home address, possibly invisible
1768
4.47k
    if (full_match_or_unknown) {
1769
4.47k
      *full_match_or_unknown = false;
1770
4.47k
    }
1771
4.47k
    return true;
1772
4.47k
  } else {
1773
    // Mismatch. Pretend we never took the reference
1774
1
    Unref(h);
1775
1
    if (full_match_or_unknown) {
1776
0
      *full_match_or_unknown = false;
1777
0
    }
1778
1
    return false;
1779
1
  }
1780
4.47k
}
1781
1782
using NextWithShift = AutoHyperClockTable::HandleImpl::NextWithShift;
1783
1784
// Assumes a chain rewrite lock prevents concurrent modification of
1785
// these chain pointers
1786
void UpgradeShiftsOnRange(AutoHyperClockTable::HandleImpl* arr,
1787
                          size_t& frontier,
1788
                          NextWithShift stop_before_or_new_tail, int old_shift,
1789
1.30k
                          int new_shift) {
1790
1.30k
  assert(frontier != SIZE_MAX);
1791
1.30k
  assert(new_shift == old_shift + 1);
1792
1.30k
  (void)old_shift;
1793
1.30k
  (void)new_shift;
1794
1.55k
  for (;;) {
1795
1.55k
    NextWithShift next_with_shift = arr[frontier].chain_next_with_shift.Load();
1796
1.55k
    assert(next_with_shift.GetShift() == old_shift);
1797
1.55k
    if (next_with_shift == stop_before_or_new_tail) {
1798
      // Stopping at entry with pointer matching "stop before"
1799
227
      assert(!next_with_shift.IsEnd());
1800
227
      return;
1801
227
    }
1802
1.33k
    if (next_with_shift.IsEnd()) {
1803
      // Also update tail to new tail
1804
1.07k
      assert(stop_before_or_new_tail.IsEnd());
1805
1.07k
      arr[frontier].chain_next_with_shift.Store(stop_before_or_new_tail);
1806
      // Mark nothing left to upgrade
1807
1.07k
      frontier = SIZE_MAX;
1808
1.07k
      return;
1809
1.07k
    }
1810
    // Next is another entry to process, so upgrade and advance frontier
1811
255
    arr[frontier].chain_next_with_shift.Apply(
1812
255
        NextWithShift::Shift::PlusTransformPromiseNoOverflow(1U));
1813
255
    assert(next_with_shift.GetShift() + 1 == new_shift);
1814
255
    frontier = next_with_shift.GetNext();
1815
255
  }
1816
1.30k
}
1817
1818
649k
size_t CalcOccupancyLimit(size_t used_length) {
1819
649k
  return static_cast<size_t>(used_length * AutoHyperClockTable::kMaxLoadFactor +
1820
649k
                             0.999);
1821
649k
}
1822
1823
}  // namespace
1824
1825
// An RAII wrapper for locking a chain of entries (flag bit on the head)
1826
// so that there is only one thread allowed to remove entries from the
1827
// chain, or to rewrite it by splitting for Grow. Without the lock,
1828
// all lookups and insertions at the head can proceed wait-free.
1829
// The class also provides functions for safely manipulating the head pointer
1830
// while holding the lock--or wanting to should it become non-empty.
1831
//
1832
// The flag bits on the head are such that the head cannot be locked if it
1833
// is an empty chain, so that a "blind" FetchOr will try to lock a non-empty
1834
// chain but have no effect on an empty chain. When a potential rewrite
1835
// operation see an empty head pointer, there is no need to lock as the
1836
// operation is a no-op. However, there are some cases such as CAS-update
1837
// where locking might be required after initially not being needed, if the
1838
// operation is forced to revisit the head pointer.
1839
class AutoHyperClockTable::ChainRewriteLock {
1840
 public:
1841
  using HandleImpl = AutoHyperClockTable::HandleImpl;
1842
1843
  // Acquire lock if head of h is not an end
1844
  explicit ChainRewriteLock(HandleImpl* h, RelaxedAtomic<uint64_t>& yield_count)
1845
19.2k
      : head_ptr_(&h->head_next_with_shift) {
1846
19.2k
    Acquire(yield_count);
1847
19.2k
  }
1848
1849
  // RAII wrap existing lock held (or end)
1850
  explicit ChainRewriteLock(HandleImpl* h,
1851
                            RelaxedAtomic<uint64_t>& /*yield_count*/,
1852
                            NextWithShift already_locked_or_end)
1853
1.97k
      : head_ptr_(&h->head_next_with_shift) {
1854
1.97k
    saved_head_ = already_locked_or_end;
1855
    // already locked or end
1856
1.97k
    assert(saved_head_.IsLocked());
1857
1.97k
  }
1858
1859
21.2k
  ~ChainRewriteLock() {
1860
21.2k
    if (!IsEnd()) {
1861
      // Release lock
1862
3.23k
      NextWithShift old;
1863
3.23k
      head_ptr_->Apply(NextWithShift::LockedFlag::ClearTransform(), &old);
1864
3.23k
      assert(old.IsLockedNotEnd());
1865
3.23k
    }
1866
21.2k
  }
1867
1868
0
  void Reset(HandleImpl* h, RelaxedAtomic<uint64_t>& yield_count) {
1869
0
    this->~ChainRewriteLock();
1870
0
    new (this) ChainRewriteLock(h, yield_count);
1871
0
  }
1872
1873
  // Expected current state, assuming no parallel updates.
1874
54.8k
  NextWithShift GetSavedHead() const { return saved_head_; }
1875
1876
  bool CasUpdate(NextWithShift next_with_shift,
1877
18.2k
                 RelaxedAtomic<uint64_t>& yield_count) {
1878
18.2k
    NextWithShift new_head =
1879
18.2k
        next_with_shift.With<NextWithShift::LockedFlag>(true);
1880
18.2k
    NextWithShift expected = GetSavedHead();
1881
18.2k
    bool success = head_ptr_->CasStrong(expected, new_head);
1882
18.2k
    if (success) {
1883
      // Ensure IsEnd() is kept up-to-date, including for dtor
1884
18.2k
      saved_head_ = new_head;
1885
18.2k
    } else {
1886
      // Parallel update to head, such as Insert()
1887
0
      if (IsEnd()) {
1888
        // Didn't previously hold a lock
1889
0
        if (expected.IsEnd()) {
1890
          // Still don't need to
1891
0
          saved_head_ = expected;
1892
0
        } else {
1893
          // Need to acquire lock before proceeding
1894
0
          Acquire(yield_count);
1895
0
        }
1896
0
      } else {
1897
        // Parallel update must preserve our lock
1898
0
        assert(expected.IsLockedNotEnd());
1899
0
        saved_head_ = expected;
1900
0
      }
1901
0
    }
1902
18.2k
    return success;
1903
18.2k
  }
1904
1905
38.5k
  bool IsEnd() const { return saved_head_.IsEnd(); }
1906
1907
 private:
1908
19.2k
  void Acquire(RelaxedAtomic<uint64_t>& yield_count) {
1909
19.2k
    for (;;) {
1910
      // Acquire removal lock on the chain
1911
19.2k
      NextWithShift old_head;
1912
19.2k
      head_ptr_->Apply(NextWithShift::LockedFlag::SetTransform(), &old_head,
1913
19.2k
                       &saved_head_);
1914
19.2k
      if (!old_head.IsLockedNotEnd()) {
1915
        // Either acquired the lock or lock not needed (end)
1916
19.2k
        assert(old_head.IsEnd() == old_head.IsLocked());
1917
19.2k
        break;
1918
19.2k
      }
1919
      // NOTE: one of the few yield-wait loops, which is rare enough in practice
1920
      // for its performance to be insignificant. (E.g. using C++20 atomic
1921
      // wait/notify would likely be worse because of wasted notify costs.)
1922
0
      yield_count.FetchAddRelaxed(1);
1923
0
      std::this_thread::yield();
1924
0
    }
1925
19.2k
  }
1926
1927
  BitFieldsAtomic<NextWithShift>* head_ptr_;
1928
  NextWithShift saved_head_;
1929
};
1930
1931
AutoHyperClockTable::AutoHyperClockTable(
1932
    size_t capacity, bool strict_capacity_limit,
1933
    CacheMetadataChargePolicy metadata_charge_policy,
1934
    MemoryAllocator* allocator,
1935
    const Cache::EvictionCallback* eviction_callback, const uint32_t* hash_seed,
1936
    const Opts& opts)
1937
646k
    : BaseClockTable(capacity, strict_capacity_limit, opts.eviction_effort_cap,
1938
646k
                     metadata_charge_policy, allocator, eviction_callback,
1939
646k
                     hash_seed),
1940
646k
      array_(MemMapping::AllocateLazyZeroed(
1941
646k
          sizeof(HandleImpl) * CalcMaxUsableLength(capacity,
1942
646k
                                                   opts.min_avg_value_size,
1943
646k
                                                   metadata_charge_policy))),
1944
646k
      length_info_(UsedLengthToLengthInfo(GetStartingLength(capacity))),
1945
646k
      occupancy_limit_(
1946
646k
          CalcOccupancyLimit(LengthInfoToUsedLength(length_info_.Load()))),
1947
646k
      grow_frontier_(GetTableSize()),
1948
646k
      clock_pointer_mask_(
1949
646k
          BottomNBits(UINT64_MAX, LengthInfoToMinShift(length_info_.Load()))) {
1950
646k
  if (array_.Get() == nullptr) {
1951
0
    fprintf(stderr,
1952
0
            "Anonymous mmap for RocksDB HyperClockCache failed. Aborting.\n");
1953
0
    std::terminate();
1954
0
  }
1955
646k
  if (metadata_charge_policy ==
1956
646k
      CacheMetadataChargePolicy::kFullChargeCacheMetadata) {
1957
    // NOTE: ignoring page boundaries for simplicity
1958
646k
    usage_.FetchAddRelaxed(size_t{GetTableSize()} * sizeof(HandleImpl));
1959
646k
  }
1960
1961
646k
  static_assert(sizeof(HandleImpl) == 64U,
1962
646k
                "Expecting size / alignment with common cache line size");
1963
1964
  // Populate head pointers
1965
646k
  uint64_t length_info = length_info_.Load();
1966
646k
  int min_shift = LengthInfoToMinShift(length_info);
1967
646k
  int max_shift = min_shift + 1;
1968
646k
  size_t major = uint64_t{1} << min_shift;
1969
646k
  size_t used_length = GetTableSize();
1970
1971
646k
  assert(major <= used_length);
1972
646k
  assert(used_length <= major * 2);
1973
1974
  // Initialize the initial usable set of slots. This slightly odd iteration
1975
  // order makes it easier to get the correct shift amount on each head.
1976
42.0M
  for (size_t i = 0; i < major; ++i) {
1977
#ifndef NDEBUG
1978
    int shift;
1979
    size_t home;
1980
#endif
1981
41.4M
    if (major + i < used_length) {
1982
0
      array_[i].head_next_with_shift.StoreRelaxed(
1983
0
          NextWithShift::MakeEnd(i, max_shift));
1984
0
      array_[major + i].head_next_with_shift.StoreRelaxed(
1985
0
          NextWithShift::MakeEnd(major + i, max_shift));
1986
#ifndef NDEBUG  // Extra invariant checking
1987
      GetHomeIndexAndShift(length_info, i, &home, &shift);
1988
      assert(home == i);
1989
      assert(shift == max_shift);
1990
      GetHomeIndexAndShift(length_info, major + i, &home, &shift);
1991
      assert(home == major + i);
1992
      assert(shift == max_shift);
1993
#endif
1994
41.4M
    } else {
1995
41.4M
      array_[i].head_next_with_shift.StoreRelaxed(
1996
41.4M
          NextWithShift::MakeEnd(i, min_shift));
1997
#ifndef NDEBUG  // Extra invariant checking
1998
      GetHomeIndexAndShift(length_info, i, &home, &shift);
1999
      assert(home == i);
2000
      assert(shift == min_shift);
2001
      GetHomeIndexAndShift(length_info, major + i, &home, &shift);
2002
      assert(home == i);
2003
      assert(shift == min_shift);
2004
#endif
2005
41.4M
    }
2006
41.4M
  }
2007
646k
}
2008
2009
646k
AutoHyperClockTable::~AutoHyperClockTable() {
2010
  // As usual, destructor assumes there are no references or active operations
2011
  // on any slot/element in the table.
2012
2013
  // It's possible that there were not enough Insert() after final concurrent
2014
  // Grow to ensure length_info_ (published GetTableSize()) is fully up to
2015
  // date. Probe for first unused slot to ensure we see the whole structure.
2016
646k
  size_t used_end = GetTableSize();
2017
646k
  while (used_end < array_.Count() &&
2018
646k
         array_[used_end].head_next_with_shift.LoadRelaxed() !=
2019
646k
             HandleImpl::kUnusedMarker) {
2020
0
    used_end++;
2021
0
  }
2022
  // This check can be extra expensive for a cache that is just created,
2023
  // maybe used for a small number of entries, as in a unit test, and then
2024
  // destroyed. Only do this in rare modes. REVISED: Don't scan the whole mmap,
2025
  // just a reasonable frontier past what we expect to have written.
2026
#ifdef MUST_FREE_HEAP_ALLOCATIONS
2027
  for (size_t i = used_end; i < array_.Count() && i < used_end + 64U; i++) {
2028
    assert(array_[i].head_next_with_shift.LoadRelaxed() ==
2029
           HandleImpl::kUnusedMarker);
2030
    assert(array_[i].chain_next_with_shift.LoadRelaxed() ==
2031
           HandleImpl::kUnusedMarker);
2032
    assert(array_[i].meta.LoadRelaxed() == SlotMeta{});
2033
  }
2034
#endif          // MUST_FREE_HEAP_ALLOCATIONS
2035
#ifndef NDEBUG  // Extra invariant checking
2036
  std::vector<bool> was_populated(used_end);
2037
  std::vector<bool> was_pointed_to(used_end);
2038
#endif  // !NDEBUG
2039
42.0M
  for (size_t i = 0; i < used_end; i++) {
2040
41.4M
    HandleImpl& h = array_[i];
2041
41.4M
    SlotMeta meta = h.meta.LoadRelaxed();
2042
41.4M
    if (meta.IsShareable()) {
2043
      // NOTE: Reaching here invisible is rare but possible
2044
89.7k
      assert(meta.GetRefcount() == 0);
2045
89.7k
      h.FreeData(allocator_);
2046
#ifndef NDEBUG  // Extra invariant checking
2047
      usage_.FetchSubRelaxed(h.total_charge);
2048
      occupancy_.FetchSubRelaxed(1U);
2049
      was_populated[i] = true;
2050
      if (!h.chain_next_with_shift.LoadRelaxed().IsEnd()) {
2051
        assert(!h.chain_next_with_shift.LoadRelaxed().IsLocked());
2052
        size_t next = h.chain_next_with_shift.LoadRelaxed().GetNext();
2053
        assert(!was_pointed_to[next]);
2054
        was_pointed_to[next] = true;
2055
      }
2056
#endif  // !NDEBUG
2057
41.3M
    } else {
2058
      // Should be no transient "under construction" states unless a thread
2059
      // was killed or we are being destructed while another thread is still
2060
      // operating on the structure
2061
41.3M
      assert(meta.IsEmpty());
2062
41.3M
    }
2063
#ifndef NDEBUG  // Extra invariant checking
2064
    if (!h.head_next_with_shift.LoadRelaxed().IsEnd()) {
2065
      size_t next = h.head_next_with_shift.LoadRelaxed().GetNext();
2066
      assert(!was_pointed_to[next]);
2067
      was_pointed_to[next] = true;
2068
    }
2069
#endif  // !NDEBUG
2070
41.4M
  }
2071
#ifndef NDEBUG  // Extra invariant checking
2072
  // This check is not perfect, but should detect most reasonable cases
2073
  // of abandonned or floating entries, etc.  (A floating cycle would not
2074
  // be reported as bad.)
2075
  for (size_t i = 0; i < used_end; i++) {
2076
    if (was_populated[i]) {
2077
      assert(was_pointed_to[i]);
2078
    } else {
2079
      assert(!was_pointed_to[i]);
2080
    }
2081
  }
2082
#endif  // !NDEBUG
2083
2084
  // Metadata charging only follows the published table size
2085
646k
  assert(usage_.LoadRelaxed() == 0 ||
2086
646k
         usage_.LoadRelaxed() == GetTableSize() * sizeof(HandleImpl));
2087
646k
  assert(occupancy_.LoadRelaxed() == 0);
2088
646k
}
2089
2090
2.61M
size_t AutoHyperClockTable::GetTableSize() const {
2091
2.61M
  return LengthInfoToUsedLength(length_info_.Load());
2092
2.61M
}
2093
2094
0
size_t AutoHyperClockTable::GetOccupancyLimit() const {
2095
0
  return occupancy_limit_.LoadRelaxed();
2096
0
}
2097
2098
107k
void AutoHyperClockTable::StartInsert(InsertState& state) {
2099
107k
  state.saved_length_info = length_info_.Load();
2100
107k
}
2101
2102
// Because we have linked lists, bugs or even hardware errors can make it
2103
// possible to create a cycle, which would lead to infinite loop.
2104
// Furthermore, when we have retry cases in the code, we want to be sure
2105
// these are not (and do not become) spin-wait loops. Given the assumption
2106
// of quality hashing and the infeasibility of consistently recurring
2107
// concurrent modifications to an entry or chain, we can safely bound the
2108
// number of loop iterations in feasible operation, whether following chain
2109
// pointers or retrying with some backtracking. A smaller limit is used for
2110
// stress testing, to detect potential issues such as cycles or spin-waits,
2111
// and a larger limit is used to break cycles should they occur in production.
2112
#define CHECK_TOO_MANY_ITERATIONS(i) \
2113
328k
  {                                  \
2114
328k
    assert(i < 768);                 \
2115
328k
    if (UNLIKELY(i >= 4096)) {       \
2116
0
      std::terminate();              \
2117
0
    }                                \
2118
328k
  }
2119
2120
bool AutoHyperClockTable::GrowIfNeeded(size_t new_occupancy,
2121
107k
                                       InsertState& state) {
2122
  // new_occupancy has taken into account other threads that are also trying
2123
  // to insert, so as soon as we see sufficient *published* usable size, we
2124
  // can declare success even if we aren't the one that grows the table.
2125
  // However, there's an awkward state where other threads own growing the
2126
  // table to sufficient usable size, but the udpated size is not yet
2127
  // published. If we wait, then that likely slows the ramp-up cache
2128
  // performance. If we unblock ourselves by ensuring we grow by at least one
2129
  // slot, we could technically overshoot required size by number of parallel
2130
  // threads accessing block cache. On balance considering typical cases and
2131
  // the modest consequences of table being slightly too large, the latter
2132
  // seems preferable.
2133
  //
2134
  // So if the published occupancy limit is too small, we unblock ourselves
2135
  // by committing to growing the table by at least one slot. Also note that
2136
  // we might need to grow more than once to actually increase the occupancy
2137
  // limit (due to max load factor < 1.0)
2138
2139
109k
  while (UNLIKELY(new_occupancy > occupancy_limit_.LoadRelaxed())) {
2140
    // At this point we commit the thread to growing unless we've reached the
2141
    // limit (returns false).
2142
1.97k
    if (!Grow(state)) {
2143
0
      return false;
2144
0
    }
2145
1.97k
  }
2146
  // Success (didn't need to grow, or did successfully)
2147
107k
  return true;
2148
107k
}
2149
2150
1.97k
bool AutoHyperClockTable::Grow(InsertState& state) {
2151
  // Allocate the next grow slot
2152
1.97k
  size_t grow_home = grow_frontier_.FetchAddRelaxed(1);
2153
1.97k
  if (grow_home >= array_.Count()) {
2154
    // Can't grow any more.
2155
    // (Tested by unit test ClockCacheTest/Limits)
2156
    // Make sure we don't overflow grow_frontier_ by reaching here repeatedly
2157
0
    grow_frontier_.StoreRelaxed(array_.Count());
2158
0
    return false;
2159
0
  }
2160
#ifdef COERCE_CONTEXT_SWITCH
2161
  // This is useful in reproducing concurrency issues in Grow()
2162
  while (Random::GetTLSInstance()->OneIn(2)) {
2163
    std::this_thread::yield();
2164
  }
2165
#endif
2166
  // Basically, to implement https://en.wikipedia.org/wiki/Linear_hashing
2167
  // entries that belong in a new chain starting at grow_home will be
2168
  // split off from the chain starting at old_home, which is computed here.
2169
1.97k
  int old_shift = FloorLog2(grow_home);
2170
1.97k
  size_t old_home = BottomNBits(grow_home, old_shift);
2171
1.97k
  assert(old_home + (size_t{1} << old_shift) == grow_home);
2172
2173
  // Wait here to ensure any Grow operations that would directly feed into
2174
  // this one are finished, though the full waiting actually completes in
2175
  // acquiring the rewrite lock for old_home in SplitForGrow. Here we ensure
2176
  // the expected shift amount has been reached, and there we ensure the
2177
  // chain rewrite lock has been released.
2178
1.97k
  size_t old_old_home = BottomNBits(grow_home, old_shift - 1);
2179
1.97k
  for (;;) {
2180
1.97k
    NextWithShift old_old_head =
2181
1.97k
        array_[old_old_home].head_next_with_shift.Load();
2182
1.97k
    if (old_old_head.GetShift() >= old_shift) {
2183
1.97k
      if (!old_old_head.IsLockedNotEnd()) {
2184
1.97k
        break;
2185
1.97k
      }
2186
1.97k
    }
2187
    // NOTE: one of the few yield-wait loops, which is rare enough in practice
2188
    // for its performance to be insignificant.
2189
0
    yield_count_.FetchAddRelaxed(1);
2190
0
    std::this_thread::yield();
2191
0
  }
2192
2193
  // Do the dirty work of splitting the chain, including updating heads and
2194
  // chain nexts for new shift amounts.
2195
1.97k
  SplitForGrow(grow_home, old_home, old_shift);
2196
2197
  // length_info_ can be updated any time after the new shift amount is
2198
  // published to both heads, potentially before the end of SplitForGrow.
2199
  // But we also can't update length_info_ until the previous Grow operation
2200
  // (with grow_home := this grow_home - 1) has published the new shift amount
2201
  // to both of its heads. However, we don't want to artificially wait here
2202
  // on that Grow that is otherwise irrelevant.
2203
  //
2204
  // We could have each Grow operation advance length_info_ here as far as it
2205
  // can without waiting, by checking for updated shift on the corresponding
2206
  // old home and also stopping at an empty head value for possible grow_home.
2207
  // However, this could increase CPU cache line sharing and in 1/64 cases
2208
  // bring in an extra page from our mmap.
2209
  //
2210
  // Instead, part of the strategy is delegated to DoInsert():
2211
  // * Here we try to bring length_info_ up to date with this grow_home as
2212
  // much as we can without waiting. It will fall short if a previous Grow
2213
  // is still between reserving the grow slot and making the first big step
2214
  // to publish the new shift amount.
2215
  // * To avoid length_info_ being perpetually out-of-date (for a small number
2216
  // of heads) after our last Grow, we do the same when Insert has to "fall
2217
  // forward" due to length_info_ being out-of-date.
2218
1.97k
  CatchUpLengthInfoNoWait(grow_home);
2219
2220
  // See usage in DoInsert()
2221
1.97k
  state.likely_empty_slot = grow_home;
2222
2223
  // Success
2224
1.97k
  return true;
2225
1.97k
}
2226
2227
// See call in Grow()
2228
void AutoHyperClockTable::CatchUpLengthInfoNoWait(
2229
2.01k
    size_t known_usable_grow_home) {
2230
2.01k
  uint64_t current_length_info = length_info_.Load();
2231
2.01k
  size_t published_usable_size = LengthInfoToUsedLength(current_length_info);
2232
4.00k
  while (published_usable_size <= known_usable_grow_home) {
2233
    // For when published_usable_size was grow_home
2234
2.00k
    size_t next_usable_size = published_usable_size + 1;
2235
2.00k
    uint64_t next_length_info = UsedLengthToLengthInfo(next_usable_size);
2236
2237
    // known_usable_grow_home is known to be ready for Lookup/Insert with
2238
    // the new shift amount, but between that and published usable size, we
2239
    // need to check.
2240
2.00k
    if (published_usable_size < known_usable_grow_home) {
2241
41
      int old_shift = FloorLog2(next_usable_size - 1);
2242
41
      size_t old_home = BottomNBits(published_usable_size, old_shift);
2243
41
      int shift = array_[old_home].head_next_with_shift.Load().GetShift();
2244
41
      if (shift <= old_shift) {
2245
        // Not ready
2246
16
        break;
2247
16
      }
2248
41
    }
2249
    // CAS update length_info_. This only moves in one direction, so if CAS
2250
    // fails, someone else made progress like we are trying, and we can just
2251
    // pick up the new value and keep going as appropriate.
2252
1.98k
    if (length_info_.CasStrong(current_length_info, next_length_info)) {
2253
1.97k
      current_length_info = next_length_info;
2254
      // Update usage_ if metadata charge policy calls for it
2255
1.97k
      if (metadata_charge_policy_ ==
2256
1.97k
          CacheMetadataChargePolicy::kFullChargeCacheMetadata) {
2257
        // NOTE: ignoring page boundaries for simplicity
2258
1.97k
        usage_.FetchAddRelaxed(sizeof(HandleImpl));
2259
1.97k
      }
2260
1.97k
    }
2261
1.98k
    published_usable_size = LengthInfoToUsedLength(current_length_info);
2262
1.98k
  }
2263
2264
  // After updating lengh_info_ we can update occupancy_limit_,
2265
  // allowing for later operations to update it before us.
2266
  // Note: there is no AcqRelAtomic max operation, so we have to use a CAS loop
2267
2.01k
  size_t old_occupancy_limit = occupancy_limit_.LoadRelaxed();
2268
2.01k
  size_t new_occupancy_limit = CalcOccupancyLimit(published_usable_size);
2269
2.01k
  while (old_occupancy_limit < new_occupancy_limit) {
2270
1.17k
    if (occupancy_limit_.CasWeakRelaxed(old_occupancy_limit,
2271
1.17k
                                        new_occupancy_limit)) {
2272
1.17k
      break;
2273
1.17k
    }
2274
1.17k
  }
2275
2.01k
}
2276
2277
void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
2278
1.97k
                                       int old_shift) {
2279
1.97k
  int new_shift = old_shift + 1;
2280
1.97k
  HandleImpl* const arr = array_.Get();
2281
2282
  // We implement a somewhat complicated splitting algorithm to ensure that
2283
  // entries are always wait-free visible to Lookup, without Lookup needing
2284
  // to double-check length_info_ to ensure every potentially relevant
2285
  // existing entry is seen. This works step-by-step, carefully sharing
2286
  // unmigrated parts of the chain between the source chain and the new
2287
  // destination chain. This means that Lookup might see a partially migrated
2288
  // chain so has to take that into consideration when checking that it hasn't
2289
  // "jumped off" its intended chain (due to a parallel modification to an
2290
  // "under (de)construction" entry that was found on the chain but has
2291
  // been reassigned).
2292
  //
2293
  // We use a "rewrite lock" on the source and desination chains to exclude
2294
  // removals from those, and we have a prior waiting step that ensures any Grow
2295
  // operations feeding into this one have completed. But this process does have
2296
  // to gracefully handle concurrent insertions to the head of the source chain,
2297
  // and once marked ready, the destination chain.
2298
  //
2299
  // With those considerations, the migration starts with one "big step,"
2300
  // potentially with retries to deal with insertions in parallel. Part of the
2301
  // big step is to mark the two chain heads as updated with the new shift
2302
  // amount, which redirects Lookups to the appropriate new chain.
2303
  //
2304
  // After that big step that updates the heads, the rewrite lock makes it
2305
  // relatively easy to deal with the rest of the migration. Big
2306
  // simplifications come from being able to read the hashed_key of each
2307
  // entry on the chain without needing to hold a read reference, and
2308
  // from never "jumping our to another chain." Concurrent insertions only
2309
  // happen at the chain head, which is outside of what is left to migrate.
2310
  //
2311
  // A series of smaller steps finishes splitting apart the existing chain into
2312
  // two distinct chains, followed by some steps to fully commit the result.
2313
  //
2314
  // Except for trivial cases in which all entries (or remaining entries)
2315
  // on the input chain go to one output chain, there is an important invariant
2316
  // after each step of migration, including after the initial "big step":
2317
  // For each output chain, the "zero chain" (new hash bit is zero) and the
2318
  // "one chain" (new hash bit is one) we have a "frontier" entry marking the
2319
  // boundary between what has been migrated and what has not. One of the
2320
  // frontiers is along the old chain after the other, and all entries between
2321
  // them are for the same target chain as the earlier frontier. Thus, the
2322
  // chains share linked list tails starting at the latter frontier. All
2323
  // pointers from the new head locations to the frontier entries are marked
2324
  // with the new shift amount, while all pointers after the frontiers use the
2325
  // old shift amount.
2326
  //
2327
  // And after each step there is a strengthening step to reach a stronger
2328
  // invariant: the frontier earlier in the original chain is advanced to be
2329
  // immediately before the other frontier.
2330
  //
2331
  // Consider this original input chain,
2332
  //
2333
  // OldHome  -Old-> A0 -Old-> B0 -Old-> A1 -Old-> C0 -Old-> OldHome(End)
2334
  // GrowHome (empty)
2335
  //
2336
  // == BIG STEP ==
2337
  // The initial big step finds the first entry that will be on the each
2338
  // output chain (in this case A0 and A1). We use brackets ([]) to mark them
2339
  // as our prospective frontiers.
2340
  //
2341
  // OldHome  -Old-> [A0] -Old-> B0 -Old-> [A1] -Old-> C0 -Old-> OldHome(End)
2342
  // GrowHome (empty)
2343
  //
2344
  // Next we speculatively update grow_home head to point to the first entry for
2345
  // the one chain. This will not be used by Lookup until the head at old_home
2346
  // uses the new shift amount.
2347
  //
2348
  // OldHome  -Old-> [A0] -Old-> B0 -Old-> [A1] -Old-> C0 -Old-> OldHome(End)
2349
  // GrowHome --------------New------------/
2350
  //
2351
  // Observe that if Lookup were to use the new head at GrowHome, it would be
2352
  // able to find all relevant entries. Finishing the initial big step
2353
  // requires a CAS (compare_exchange) of the OldHome head because there
2354
  // might have been parallel insertions there, in which case we roll back
2355
  // and try again. (We might need to point GrowHome head differently.)
2356
  //
2357
  // OldHome  -New-> [A0] -Old-> B0 -Old-> [A1] -Old-> C0 -Old-> OldHome(End)
2358
  // GrowHome --------------New------------/
2359
  //
2360
  // Upgrading the OldHome head pointer with the new shift amount, with a
2361
  // compare_exchange, completes the initial big step, with [A0] as zero
2362
  // chain frontier and [A1] as one chain frontier. Links before the frontiers
2363
  // use the new shift amount and links after use the old shift amount.
2364
  // == END BIG STEP==
2365
  // == STRENGTHENING ==
2366
  // Zero chain frontier is advanced to [B0] (immediately before other
2367
  // frontier) by updating pointers with new shift amounts.
2368
  //
2369
  // OldHome  -New-> A0 -New-> [B0] -Old-> [A1] -Old-> C0 -Old-> OldHome(End)
2370
  // GrowHome -------------New-----------/
2371
  //
2372
  // == END STRENGTHENING ==
2373
  // == SMALL STEP #1 ==
2374
  // From the strong invariant state, we need to find the next entry for
2375
  // the new chain with the earlier frontier. In this case, we need to find
2376
  // the next entry for the zero chain that comes after [B0], which in this
2377
  // case is C0. This will be our next zero chain frontier, at least under
2378
  // the weak invariant. To get there, we simply update the link between
2379
  // the current two frontiers to skip over the entries irreleveant to the
2380
  // ealier frontier chain. In this case, the zero chain skips over A1. As a
2381
  // result, he other chain is now the "earlier."
2382
  //
2383
  // OldHome  -New-> A0 -New-> B0 -New-> [C0] -Old-> OldHome(End)
2384
  // GrowHome -New-> [A1] ------Old-----/
2385
  //
2386
  // == END SMALL STEP #1 ==
2387
  //
2388
  // Repeating the cycle and end handling is not as interesting.
2389
2390
  // Acquire rewrite lock on zero chain (if it's non-empty)
2391
1.97k
  ChainRewriteLock zero_head_lock(&arr[old_home], yield_count_);
2392
2393
  // Used for locking the one chain below
2394
1.97k
  NextWithShift saved_one_head;
2395
  // One head has not been written to
2396
1.97k
  assert(arr[grow_home].head_next_with_shift.Load() ==
2397
1.97k
         HandleImpl::kUnusedMarker);
2398
2399
  // old_home will also the head of the new "zero chain" -- all entries in the
2400
  // "from" chain whose next hash bit is 0. grow_home will be head of the new
2401
  // "one chain".
2402
2403
  // For these, SIZE_MAX is like nullptr (unknown)
2404
1.97k
  size_t zero_chain_frontier = SIZE_MAX;
2405
1.97k
  size_t one_chain_frontier = SIZE_MAX;
2406
1.97k
  size_t cur = SIZE_MAX;
2407
2408
  // Set to 0 (zero chain frontier earlier), 1 (one chain), or -1 (unknown)
2409
1.97k
  int chain_frontier_first = -1;
2410
2411
  // Might need to retry initial update of heads
2412
1.97k
  for (int i = 0;; ++i) {
2413
1.97k
    CHECK_TOO_MANY_ITERATIONS(i);
2414
1.97k
    assert(zero_chain_frontier == SIZE_MAX);
2415
1.97k
    assert(one_chain_frontier == SIZE_MAX);
2416
1.97k
    assert(cur == SIZE_MAX);
2417
1.97k
    assert(chain_frontier_first == -1);
2418
2419
1.97k
    NextWithShift next_with_shift = zero_head_lock.GetSavedHead();
2420
2421
    // Find a single representative for each target chain, or scan the whole
2422
    // chain if some target chain has no representative.
2423
3.28k
    for (;; ++i) {
2424
3.28k
      CHECK_TOO_MANY_ITERATIONS(i);
2425
2426
      // Loop invariants
2427
3.28k
      assert((chain_frontier_first < 0) == (zero_chain_frontier == SIZE_MAX &&
2428
3.28k
                                            one_chain_frontier == SIZE_MAX));
2429
3.28k
      assert((cur == SIZE_MAX) == (zero_chain_frontier == SIZE_MAX &&
2430
3.28k
                                   one_chain_frontier == SIZE_MAX));
2431
2432
3.28k
      assert(next_with_shift.GetShift() == old_shift);
2433
2434
      // Check for end of original chain
2435
3.28k
      if (next_with_shift.IsEnd()) {
2436
1.77k
        cur = SIZE_MAX;
2437
1.77k
        break;
2438
1.77k
      }
2439
2440
      // next_with_shift is not End
2441
1.50k
      cur = next_with_shift.GetNext();
2442
2443
1.50k
      if (BottomNBits(arr[cur].hashed_key[1], new_shift) == old_home) {
2444
        // Entry for zero chain
2445
784
        if (zero_chain_frontier == SIZE_MAX) {
2446
660
          zero_chain_frontier = cur;
2447
660
          if (one_chain_frontier != SIZE_MAX) {
2448
            // Ready to update heads
2449
107
            break;
2450
107
          }
2451
          // Nothing yet for one chain
2452
553
          chain_frontier_first = 0;
2453
553
        }
2454
784
      } else {
2455
724
        assert(BottomNBits(arr[cur].hashed_key[1], new_shift) == grow_home);
2456
        // Entry for one chain
2457
724
        if (one_chain_frontier == SIZE_MAX) {
2458
621
          one_chain_frontier = cur;
2459
621
          if (zero_chain_frontier != SIZE_MAX) {
2460
            // Ready to update heads
2461
99
            break;
2462
99
          }
2463
          // Nothing yet for zero chain
2464
522
          chain_frontier_first = 1;
2465
522
        }
2466
724
      }
2467
2468
1.30k
      next_with_shift = arr[cur].chain_next_with_shift.Load();
2469
1.30k
    }
2470
2471
    // Try to update heads for initial migration info
2472
    // We only reached the end of the migrate-from chain already if one of the
2473
    // target chains will be empty.
2474
1.97k
    assert((cur == SIZE_MAX) ==
2475
1.97k
           (zero_chain_frontier == SIZE_MAX || one_chain_frontier == SIZE_MAX));
2476
1.97k
    assert((chain_frontier_first < 0) ==
2477
1.97k
           (zero_chain_frontier == SIZE_MAX && one_chain_frontier == SIZE_MAX));
2478
2479
    // Always update one chain's head first (safe), and mark it as locked
2480
1.97k
    saved_one_head = one_chain_frontier != SIZE_MAX
2481
1.97k
                         ? NextWithShift::Make(one_chain_frontier, new_shift)
2482
1.97k
                         : NextWithShift::MakeEnd(grow_home, new_shift);
2483
1.97k
    saved_one_head.Set<NextWithShift::LockedFlag>(true);
2484
1.97k
    arr[grow_home].head_next_with_shift.Store(saved_one_head);
2485
2486
    // Make sure length_info_ hasn't been updated too early, as we're about
2487
    // to make the change that makes it safe to update (e.g. in DoInsert())
2488
1.97k
    assert(LengthInfoToUsedLength(length_info_.Load()) <= grow_home);
2489
2490
    // Try to set zero's head.
2491
1.97k
    if (zero_head_lock.CasUpdate(
2492
1.97k
            zero_chain_frontier != SIZE_MAX
2493
1.97k
                ? NextWithShift::Make(zero_chain_frontier, new_shift)
2494
1.97k
                : NextWithShift::MakeEnd(old_home, new_shift),
2495
1.97k
            yield_count_)) {
2496
      // Both heads successfully updated to new shift
2497
1.97k
      break;
2498
1.97k
    } else {
2499
      // Concurrent insertion. This should not happen too many times.
2500
1
      CHECK_TOO_MANY_ITERATIONS(i);
2501
      // The easiest solution is to restart.
2502
1
      zero_chain_frontier = SIZE_MAX;
2503
1
      one_chain_frontier = SIZE_MAX;
2504
1
      cur = SIZE_MAX;
2505
1
      chain_frontier_first = -1;
2506
1
      continue;
2507
1
    }
2508
1.97k
  }
2509
2510
  // Create an RAII wrapper for the one chain rewrite lock we are already
2511
  // holding (if was not end) and is now "published" after successful CAS on
2512
  // zero chain head.
2513
1.97k
  ChainRewriteLock one_head_lock(&arr[grow_home], yield_count_, saved_one_head);
2514
2515
  // Except for trivial cases, we have something like
2516
  // AHome -New-> [A0] -Old-> [B0] -Old-> [C0] \                        |
2517
  // BHome --------------------New------------> [A1] -Old-> ...
2518
  // And we need to upgrade as much as we can on the "first" chain
2519
  // (the one eventually pointing to the other's frontier). This will
2520
  // also finish off any case in which one of the target chains will be empty.
2521
1.97k
  if (chain_frontier_first >= 0) {
2522
1.07k
    size_t& first_frontier = chain_frontier_first == 0
2523
1.07k
                                 ? /*&*/ zero_chain_frontier
2524
1.07k
                                 : /*&*/ one_chain_frontier;
2525
1.07k
    size_t& other_frontier = chain_frontier_first != 0
2526
1.07k
                                 ? /*&*/ zero_chain_frontier
2527
1.07k
                                 : /*&*/ one_chain_frontier;
2528
1.07k
    NextWithShift stop_before_or_new_tail =
2529
1.07k
        other_frontier != SIZE_MAX
2530
1.07k
            ? /*stop before*/ NextWithShift::Make(other_frontier, old_shift)
2531
1.07k
            : /*new tail*/ NextWithShift::MakeEnd(
2532
869
                  chain_frontier_first == 0 ? old_home : grow_home, new_shift);
2533
1.07k
    UpgradeShiftsOnRange(arr, first_frontier, stop_before_or_new_tail,
2534
1.07k
                         old_shift, new_shift);
2535
1.07k
  }
2536
2537
1.97k
  if (zero_chain_frontier == SIZE_MAX) {
2538
    // Already finished migrating
2539
1.77k
    assert(one_chain_frontier == SIZE_MAX);
2540
1.77k
    assert(cur == SIZE_MAX);
2541
1.77k
  } else {
2542
    // Still need to migrate between two target chains
2543
255
    for (int i = 0;; ++i) {
2544
255
      CHECK_TOO_MANY_ITERATIONS(i);
2545
      // Overall loop invariants
2546
255
      assert(zero_chain_frontier != SIZE_MAX);
2547
255
      assert(one_chain_frontier != SIZE_MAX);
2548
255
      assert(cur != SIZE_MAX);
2549
255
      assert(chain_frontier_first >= 0);
2550
255
      size_t& first_frontier = chain_frontier_first == 0
2551
255
                                   ? /*&*/ zero_chain_frontier
2552
255
                                   : /*&*/ one_chain_frontier;
2553
255
      size_t& other_frontier = chain_frontier_first != 0
2554
255
                                   ? /*&*/ zero_chain_frontier
2555
255
                                   : /*&*/ one_chain_frontier;
2556
255
      assert(cur != first_frontier);
2557
255
      assert(arr[first_frontier].chain_next_with_shift.Load().GetNext() ==
2558
255
             other_frontier);
2559
2560
255
      NextWithShift next_with_shift = arr[cur].chain_next_with_shift.Load();
2561
2562
      // Check for end of original chain
2563
255
      if (next_with_shift.IsEnd()) {
2564
        // Can set upgraded tail on first chain
2565
206
        NextWithShift first_new_tail = NextWithShift::MakeEnd(
2566
206
            chain_frontier_first == 0 ? old_home : grow_home, new_shift);
2567
206
        arr[first_frontier].chain_next_with_shift.Store(first_new_tail);
2568
        // And upgrade remainder of other chain
2569
206
        NextWithShift other_new_tail = NextWithShift::MakeEnd(
2570
206
            chain_frontier_first != 0 ? old_home : grow_home, new_shift);
2571
206
        UpgradeShiftsOnRange(arr, other_frontier, other_new_tail, old_shift,
2572
206
                             new_shift);
2573
206
        assert(other_frontier == SIZE_MAX);  // Finished
2574
206
        break;
2575
206
      }
2576
2577
      // next_with_shift is not End
2578
49
      cur = next_with_shift.GetNext();
2579
2580
49
      int target_chain;
2581
49
      if (BottomNBits(arr[cur].hashed_key[1], new_shift) == old_home) {
2582
        // Entry for zero chain
2583
26
        target_chain = 0;
2584
26
      } else {
2585
23
        assert(BottomNBits(arr[cur].hashed_key[1], new_shift) == grow_home);
2586
        // Entry for one chain
2587
23
        target_chain = 1;
2588
23
      }
2589
49
      if (target_chain == chain_frontier_first) {
2590
        // Found next entry to skip to on the first chain
2591
21
        NextWithShift skip_to = NextWithShift::Make(cur, new_shift);
2592
21
        arr[first_frontier].chain_next_with_shift.Store(skip_to);
2593
21
        first_frontier = cur;
2594
        // Upgrade other chain up to entry before that one
2595
21
        UpgradeShiftsOnRange(arr, other_frontier, next_with_shift, old_shift,
2596
21
                             new_shift);
2597
        // Swap which is marked as first
2598
21
        chain_frontier_first = 1 - chain_frontier_first;
2599
28
      } else {
2600
        // Nothing to do yet, as we need to keep old generation pointers in
2601
        // place for lookups
2602
28
      }
2603
49
    }
2604
206
  }
2605
1.97k
}
2606
2607
// Variant of PurgeImplLocked: Removes all "under (de) construction" entries
2608
// from a chain where already holding a rewrite lock
2609
using PurgeLockedOpData = void;
2610
// Variant of PurgeImplLocked: Clock-updates all entries in a chain, in
2611
// addition to functionality of PurgeLocked, where already holding a rewrite
2612
// lock. (Caller finalizes eviction on entries added to the autovector, in part
2613
// so that we don't hold the rewrite lock while doing potentially expensive
2614
// callback and allocator free.)
2615
using ClockUpdateChainLockedOpData =
2616
    autovector<AutoHyperClockTable::HandleImpl*>;
2617
2618
template <class OpData>
2619
void AutoHyperClockTable::PurgeImplLocked(OpData* op_data,
2620
                                          ChainRewriteLock& rewrite_lock,
2621
                                          size_t home,
2622
17.3k
                                          BaseClockTable::EvictionData* data) {
2623
17.3k
  constexpr bool kIsPurge = std::is_same_v<OpData, PurgeLockedOpData>;
2624
17.3k
  constexpr bool kIsClockUpdateChain =
2625
17.3k
      std::is_same_v<OpData, ClockUpdateChainLockedOpData>;
2626
2627
  // Exactly one op specified
2628
17.3k
  static_assert(kIsPurge + kIsClockUpdateChain == 1);
2629
2630
17.3k
  HandleImpl* const arr = array_.Get();
2631
2632
17.3k
  NextWithShift next_with_shift = rewrite_lock.GetSavedHead();
2633
17.3k
  assert(!next_with_shift.IsEnd());
2634
17.3k
  int home_shift = next_with_shift.GetShift();
2635
17.3k
  (void)home;
2636
17.3k
  (void)home_shift;
2637
17.3k
  size_t next = next_with_shift.GetNext();
2638
17.3k
  assert(next < array_.Count());
2639
17.3k
  HandleImpl* h = &arr[next];
2640
17.3k
  HandleImpl* prev_to_keep = nullptr;
2641
#ifndef NDEBUG
2642
  NextWithShift prev_to_keep_next_with_shift{};
2643
#endif
2644
  // Whether there are entries between h and prev_to_keep that should be
2645
  // purged from the chain.
2646
17.3k
  bool pending_purge = false;
2647
2648
  // Walk the chain, and stitch together any entries that are still
2649
  // "shareable," possibly after clock update. prev_to_keep tells us where
2650
  // the last "stitch back to" location is (nullptr => head).
2651
36.7k
  for (size_t i = 0;; ++i) {
2652
36.7k
    CHECK_TOO_MANY_ITERATIONS(i);
2653
2654
36.7k
    bool purgeable = false;
2655
    // In last iteration, h will be nullptr, to stitch together the tail of
2656
    // the chain.
2657
36.7k
    if (h) {
2658
      // NOTE: holding a rewrite lock on the chain prevents any "under
2659
      // (de)construction" entries in the chain from being marked empty, which
2660
      // allows us to access the hashed_keys without holding a read ref.
2661
19.4k
      assert(home == BottomNBits(h->hashed_key[1], home_shift));
2662
19.4k
      if constexpr (kIsClockUpdateChain) {
2663
        // Clock update and/or check for purgeable (under (de)construction)
2664
0
        if (ClockUpdate(*h, data, &purgeable)) {
2665
          // Remember for finishing eviction
2666
0
          op_data->push_back(h);
2667
          // Entries for eviction become purgeable
2668
0
          purgeable = true;
2669
0
          assert(h->meta.Load().IsUnderConstruction());
2670
0
        }
2671
19.4k
      } else {
2672
19.4k
        (void)op_data;
2673
19.4k
        (void)data;
2674
19.4k
        purgeable = !h->meta.Load().IsShareable();
2675
19.4k
      }
2676
19.4k
    }
2677
2678
36.7k
    if (purgeable) {
2679
17.3k
      assert(h->meta.Load().IsUnderConstruction());
2680
17.3k
      pending_purge = true;
2681
19.4k
    } else if (pending_purge) {
2682
17.3k
      if (prev_to_keep) {
2683
        // Update chain next to skip purgeable entries
2684
1.01k
        assert(prev_to_keep->chain_next_with_shift.Load() ==
2685
1.01k
               prev_to_keep_next_with_shift);
2686
1.01k
        prev_to_keep->chain_next_with_shift.Store(next_with_shift);
2687
16.2k
      } else if (rewrite_lock.CasUpdate(next_with_shift, yield_count_)) {
2688
        // Managed to update head without any parallel insertions
2689
16.2k
      } else {
2690
        // Parallel insertion must have interfered. Need to do a purge
2691
        // from updated head to here. Since we have no prev_to_keep, there's
2692
        // no risk of duplicate clock updates to entries. Any entries already
2693
        // updated must have been evicted (purgeable) and it's OK to clock
2694
        // update any new entries just inserted in parallel.
2695
        // Can simply restart (GetSavedHead() already updated from CAS failure).
2696
0
        next_with_shift = rewrite_lock.GetSavedHead();
2697
0
        assert(!next_with_shift.IsEnd());
2698
0
        next = next_with_shift.GetNext();
2699
0
        assert(next < array_.Count());
2700
0
        h = &arr[next];
2701
0
        pending_purge = false;
2702
0
        assert(prev_to_keep == nullptr);
2703
0
        assert(next_with_shift.GetShift() == home_shift);
2704
0
        continue;
2705
0
      }
2706
17.3k
      pending_purge = false;
2707
17.3k
      prev_to_keep = h;
2708
17.3k
    } else {
2709
2.14k
      prev_to_keep = h;
2710
2.14k
    }
2711
2712
36.7k
    if (h == nullptr) {
2713
      // Reached end of the chain
2714
17.3k
      return;
2715
17.3k
    }
2716
2717
    // Read chain pointer
2718
19.4k
    next_with_shift = h->chain_next_with_shift.Load();
2719
#ifndef NDEBUG
2720
    if (prev_to_keep == h) {
2721
      prev_to_keep_next_with_shift = next_with_shift;
2722
    }
2723
#endif
2724
2725
19.4k
    assert(next_with_shift.GetShift() == home_shift);
2726
2727
    // Check for end marker
2728
19.4k
    if (next_with_shift.IsEnd()) {
2729
17.3k
      h = nullptr;
2730
17.3k
    } else {
2731
2.14k
      next = next_with_shift.GetNext();
2732
2.14k
      assert(next < array_.Count());
2733
2.14k
      h = &arr[next];
2734
2.14k
      assert(h != prev_to_keep);
2735
2.14k
    }
2736
19.4k
  }
2737
17.3k
}
void rocksdb::clock_cache::AutoHyperClockTable::PurgeImplLocked<void>(void*, rocksdb::clock_cache::AutoHyperClockTable::ChainRewriteLock&, unsigned long, rocksdb::clock_cache::BaseClockTable::EvictionData*)
Line
Count
Source
2622
17.3k
                                          BaseClockTable::EvictionData* data) {
2623
17.3k
  constexpr bool kIsPurge = std::is_same_v<OpData, PurgeLockedOpData>;
2624
17.3k
  constexpr bool kIsClockUpdateChain =
2625
17.3k
      std::is_same_v<OpData, ClockUpdateChainLockedOpData>;
2626
2627
  // Exactly one op specified
2628
17.3k
  static_assert(kIsPurge + kIsClockUpdateChain == 1);
2629
2630
17.3k
  HandleImpl* const arr = array_.Get();
2631
2632
17.3k
  NextWithShift next_with_shift = rewrite_lock.GetSavedHead();
2633
17.3k
  assert(!next_with_shift.IsEnd());
2634
17.3k
  int home_shift = next_with_shift.GetShift();
2635
17.3k
  (void)home;
2636
17.3k
  (void)home_shift;
2637
17.3k
  size_t next = next_with_shift.GetNext();
2638
17.3k
  assert(next < array_.Count());
2639
17.3k
  HandleImpl* h = &arr[next];
2640
17.3k
  HandleImpl* prev_to_keep = nullptr;
2641
#ifndef NDEBUG
2642
  NextWithShift prev_to_keep_next_with_shift{};
2643
#endif
2644
  // Whether there are entries between h and prev_to_keep that should be
2645
  // purged from the chain.
2646
17.3k
  bool pending_purge = false;
2647
2648
  // Walk the chain, and stitch together any entries that are still
2649
  // "shareable," possibly after clock update. prev_to_keep tells us where
2650
  // the last "stitch back to" location is (nullptr => head).
2651
36.7k
  for (size_t i = 0;; ++i) {
2652
36.7k
    CHECK_TOO_MANY_ITERATIONS(i);
2653
2654
36.7k
    bool purgeable = false;
2655
    // In last iteration, h will be nullptr, to stitch together the tail of
2656
    // the chain.
2657
36.7k
    if (h) {
2658
      // NOTE: holding a rewrite lock on the chain prevents any "under
2659
      // (de)construction" entries in the chain from being marked empty, which
2660
      // allows us to access the hashed_keys without holding a read ref.
2661
19.4k
      assert(home == BottomNBits(h->hashed_key[1], home_shift));
2662
      if constexpr (kIsClockUpdateChain) {
2663
        // Clock update and/or check for purgeable (under (de)construction)
2664
        if (ClockUpdate(*h, data, &purgeable)) {
2665
          // Remember for finishing eviction
2666
          op_data->push_back(h);
2667
          // Entries for eviction become purgeable
2668
          purgeable = true;
2669
          assert(h->meta.Load().IsUnderConstruction());
2670
        }
2671
19.4k
      } else {
2672
19.4k
        (void)op_data;
2673
19.4k
        (void)data;
2674
19.4k
        purgeable = !h->meta.Load().IsShareable();
2675
19.4k
      }
2676
19.4k
    }
2677
2678
36.7k
    if (purgeable) {
2679
17.3k
      assert(h->meta.Load().IsUnderConstruction());
2680
17.3k
      pending_purge = true;
2681
19.4k
    } else if (pending_purge) {
2682
17.3k
      if (prev_to_keep) {
2683
        // Update chain next to skip purgeable entries
2684
1.01k
        assert(prev_to_keep->chain_next_with_shift.Load() ==
2685
1.01k
               prev_to_keep_next_with_shift);
2686
1.01k
        prev_to_keep->chain_next_with_shift.Store(next_with_shift);
2687
16.2k
      } else if (rewrite_lock.CasUpdate(next_with_shift, yield_count_)) {
2688
        // Managed to update head without any parallel insertions
2689
16.2k
      } else {
2690
        // Parallel insertion must have interfered. Need to do a purge
2691
        // from updated head to here. Since we have no prev_to_keep, there's
2692
        // no risk of duplicate clock updates to entries. Any entries already
2693
        // updated must have been evicted (purgeable) and it's OK to clock
2694
        // update any new entries just inserted in parallel.
2695
        // Can simply restart (GetSavedHead() already updated from CAS failure).
2696
0
        next_with_shift = rewrite_lock.GetSavedHead();
2697
0
        assert(!next_with_shift.IsEnd());
2698
0
        next = next_with_shift.GetNext();
2699
0
        assert(next < array_.Count());
2700
0
        h = &arr[next];
2701
0
        pending_purge = false;
2702
0
        assert(prev_to_keep == nullptr);
2703
0
        assert(next_with_shift.GetShift() == home_shift);
2704
0
        continue;
2705
0
      }
2706
17.3k
      pending_purge = false;
2707
17.3k
      prev_to_keep = h;
2708
17.3k
    } else {
2709
2.14k
      prev_to_keep = h;
2710
2.14k
    }
2711
2712
36.7k
    if (h == nullptr) {
2713
      // Reached end of the chain
2714
17.3k
      return;
2715
17.3k
    }
2716
2717
    // Read chain pointer
2718
19.4k
    next_with_shift = h->chain_next_with_shift.Load();
2719
#ifndef NDEBUG
2720
    if (prev_to_keep == h) {
2721
      prev_to_keep_next_with_shift = next_with_shift;
2722
    }
2723
#endif
2724
2725
19.4k
    assert(next_with_shift.GetShift() == home_shift);
2726
2727
    // Check for end marker
2728
19.4k
    if (next_with_shift.IsEnd()) {
2729
17.3k
      h = nullptr;
2730
17.3k
    } else {
2731
2.14k
      next = next_with_shift.GetNext();
2732
2.14k
      assert(next < array_.Count());
2733
2.14k
      h = &arr[next];
2734
      assert(h != prev_to_keep);
2735
2.14k
    }
2736
19.4k
  }
2737
17.3k
}
Unexecuted instantiation: void rocksdb::clock_cache::AutoHyperClockTable::PurgeImplLocked<rocksdb::autovector<rocksdb::clock_cache::AutoHyperClockTable::HandleImpl*, 8ul> >(rocksdb::autovector<rocksdb::clock_cache::AutoHyperClockTable::HandleImpl*, 8ul>*, rocksdb::clock_cache::AutoHyperClockTable::ChainRewriteLock&, unsigned long, rocksdb::clock_cache::BaseClockTable::EvictionData*)
2738
2739
// Variant of PurgeImpl: Removes all "under (de) construction" entries in a
2740
// chain, such that any entry with the given key must have been purged.
2741
using PurgeOpData = const UniqueId64x2;
2742
// Variant of PurgeImpl: Clock-updates all entries in a chain, in addition to
2743
// purging as appropriate. (Caller finalizes eviction on entries added to the
2744
// autovector, in part so that we don't hold the rewrite lock while doing
2745
// potentially expensive callback and allocator free.)
2746
using ClockUpdateChainOpData = ClockUpdateChainLockedOpData;
2747
2748
template <class OpData>
2749
void AutoHyperClockTable::PurgeImpl(OpData* op_data, size_t home,
2750
17.3k
                                    BaseClockTable::EvictionData* data) {
2751
  // Early efforts to make AutoHCC fully wait-free ran into too many problems
2752
  // that needed obscure and potentially inefficient work-arounds to have a
2753
  // chance at working.
2754
  //
2755
  // The implementation settled on "essentially wait-free" which can be
2756
  // achieved by locking at the level of each probing chain and only for
2757
  // operations that might remove entries from the chain. Because parallel
2758
  // clock updates and Grow operations are ordered, contention is very rare.
2759
  // However, parallel insertions at any chain head have to be accommodated
2760
  // to keep them wait-free.
2761
  //
2762
  // This function implements Purge and ClockUpdateChain functions (see above
2763
  // OpData type definitions) as part of higher-level operations. This function
2764
  // ensures the correct chain is (eventually) covered and handles rewrite
2765
  // locking the chain. PurgeImplLocked has lower level details.
2766
  //
2767
  // In general, these operations and Grow are kept simpler by allowing eager
2768
  // purging of under (de-)construction entries. For example, an Erase
2769
  // operation might find that another thread has purged the entry from the
2770
  // chain by the time its own purge operation acquires the rewrite lock and
2771
  // proceeds. This is OK, and potentially reduces the number of lock/unlock
2772
  // cycles because empty chains are not rewrite-lockable.
2773
2774
17.3k
  constexpr bool kIsPurge = std::is_same_v<OpData, PurgeOpData>;
2775
17.3k
  constexpr bool kIsClockUpdateChain =
2776
17.3k
      std::is_same_v<OpData, ClockUpdateChainOpData>;
2777
2778
  // Exactly one op specified
2779
17.3k
  static_assert(kIsPurge + kIsClockUpdateChain == 1);
2780
2781
17.3k
  int home_shift = 0;
2782
17.3k
  if constexpr (kIsPurge) {
2783
    // Purge callers leave home unspecified, to be determined from key
2784
17.3k
    assert(home == SIZE_MAX);
2785
17.3k
    GetHomeIndexAndShift(length_info_.Load(), (*op_data)[1], &home,
2786
17.3k
                         &home_shift);
2787
17.3k
    assert(home_shift > 0);
2788
17.3k
  } else {
2789
0
    assert(kIsClockUpdateChain);
2790
    // Evict callers must specify home
2791
0
    assert(home < SIZE_MAX);
2792
0
  }
2793
2794
17.3k
  HandleImpl* const arr = array_.Get();
2795
2796
  // Acquire the RAII rewrite lock (if not an empty chain)
2797
17.3k
  ChainRewriteLock rewrite_lock(&arr[home], yield_count_);
2798
2799
17.3k
  if constexpr (kIsPurge) {
2800
    // Ensure we are at the correct home for the shift in effect for the
2801
    // chain head.
2802
17.3k
    for (;;) {
2803
17.3k
      int shift = rewrite_lock.GetSavedHead().GetShift();
2804
2805
17.3k
      if (shift > home_shift) {
2806
        // Found a newer shift at candidate head, which must apply to us.
2807
        // Newer shift might not yet be reflected in length_info_ (an atomicity
2808
        // gap in Grow), so operate as if it is. Note that other insertions
2809
        // could happen using this shift before length_info_ is updated, and
2810
        // it's possible (though unlikely) that multiple generations of Grow
2811
        // have occurred. If shift is more than one generation ahead of
2812
        // home_shift, it's possible that not all descendent homes have
2813
        // reached the `shift` generation. Thus, we need to advance only one
2814
        // shift at a time looking for a home+head with a matching shift
2815
        // amount.
2816
0
        home_shift++;
2817
0
        home = GetHomeIndex((*op_data)[1], home_shift);
2818
0
        rewrite_lock.Reset(&arr[home], yield_count_);
2819
0
        continue;
2820
17.3k
      } else {
2821
17.3k
        assert(shift == home_shift);
2822
17.3k
      }
2823
17.3k
      break;
2824
17.3k
    }
2825
17.3k
  }
2826
2827
  // If the chain is empty, nothing to do
2828
17.3k
  if (!rewrite_lock.IsEnd()) {
2829
17.3k
    if constexpr (kIsPurge) {
2830
17.3k
      PurgeLockedOpData* locked_op_data{};
2831
17.3k
      PurgeImplLocked(locked_op_data, rewrite_lock, home, data);
2832
17.3k
    } else {
2833
0
      PurgeImplLocked(op_data, rewrite_lock, home, data);
2834
0
    }
2835
17.3k
  }
2836
17.3k
}
void rocksdb::clock_cache::AutoHyperClockTable::PurgeImpl<std::__1::array<unsigned long, 2ul> const>(std::__1::array<unsigned long, 2ul> const*, unsigned long, rocksdb::clock_cache::BaseClockTable::EvictionData*)
Line
Count
Source
2750
17.3k
                                    BaseClockTable::EvictionData* data) {
2751
  // Early efforts to make AutoHCC fully wait-free ran into too many problems
2752
  // that needed obscure and potentially inefficient work-arounds to have a
2753
  // chance at working.
2754
  //
2755
  // The implementation settled on "essentially wait-free" which can be
2756
  // achieved by locking at the level of each probing chain and only for
2757
  // operations that might remove entries from the chain. Because parallel
2758
  // clock updates and Grow operations are ordered, contention is very rare.
2759
  // However, parallel insertions at any chain head have to be accommodated
2760
  // to keep them wait-free.
2761
  //
2762
  // This function implements Purge and ClockUpdateChain functions (see above
2763
  // OpData type definitions) as part of higher-level operations. This function
2764
  // ensures the correct chain is (eventually) covered and handles rewrite
2765
  // locking the chain. PurgeImplLocked has lower level details.
2766
  //
2767
  // In general, these operations and Grow are kept simpler by allowing eager
2768
  // purging of under (de-)construction entries. For example, an Erase
2769
  // operation might find that another thread has purged the entry from the
2770
  // chain by the time its own purge operation acquires the rewrite lock and
2771
  // proceeds. This is OK, and potentially reduces the number of lock/unlock
2772
  // cycles because empty chains are not rewrite-lockable.
2773
2774
17.3k
  constexpr bool kIsPurge = std::is_same_v<OpData, PurgeOpData>;
2775
17.3k
  constexpr bool kIsClockUpdateChain =
2776
17.3k
      std::is_same_v<OpData, ClockUpdateChainOpData>;
2777
2778
  // Exactly one op specified
2779
17.3k
  static_assert(kIsPurge + kIsClockUpdateChain == 1);
2780
2781
17.3k
  int home_shift = 0;
2782
17.3k
  if constexpr (kIsPurge) {
2783
    // Purge callers leave home unspecified, to be determined from key
2784
17.3k
    assert(home == SIZE_MAX);
2785
17.3k
    GetHomeIndexAndShift(length_info_.Load(), (*op_data)[1], &home,
2786
17.3k
                         &home_shift);
2787
17.3k
    assert(home_shift > 0);
2788
  } else {
2789
    assert(kIsClockUpdateChain);
2790
    // Evict callers must specify home
2791
    assert(home < SIZE_MAX);
2792
  }
2793
2794
17.3k
  HandleImpl* const arr = array_.Get();
2795
2796
  // Acquire the RAII rewrite lock (if not an empty chain)
2797
17.3k
  ChainRewriteLock rewrite_lock(&arr[home], yield_count_);
2798
2799
17.3k
  if constexpr (kIsPurge) {
2800
    // Ensure we are at the correct home for the shift in effect for the
2801
    // chain head.
2802
17.3k
    for (;;) {
2803
17.3k
      int shift = rewrite_lock.GetSavedHead().GetShift();
2804
2805
17.3k
      if (shift > home_shift) {
2806
        // Found a newer shift at candidate head, which must apply to us.
2807
        // Newer shift might not yet be reflected in length_info_ (an atomicity
2808
        // gap in Grow), so operate as if it is. Note that other insertions
2809
        // could happen using this shift before length_info_ is updated, and
2810
        // it's possible (though unlikely) that multiple generations of Grow
2811
        // have occurred. If shift is more than one generation ahead of
2812
        // home_shift, it's possible that not all descendent homes have
2813
        // reached the `shift` generation. Thus, we need to advance only one
2814
        // shift at a time looking for a home+head with a matching shift
2815
        // amount.
2816
0
        home_shift++;
2817
0
        home = GetHomeIndex((*op_data)[1], home_shift);
2818
0
        rewrite_lock.Reset(&arr[home], yield_count_);
2819
0
        continue;
2820
17.3k
      } else {
2821
17.3k
        assert(shift == home_shift);
2822
17.3k
      }
2823
17.3k
      break;
2824
17.3k
    }
2825
17.3k
  }
2826
2827
  // If the chain is empty, nothing to do
2828
17.3k
  if (!rewrite_lock.IsEnd()) {
2829
17.3k
    if constexpr (kIsPurge) {
2830
17.3k
      PurgeLockedOpData* locked_op_data{};
2831
17.3k
      PurgeImplLocked(locked_op_data, rewrite_lock, home, data);
2832
    } else {
2833
      PurgeImplLocked(op_data, rewrite_lock, home, data);
2834
    }
2835
17.3k
  }
2836
17.3k
}
Unexecuted instantiation: void rocksdb::clock_cache::AutoHyperClockTable::PurgeImpl<rocksdb::autovector<rocksdb::clock_cache::AutoHyperClockTable::HandleImpl*, 8ul> >(rocksdb::autovector<rocksdb::clock_cache::AutoHyperClockTable::HandleImpl*, 8ul>*, unsigned long, rocksdb::clock_cache::BaseClockTable::EvictionData*)
2837
2838
AutoHyperClockTable::HandleImpl* AutoHyperClockTable::DoInsert(
2839
    const ClockHandleBasicData& proto, uint32_t initial_countdown,
2840
107k
    bool take_ref, InsertState& state) {
2841
107k
  size_t home;
2842
107k
  int orig_home_shift;
2843
107k
  GetHomeIndexAndShift(state.saved_length_info, proto.hashed_key[1], &home,
2844
107k
                       &orig_home_shift);
2845
107k
  HandleImpl* const arr = array_.Get();
2846
2847
  // We could go searching through the chain for any duplicate, but that's
2848
  // not typically helpful, except for the REDUNDANT block cache stats.
2849
  // (Inferior duplicates will age out with eviction.) However, we do skip
2850
  // insertion if the home slot (or some other we happen to probe) already
2851
  // has a match (already_matches below). This helps to keep better locality
2852
  // when we can.
2853
  //
2854
  // And we can do that as part of searching for an available slot to
2855
  // insert the new entry, because our preferred location and first slot
2856
  // checked will be the home slot.
2857
  //
2858
  // As the table initially grows to size, few entries will be in the same
2859
  // cache line as the chain head. However, churn in the cache relatively
2860
  // quickly improves the proportion of entries sharing that cache line with
2861
  // the chain head. Data:
2862
  //
2863
  // Initial population only: (cache_bench with -ops_per_thread=1)
2864
  // Entries at home count: 29,202 (out of 129,170 entries in 94,411 chains)
2865
  // Approximate average cache lines read to find an existing entry:
2866
  //           129.2 / 94.4 [without the heads]
2867
  // + (94.4 - 29.2) / 94.4 [the heads not included with entries]
2868
  // = 2.06 cache lines
2869
  //
2870
  // After 10 million ops: (-threads=10 -ops_per_thread=100000)
2871
  // Entries at home count: 67,556 (out of 129,359 entries in 94,756 chains)
2872
  // That's a majority of entries and more than 2/3rds of chains.
2873
  // Approximate average cache lines read to find an existing entry:
2874
  // = 1.65 cache lines
2875
2876
  // Even if we aren't saving a ref to this entry (take_ref == false), we need
2877
  // to keep a reference while we are inserting the entry into a chain, so that
2878
  // it is not erased by another thread while trying to insert it on the chain.
2879
107k
  constexpr bool initial_take_ref = true;
2880
2881
107k
  size_t used_length = LengthInfoToUsedLength(state.saved_length_info);
2882
107k
  assert(home < used_length);
2883
2884
107k
  size_t idx = home;
2885
107k
  bool already_matches = false;
2886
107k
  bool already_matches_ignore = false;
2887
107k
  if (TryInsert(proto, arr[idx], initial_countdown, initial_take_ref,
2888
107k
                &already_matches)) {
2889
102k
    assert(idx == home);
2890
102k
  } else if (already_matches) {
2891
0
    return nullptr;
2892
    // Here we try to populate newly-opened slots in the table, but not
2893
    // when we can add something to its home slot. This makes the structure
2894
    // more performant more quickly on (initial) growth. We ignore "already
2895
    // matches" in this case because it is unlikely and difficult to
2896
    // incorporate logic for here cleanly and efficiently.
2897
4.96k
  } else if (UNLIKELY(state.likely_empty_slot > 0) &&
2898
727
             TryInsert(proto, arr[state.likely_empty_slot], initial_countdown,
2899
727
                       initial_take_ref, &already_matches_ignore)) {
2900
727
    idx = state.likely_empty_slot;
2901
4.24k
  } else {
2902
    // We need to search for an available slot outside of the home.
2903
    // Linear hashing provides nice resizing but does typically mean
2904
    // that some heads (home locations) have (in expectation) twice as
2905
    // many entries mapped to them as other heads. For example if the
2906
    // usable length is 80, then heads 16-63 are (in expectation) twice
2907
    // as loaded as heads 0-15 and 64-79, which are using another hash bit.
2908
    //
2909
    // This means that if we just use linear probing (by a small constant)
2910
    // to find an available slot, part of the structure could easily fill up
2911
    // and resort to linear time operations even when the overall load factor
2912
    // is only modestly high, like 70%. Even though each slot has its own CPU
2913
    // cache line, there appears to be a small locality benefit (e.g. TLB and
2914
    // paging) to iterating one by one, as long as we don't afoul of the
2915
    // linear hashing imbalance.
2916
    //
2917
    // In a traditional non-concurrent structure, we could keep a "free list"
2918
    // to ensure immediate access to an available slot, but maintaining such
2919
    // a structure could require more cross-thread coordination to ensure
2920
    // all entries are eventually available to all threads.
2921
    //
2922
    // The way we solve this problem is to use unit-increment linear probing
2923
    // with a small bound, and then fall back on big jumps to have a good
2924
    // chance of finding a slot in an under-populated region quickly if that
2925
    // doesn't work.
2926
4.24k
    size_t i = 0;
2927
4.24k
    constexpr size_t kMaxLinearProbe = 4;
2928
6.46k
    for (; i < kMaxLinearProbe; i++) {
2929
6.29k
      idx++;
2930
6.29k
      if (idx >= used_length) {
2931
79
        idx -= used_length;
2932
79
      }
2933
6.29k
      if (TryInsert(proto, arr[idx], initial_countdown, initial_take_ref,
2934
6.29k
                    &already_matches)) {
2935
4.07k
        break;
2936
4.07k
      }
2937
2.22k
      if (already_matches) {
2938
0
        return nullptr;
2939
0
      }
2940
2.22k
    }
2941
4.24k
    if (i == kMaxLinearProbe) {
2942
      // Keep searching, but change to a search method that should quickly
2943
      // find any under-populated region. Switching to an increment based
2944
      // on the golden ratio helps with that, but we also inject some minor
2945
      // variation (less than 2%, 1 in 2^6) to avoid clustering effects on
2946
      // this larger increment (if it were a fixed value in steady state
2947
      // operation). Here we are primarily using upper bits of hashed_key[1]
2948
      // while home is based on lowest bits.
2949
166
      uint64_t incr_ratio = 0x9E3779B185EBCA87U + (proto.hashed_key[1] >> 6);
2950
166
      size_t incr = FastRange64(incr_ratio, used_length);
2951
166
      assert(incr > 0);
2952
166
      size_t start = idx;
2953
297
      for (;; i++) {
2954
297
        idx += incr;
2955
297
        if (idx >= used_length) {
2956
          // Wrap around (faster than %)
2957
160
          idx -= used_length;
2958
160
        }
2959
297
        if (idx == start) {
2960
          // We have just completed a cycle that might not have covered all
2961
          // slots. (incr and used_length could have common factors.)
2962
          // Increment for the next cycle, which eventually ensures complete
2963
          // iteration over the set of slots before repeating.
2964
0
          idx++;
2965
0
          if (idx >= used_length) {
2966
0
            idx -= used_length;
2967
0
          }
2968
0
          start++;
2969
0
          if (start >= used_length) {
2970
0
            start -= used_length;
2971
0
          }
2972
0
          if (i >= used_length) {
2973
0
            used_length = LengthInfoToUsedLength(length_info_.Load());
2974
0
            if (i >= used_length * 2) {
2975
              // Cycling back should not happen unless there is enough random
2976
              // churn in parallel that we happen to hit each slot at a time
2977
              // that it's occupied, which is really only feasible for small
2978
              // structures, though with linear probing to find empty slots,
2979
              // "small" here might be larger than for double hashing.
2980
0
              assert(used_length <= 256);
2981
              // Fall back on standalone insert in case something goes awry to
2982
              // cause this
2983
0
              return nullptr;
2984
0
            }
2985
0
          }
2986
0
        }
2987
297
        if (TryInsert(proto, arr[idx], initial_countdown, initial_take_ref,
2988
297
                      &already_matches)) {
2989
166
          break;
2990
166
        }
2991
131
        if (already_matches) {
2992
0
          return nullptr;
2993
0
        }
2994
131
      }
2995
166
    }
2996
4.24k
  }
2997
2998
  // Now insert into chain using head pointer
2999
107k
  NextWithShift next_with_shift;
3000
107k
  int home_shift = orig_home_shift;
3001
3002
  // Might need to retry
3003
107k
  for (int i = 0;; ++i) {
3004
107k
    CHECK_TOO_MANY_ITERATIONS(i);
3005
107k
    next_with_shift = arr[home].head_next_with_shift.Load();
3006
107k
    int shift = next_with_shift.GetShift();
3007
3008
107k
    if (UNLIKELY(shift != home_shift)) {
3009
      // NOTE: shift increases with table growth
3010
37
      if (shift > home_shift) {
3011
        // Must be grow in progress or completed since reading length_info.
3012
        // Pull out one more hash bit. (See Lookup() for why we can't
3013
        // safely jump to the shift that was read.)
3014
37
        home_shift++;
3015
37
        uint64_t hash_bit_mask = uint64_t{1} << (home_shift - 1);
3016
37
        assert((home & hash_bit_mask) == 0);
3017
        // BEGIN leftover updates to length_info_ for Grow()
3018
37
        size_t grow_home = home + hash_bit_mask;
3019
37
        assert(arr[grow_home].head_next_with_shift.Load() !=
3020
37
               HandleImpl::kUnusedMarker);
3021
37
        CatchUpLengthInfoNoWait(grow_home);
3022
        // END leftover updates to length_info_ for Grow()
3023
37
        home += proto.hashed_key[1] & hash_bit_mask;
3024
37
        continue;
3025
37
      } else {
3026
        // Should not happen because length_info_ is only updated after both
3027
        // old and new home heads are marked with new shift
3028
0
        assert(false);
3029
0
      }
3030
37
    }
3031
3032
    // Values to update to
3033
107k
    NextWithShift head_next_with_shift = NextWithShift::Make(idx, home_shift);
3034
107k
    NextWithShift chain_next_with_shift = next_with_shift;
3035
3036
    // Preserve the locked state in head, without propagating to chain next
3037
    // where it is meaningless (and not allowed)
3038
107k
    if (UNLIKELY(next_with_shift.IsLockedNotEnd())) {
3039
0
      head_next_with_shift.Set<NextWithShift::LockedFlag>(true);
3040
0
      chain_next_with_shift.Set<NextWithShift::LockedFlag>(false);
3041
0
    }
3042
3043
107k
    arr[idx].chain_next_with_shift.Store(chain_next_with_shift);
3044
107k
    if (arr[home].head_next_with_shift.CasWeak(next_with_shift,
3045
107k
                                               head_next_with_shift)) {
3046
      // Success
3047
107k
      if (!take_ref) {
3048
0
        Unref(arr[idx]);
3049
0
      }
3050
107k
      return arr + idx;
3051
107k
    }
3052
107k
  }
3053
107k
}
3054
3055
AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
3056
232k
    const UniqueId64x2& hashed_key) {
3057
  // Lookups are wait-free with low occurrence of retries, back-tracking,
3058
  // and fallback. We do not have the benefit of holding a rewrite lock on
3059
  // the chain so must be prepared for many kinds of mayhem, most notably
3060
  // "falling off our chain" where a slot that Lookup has identified but
3061
  // has not read-referenced is removed from one chain and inserted into
3062
  // another. The full algorithm uses the following mitigation strategies to
3063
  // ensure every relevant entry inserted before this Lookup, and not yet
3064
  // evicted, is seen by Lookup, without excessive backtracking etc.:
3065
  // * Keep a known good read ref in the chain for "island hopping." When
3066
  // we observe that a concurrent write takes us off to another chain, we
3067
  // only need to fall back to our last known good read ref (most recent
3068
  // entry on the chain that is not "under construction," which is a transient
3069
  // state). We don't want to compound the CPU toil of a long chain with
3070
  // operations that might need to retry from scratch, with probability
3071
  // in proportion to chain length.
3072
  // * Only detect a chain is potentially incomplete because of a Grow in
3073
  // progress by looking at shift in the next pointer tags (rather than
3074
  // re-checking length_info_).
3075
  // * SplitForGrow, Insert, and PurgeImplLocked ensure that there are no
3076
  // transient states that might cause this full Lookup algorithm to skip over
3077
  // live entries.
3078
3079
  // Reading length_info_ is not strictly required for Lookup, if we were
3080
  // to increment shift sizes until we see a shift size match on the
3081
  // relevant head pointer. Thus, reading with relaxed memory order gives
3082
  // us a safe and almost always up-to-date jump into finding the correct
3083
  // home and head.
3084
232k
  size_t home;
3085
232k
  int home_shift;
3086
232k
  GetHomeIndexAndShift(length_info_.LoadRelaxed(), hashed_key[1], &home,
3087
232k
                       &home_shift);
3088
232k
  assert(home_shift > 0);
3089
3090
  // The full Lookup algorithm however is not great for hot path efficiency,
3091
  // because of the extra careful tracking described above. Overwhelmingly,
3092
  // we can find what we're looking for with a naive linked list traversal
3093
  // of the chain. Even if we "fall off our chain" to another, we don't
3094
  // violate memory safety. We just won't match the key we're looking for.
3095
  // And we would eventually reach an end state, possibly even experiencing a
3096
  // cycle as an entry is freed and reused during our traversal (though at
3097
  // any point in time the structure doesn't have cycles).
3098
  //
3099
  // So for hot path efficiency, we start with a naive Lookup attempt, and
3100
  // then fall back on full Lookup if we don't find the correct entry. To
3101
  // cap how much we invest into the naive Lookup, we simply cap the traversal
3102
  // length before falling back. Also, when we do fall back on full Lookup,
3103
  // we aren't paying much penalty by starting over. Much or most of the cost
3104
  // of Lookup is memory latency in following the chain pointers, and the
3105
  // naive Lookup has warmed the CPU cache for these entries, using as tight
3106
  // of a loop as possible.
3107
3108
232k
  HandleImpl* const arr = array_.Get();
3109
232k
  NextWithShift next_with_shift = arr[home].head_next_with_shift.LoadRelaxed();
3110
237k
  for (size_t i = 0; !next_with_shift.IsEnd() && i < 10; ++i) {
3111
62.9k
    HandleImpl* h = &arr[next_with_shift.GetNext()];
3112
    // Attempt cheap key match without acquiring a read ref. This could give a
3113
    // false positive, which is re-checked after acquiring read ref, or false
3114
    // negative, which is re-checked in the full Lookup. Also, this is a
3115
    // technical UB data race according to TSAN, but we don't need to read
3116
    // a "correct" value here for correct overall behavior.
3117
#ifdef __SANITIZE_THREAD__
3118
    bool probably_equal = Random::GetTLSInstance()->OneIn(2);
3119
#else
3120
62.9k
    bool probably_equal = h->hashed_key == hashed_key;
3121
62.9k
#endif
3122
62.9k
    if (probably_equal) {
3123
      // Increment acquire counter for definitive check
3124
57.6k
      auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
3125
57.6k
      SlotMeta old_meta, new_meta;
3126
57.6k
      h->meta.Apply(add_acquire, &old_meta, &new_meta);
3127
      // Check if it's a referencable (sharable) entry
3128
57.6k
      if (LIKELY(old_meta.IsShareable())) {
3129
57.6k
        assert(new_meta.GetRefcount() > 0);
3130
57.6k
        if (LIKELY(h->hashed_key == hashed_key) &&
3131
57.6k
            LIKELY(old_meta.IsVisible())) {
3132
57.6k
          return h;
3133
57.6k
        } else {
3134
1
          Unref(*h);
3135
1
        }
3136
18.4E
      } else {
3137
        // For non-sharable states, incrementing the acquire counter has no
3138
        // effect so we don't need to undo it. Furthermore, we cannot safely
3139
        // undo it because we did not acquire a read reference to lock the entry
3140
        // in a Shareable state.
3141
18.4E
      }
3142
57.6k
    }
3143
3144
5.27k
    next_with_shift = h->chain_next_with_shift.LoadRelaxed();
3145
5.27k
  }
3146
3147
  // If we get here, falling back on full Lookup algorithm.
3148
174k
  HandleImpl* h = nullptr;
3149
174k
  HandleImpl* read_ref_on_chain = nullptr;
3150
3151
178k
  for (size_t i = 0;; ++i) {
3152
178k
    CHECK_TOO_MANY_ITERATIONS(i);
3153
    // Read head or chain pointer
3154
178k
    next_with_shift = h ? h->chain_next_with_shift.Load()
3155
178k
                        : arr[home].head_next_with_shift.Load();
3156
178k
    int shift = next_with_shift.GetShift();
3157
3158
    // Make sure it's usable
3159
178k
    size_t effective_home = home;
3160
178k
    if (UNLIKELY(shift != home_shift)) {
3161
      // We have potentially gone awry somehow, but it's possible we're just
3162
      // hitting old data that is not yet completed Grow.
3163
      // NOTE: shift bits goes up with table growth.
3164
0
      if (shift < home_shift) {
3165
        // To avoid waiting on Grow in progress, an old shift amount needs
3166
        // to be processed as if we were still using it and (potentially
3167
        // different or the same) the old home.
3168
        // We can assert it's not too old, because each generation of Grow
3169
        // waits on its ancestor in the previous generation.
3170
0
        assert(shift + 1 == home_shift);
3171
0
        effective_home = GetHomeIndex(home, shift);
3172
0
      } else if (h == read_ref_on_chain) {
3173
0
        assert(shift > home_shift);
3174
        // At head or coming from an entry on our chain where we're holding
3175
        // a read reference. Thus, we know the newer shift applies to us.
3176
        // Newer shift might not yet be reflected in length_info_ (an atomicity
3177
        // gap in Grow), so operate as if it is. Note that other insertions
3178
        // could happen using this shift before length_info_ is updated, and
3179
        // it's possible (though unlikely) that multiple generations of Grow
3180
        // have occurred. If shift is more than one generation ahead of
3181
        // home_shift, it's possible that not all descendent homes have
3182
        // reached the `shift` generation. Thus, we need to advance only one
3183
        // shift at a time looking for a home+head with a matching shift
3184
        // amount.
3185
0
        home_shift++;
3186
        // Update home in case it has changed
3187
0
        home = GetHomeIndex(hashed_key[1], home_shift);
3188
        // This should be rare enough occurrence that it's simplest just
3189
        // to restart (TODO: improve in some cases?)
3190
0
        h = nullptr;
3191
0
        if (read_ref_on_chain) {
3192
0
          Unref(*read_ref_on_chain);
3193
0
          read_ref_on_chain = nullptr;
3194
0
        }
3195
        // Didn't make progress & retry
3196
0
        continue;
3197
0
      } else {
3198
0
        assert(shift > home_shift);
3199
0
        assert(h != nullptr);
3200
        // An "under (de)construction" entry has a new shift amount, which
3201
        // means we have either gotten off our chain or our home shift is out
3202
        // of date. If we revert back to saved ref, we will get updated info.
3203
0
        h = read_ref_on_chain;
3204
        // Didn't make progress & retry
3205
0
        continue;
3206
0
      }
3207
0
    }
3208
3209
    // Check for end marker
3210
178k
    if (next_with_shift.IsEnd()) {
3211
      // To ensure we didn't miss anything in the chain, the end marker must
3212
      // point back to the correct home.
3213
174k
      if (LIKELY(next_with_shift.GetNext() == effective_home)) {
3214
        // Complete, clean iteration of the chain, not found.
3215
        // Clean up.
3216
174k
        if (read_ref_on_chain) {
3217
3.87k
          Unref(*read_ref_on_chain);
3218
3.87k
        }
3219
174k
        return nullptr;
3220
174k
      } else {
3221
        // Something went awry. Revert back to a safe point (if we have it)
3222
0
        h = read_ref_on_chain;
3223
        // Didn't make progress & retry
3224
0
        continue;
3225
0
      }
3226
174k
    }
3227
3228
    // Follow the next and check for full key match, home match, or neither
3229
4.47k
    h = &arr[next_with_shift.GetNext()];
3230
4.47k
    bool full_match_or_unknown = false;
3231
4.47k
    if (MatchAndRef(&hashed_key, *h, shift, effective_home,
3232
4.47k
                    &full_match_or_unknown)) {
3233
      // Got a read ref on next (h).
3234
      //
3235
      // There is a very small chance that between getting the next pointer
3236
      // (now h) and doing MatchAndRef on it, another thread erased/evicted it
3237
      // reinserted it into the same chain, causing us to cycle back in the
3238
      // same chain and potentially see some entries again if we keep walking.
3239
      // Newly-inserted entries are inserted before older ones, so we are at
3240
      // least guaranteed not to miss anything. Here in Lookup, it's just a
3241
      // transient, slight hiccup in performance.
3242
3243
4.47k
      if (full_match_or_unknown) {
3244
        // Full match.
3245
        // Release old read ref on chain if applicable
3246
0
        if (read_ref_on_chain) {
3247
          // Pretend we never took the reference.
3248
0
          Unref(*read_ref_on_chain);
3249
0
        }
3250
        // Update the hit bit
3251
0
        if (eviction_callback_) {
3252
0
          h->meta.ApplyRelaxed(SlotMeta::HitFlag::SetTransform());
3253
0
        }
3254
        // All done.
3255
0
        return h;
3256
4.47k
      } else if (UNLIKELY(shift != home_shift) &&
3257
0
                 home != BottomNBits(h->hashed_key[1], home_shift)) {
3258
        // This chain is in a Grow operation and we've landed on an entry
3259
        // that belongs to the wrong destination chain. We can keep going, but
3260
        // there's a chance we'll need to backtrack back *before* this entry,
3261
        // if the Grow finishes before this Lookup. We cannot save this entry
3262
        // for backtracking because it might soon or already be on the wrong
3263
        // chain.
3264
        // NOTE: if we simply backtrack rather than continuing, we would
3265
        // be in a wait loop (not allowed in Lookup!) until the other thread
3266
        // finishes its Grow.
3267
0
        Unref(*h);
3268
4.47k
      } else {
3269
        // Correct home location, so we are on the right chain.
3270
        // With new usable read ref, can release old one (if applicable).
3271
4.47k
        if (read_ref_on_chain) {
3272
          // Pretend we never took the reference.
3273
598
          Unref(*read_ref_on_chain);
3274
598
        }
3275
        // And keep the new one.
3276
4.47k
        read_ref_on_chain = h;
3277
4.47k
      }
3278
4.47k
    } else {
3279
2
      if (full_match_or_unknown) {
3280
        // Must have been an "under construction" entry. Can safely skip it,
3281
        // but there's a chance we'll have to backtrack later
3282
2
      } else {
3283
        // Home mismatch! Revert back to a safe point (if we have it)
3284
2
        h = read_ref_on_chain;
3285
        // Didn't make progress & retry
3286
2
      }
3287
2
    }
3288
4.47k
  }
3289
174k
}
3290
3291
17.3k
void AutoHyperClockTable::Remove(HandleImpl* h) {
3292
17.3k
  assert(h->meta.Load().IsUnderConstruction());
3293
3294
17.3k
  const HandleImpl& c_h = *h;
3295
17.3k
  PurgeImpl(&c_h.hashed_key);
3296
17.3k
}
3297
3298
bool AutoHyperClockTable::TryEraseHandle(HandleImpl* h, bool holding_ref,
3299
17.3k
                                         bool mark_invisible) {
3300
17.3k
  SlotMeta meta = h->meta.Load();
3301
17.3k
  assert(!holding_ref || meta.IsShareable());
3302
3303
  // Take ownership if no other refs, or set invisible if other refs exist (and
3304
  // mark_invisible is set).
3305
17.3k
  SlotMeta construction_meta;
3306
17.3k
  construction_meta.SetUnderConstruction();
3307
17.3k
  do {
3308
17.3k
    if (meta.GetRefcount() != uint32_t{holding_ref}) {
3309
      // Not last ref at some point in time during this call
3310
0
      if (mark_invisible) {
3311
        // Set invisible
3312
0
        h->meta.Apply(SlotMeta::VisibleFlag::ClearTransform());
3313
0
      }
3314
0
      return false;
3315
0
    }
3316
17.3k
    if (!meta.IsShareable()) {
3317
      // Someone else took ownership
3318
0
      return false;
3319
0
    }
3320
    // Note that if !holding_ref, there's a small chance that we release,
3321
    // another thread replaces this entry with another, reaches zero refs, and
3322
    // then we end up erasing that other entry. That's an acceptable risk /
3323
    // imprecision.
3324
17.3k
  } while (!h->meta.CasWeak(meta, construction_meta));
3325
  // Took ownership
3326
  // TODO? Delay freeing?
3327
17.3k
  h->FreeData(allocator_);
3328
17.3k
  size_t total_charge = h->total_charge;
3329
17.3k
  if (UNLIKELY(h->IsStandalone())) {
3330
    // Delete detached handle
3331
0
    delete h;
3332
0
    standalone_usage_.FetchSubRelaxed(total_charge);
3333
17.3k
  } else {
3334
17.3k
    Remove(h);
3335
17.3k
    MarkEmpty(*h);
3336
17.3k
    occupancy_.FetchSub(1U);
3337
17.3k
  }
3338
17.3k
  usage_.FetchSubRelaxed(total_charge);
3339
17.3k
  assert(usage_.LoadRelaxed() < SIZE_MAX / 2);
3340
17.3k
  return true;
3341
17.3k
}
3342
3343
bool AutoHyperClockTable::Release(HandleImpl* h, bool useful,
3344
164k
                                  bool erase_if_last_ref) {
3345
  // In contrast with LRUCache's Release, this function won't delete the handle
3346
  // when the cache is above capacity and the reference is the last one. Space
3347
  // is only freed up by Evict/PurgeImpl (called by Insert when space
3348
  // is needed) and Erase. We do this to avoid an extra atomic read of the
3349
  // variable usage_.
3350
3351
164k
  SlotMeta old_meta;
3352
164k
  if (useful) {
3353
    // Increment release counter to indicate was used
3354
164k
    auto add_release = ReleaseCounter::PlusTransformPromiseNoOverflow(1);
3355
164k
    h->meta.Apply(add_release, &old_meta);
3356
    // Correct for possible (but rare) overflow
3357
164k
    CorrectNearOverflow(old_meta, h->meta);
3358
164k
  } else {
3359
    // Decrement acquire counter to pretend it never happened
3360
0
    auto sub_acquire = AcquireCounter::MinusTransformPromiseNoUnderflow(1);
3361
0
    h->meta.Apply(sub_acquire, &old_meta);
3362
0
  }
3363
3364
164k
  assert(old_meta.IsShareable());
3365
  // No underflow
3366
164k
  assert(old_meta.GetAcquireCounter() != old_meta.GetReleaseCounter());
3367
3368
164k
  if ((erase_if_last_ref || UNLIKELY(old_meta.IsInvisible()))) {
3369
    // FIXME: There's a chance here that another thread could replace this
3370
    // entry and we end up erasing the wrong one.
3371
17.3k
    return TryEraseHandle(h, /*holding_ref=*/false, /*mark_invisible=*/false);
3372
147k
  } else {
3373
147k
    return false;
3374
147k
  }
3375
164k
}
3376
3377
#ifndef NDEBUG
3378
void AutoHyperClockTable::TEST_ReleaseN(HandleImpl* h, uint32_t n) {
3379
  if (n > 0) {
3380
    // Do n-1 simple releases first
3381
    TEST_ReleaseNMinus1(h, n);
3382
3383
    // Then the last release might be more involved
3384
    Release(h, /*useful*/ true, /*erase_if_last_ref*/ false);
3385
  }
3386
}
3387
#endif
3388
3389
0
void AutoHyperClockTable::Erase(const UniqueId64x2& hashed_key) {
3390
  // Don't need to be efficient.
3391
  // Might be one match masking another, so loop.
3392
0
  while (HandleImpl* h = Lookup(hashed_key)) {
3393
0
    bool gone =
3394
0
        TryEraseHandle(h, /*holding_ref=*/true, /*mark_invisible=*/true);
3395
0
    if (!gone) {
3396
      // Only marked invisible, which is ok.
3397
      // Pretend we never took the reference from Lookup.
3398
0
      Unref(*h);
3399
0
    }
3400
0
  }
3401
0
}
3402
3403
0
void AutoHyperClockTable::EraseUnRefEntries() {
3404
0
  size_t usable_size = GetTableSize();
3405
0
  for (size_t i = 0; i < usable_size; i++) {
3406
0
    HandleImpl& h = array_[i];
3407
3408
0
    SlotMeta old_meta = h.meta.LoadRelaxed();
3409
0
    if (old_meta.IsShareable() && old_meta.GetRefcount() == 0) {
3410
0
      SlotMeta construction_meta;
3411
0
      construction_meta.SetUnderConstruction();
3412
0
      if (h.meta.CasStrong(old_meta, construction_meta)) {
3413
        // Took ownership
3414
0
        h.FreeData(allocator_);
3415
0
        usage_.FetchSubRelaxed(h.total_charge);
3416
        // NOTE: could be more efficient with a dedicated variant of
3417
        // PurgeImpl, but this is not a common operation
3418
0
        Remove(&h);
3419
0
        MarkEmpty(h);
3420
0
        occupancy_.FetchSub(1U);
3421
0
      }
3422
0
    }
3423
0
  }
3424
0
}
3425
3426
void AutoHyperClockTable::Evict(size_t requested_charge, InsertState& state,
3427
0
                                EvictionData* data) {
3428
  // precondition
3429
0
  assert(requested_charge > 0);
3430
3431
  // We need the clock pointer to seemlessly "wrap around" at the end of the
3432
  // table, and to be reasonably stable under Grow operations. This is
3433
  // challenging when the linear hashing progressively opens additional
3434
  // most-significant-hash-bits in determining home locations.
3435
3436
  // TODO: make a tuning parameter?
3437
  // Up to 2x this number of homes will be evicted per step. In very rare
3438
  // cases, possibly more, as homes of an out-of-date generation will be
3439
  // resolved to multiple in a newer generation.
3440
0
  constexpr size_t step_size = 4;
3441
3442
  // A clock_pointer_mask_ field separate from length_info_ enables us to use
3443
  // the same mask (way of dividing up the space among evicting threads) for
3444
  // iterating over the whole structure before considering changing the mask
3445
  // at the beginning of each pass. This ensures we do not have a large portion
3446
  // of the space that receives redundant or missed clock updates. However,
3447
  // with two variables, for each update to clock_pointer_mask (< 64 ever in
3448
  // the life of the cache), there will be a brief period where concurrent
3449
  // eviction threads could use the old mask value, possibly causing redundant
3450
  // or missed clock updates for a *small* portion of the table.
3451
0
  size_t clock_pointer_mask = clock_pointer_mask_.LoadRelaxed();
3452
3453
0
  uint64_t max_clock_pointer = 0;  // unset
3454
3455
  // TODO: consider updating during a long eviction
3456
0
  size_t used_length = LengthInfoToUsedLength(state.saved_length_info);
3457
3458
0
  autovector<HandleImpl*> to_finish_eviction;
3459
3460
  // Loop until enough freed, or limit reached (see bottom of loop)
3461
0
  for (;;) {
3462
    // First (concurrent) increment clock pointer
3463
0
    uint64_t old_clock_pointer = clock_pointer_.FetchAddRelaxed(step_size);
3464
3465
0
    if (UNLIKELY((old_clock_pointer & clock_pointer_mask) == 0)) {
3466
      // Back at the beginning. See if clock_pointer_mask should be updated.
3467
0
      uint64_t mask = BottomNBits(
3468
0
          UINT64_MAX, LengthInfoToMinShift(state.saved_length_info));
3469
0
      if (clock_pointer_mask != mask) {
3470
0
        clock_pointer_mask = static_cast<size_t>(mask);
3471
0
        clock_pointer_mask_.StoreRelaxed(clock_pointer_mask);
3472
0
      }
3473
0
    }
3474
3475
0
    size_t major_step = clock_pointer_mask + 1;
3476
0
    assert((major_step & clock_pointer_mask) == 0);
3477
3478
0
    for (size_t base_home = old_clock_pointer & clock_pointer_mask;
3479
0
         base_home < used_length; base_home += major_step) {
3480
0
      for (size_t i = 0; i < step_size; i++) {
3481
0
        size_t home = base_home + i;
3482
0
        if (home >= used_length) {
3483
0
          break;
3484
0
        }
3485
0
        PurgeImpl(&to_finish_eviction, home, data);
3486
0
      }
3487
0
    }
3488
3489
0
    for (HandleImpl* h : to_finish_eviction) {
3490
0
      TrackAndReleaseEvictedEntry(h);
3491
      // NOTE: setting likely_empty_slot here can cause us to reduce the
3492
      // portion of "at home" entries, probably because an evicted entry
3493
      // is more likely to come back than a random new entry and would be
3494
      // unable to go into its home slot.
3495
0
    }
3496
0
    to_finish_eviction.clear();
3497
3498
    // Loop exit conditions
3499
0
    if (data->freed_charge >= requested_charge) {
3500
0
      return;
3501
0
    }
3502
3503
0
    if (max_clock_pointer == 0) {
3504
      // Cap the eviction effort at this thread (along with those operating in
3505
      // parallel) circling through the whole structure kMaxCountdown times.
3506
      // In other words, this eviction run must find something/anything that is
3507
      // unreferenced at start of and during the eviction run that isn't
3508
      // reclaimed by a concurrent eviction run.
3509
      // TODO: Does HyperClockCache need kMaxCountdown + 1?
3510
0
      max_clock_pointer =
3511
0
          old_clock_pointer +
3512
0
          (uint64_t{ClockHandle::kMaxCountdown + 1} * major_step);
3513
0
    }
3514
3515
0
    if (old_clock_pointer + step_size >= max_clock_pointer) {
3516
0
      return;
3517
0
    }
3518
3519
0
    if (IsEvictionEffortExceeded(*data)) {
3520
0
      eviction_effort_exceeded_count_.FetchAddRelaxed(1);
3521
0
      return;
3522
0
    }
3523
0
  }
3524
0
}
3525
3526
size_t AutoHyperClockTable::CalcMaxUsableLength(
3527
    size_t capacity, size_t min_avg_value_size,
3528
646k
    CacheMetadataChargePolicy metadata_charge_policy) {
3529
646k
  double min_avg_slot_charge = min_avg_value_size * kMaxLoadFactor;
3530
646k
  if (metadata_charge_policy == kFullChargeCacheMetadata) {
3531
646k
    min_avg_slot_charge += sizeof(HandleImpl);
3532
646k
  }
3533
646k
  assert(min_avg_slot_charge > 0.0);
3534
646k
  size_t num_slots =
3535
646k
      static_cast<size_t>(capacity / min_avg_slot_charge + 0.999999);
3536
3537
646k
  const size_t slots_per_page = kPresumedPageSize / sizeof(HandleImpl);
3538
3539
  // Round up to page size
3540
646k
  return ((num_slots + slots_per_page - 1) / slots_per_page) * slots_per_page;
3541
646k
}
3542
3543
namespace {
3544
0
bool IsHeadNonempty(const AutoHyperClockTable::HandleImpl& h) {
3545
0
  return !h.head_next_with_shift.LoadRelaxed().IsEnd();
3546
0
}
3547
bool IsEntryAtHome(const AutoHyperClockTable::HandleImpl& h, int shift,
3548
0
                   size_t home) {
3549
0
  if (MatchAndRef(nullptr, h, shift, home)) {
3550
0
    Unref(h);
3551
0
    return true;
3552
0
  } else {
3553
0
    return false;
3554
0
  }
3555
0
}
3556
}  // namespace
3557
3558
void AutoHyperClockCache::ReportProblems(
3559
442
    const std::shared_ptr<Logger>& info_log) const {
3560
442
  BaseHyperClockCache::ReportProblems(info_log);
3561
3562
442
  if (info_log->GetInfoLogLevel() <= InfoLogLevel::DEBUG_LEVEL) {
3563
0
    LoadVarianceStats head_stats;
3564
0
    size_t entry_at_home_count = 0;
3565
0
    uint64_t yield_count = 0;
3566
0
    this->ForEachShard([&](const Shard* shard) {
3567
0
      size_t count = shard->GetTableAddressCount();
3568
0
      uint64_t length_info = UsedLengthToLengthInfo(count);
3569
0
      for (size_t i = 0; i < count; ++i) {
3570
0
        const auto& h = *shard->GetTable().HandlePtr(i);
3571
0
        head_stats.Add(IsHeadNonempty(h));
3572
0
        int shift;
3573
0
        size_t home;
3574
0
        GetHomeIndexAndShift(length_info, i, &home, &shift);
3575
0
        assert(home == i);
3576
0
        entry_at_home_count += IsEntryAtHome(h, shift, home);
3577
0
      }
3578
0
      yield_count += shard->GetTable().GetYieldCount();
3579
0
    });
3580
0
    ROCKS_LOG_AT_LEVEL(info_log, InfoLogLevel::DEBUG_LEVEL,
3581
0
                       "Head occupancy stats: %s", head_stats.Report().c_str());
3582
0
    ROCKS_LOG_AT_LEVEL(info_log, InfoLogLevel::DEBUG_LEVEL,
3583
0
                       "Entries at home count: %zu", entry_at_home_count);
3584
0
    ROCKS_LOG_AT_LEVEL(info_log, InfoLogLevel::DEBUG_LEVEL,
3585
0
                       "Yield count: %" PRIu64, yield_count);
3586
0
  }
3587
442
}
3588
3589
}  // namespace clock_cache
3590
3591
// DEPRECATED (see public API)
3592
std::shared_ptr<Cache> NewClockCache(
3593
    size_t capacity, int num_shard_bits, bool strict_capacity_limit,
3594
0
    CacheMetadataChargePolicy metadata_charge_policy) {
3595
0
  return NewLRUCache(capacity, num_shard_bits, strict_capacity_limit,
3596
0
                     /* high_pri_pool_ratio */ 0.5, nullptr,
3597
0
                     kDefaultToAdaptiveMutex, metadata_charge_policy,
3598
0
                     /* low_pri_pool_ratio */ 0.0);
3599
0
}
3600
3601
646k
std::shared_ptr<Cache> HyperClockCacheOptions::MakeSharedCache() const {
3602
  // For sanitized options
3603
646k
  HyperClockCacheOptions opts = *this;
3604
646k
  if (opts.num_shard_bits >= 20) {
3605
0
    return nullptr;  // The cache cannot be sharded into too many fine pieces.
3606
0
  }
3607
646k
  if (opts.num_shard_bits < 0) {
3608
    // Use larger shard size to reduce risk of large entries clustering
3609
    // or skewing individual shards.
3610
646k
    constexpr size_t min_shard_size = 32U * 1024U * 1024U;
3611
646k
    opts.num_shard_bits =
3612
646k
        GetDefaultCacheShardBits(opts.capacity, min_shard_size);
3613
646k
  }
3614
646k
  std::shared_ptr<Cache> cache;
3615
646k
  if (opts.estimated_entry_charge == 0) {
3616
646k
    cache = std::make_shared<clock_cache::AutoHyperClockCache>(opts);
3617
646k
  } else {
3618
0
    cache = std::make_shared<clock_cache::FixedHyperClockCache>(opts);
3619
0
  }
3620
646k
  if (opts.secondary_cache) {
3621
0
    cache = std::make_shared<CacheWithSecondaryAdapter>(cache,
3622
0
                                                        opts.secondary_cache);
3623
0
  }
3624
646k
  return cache;
3625
646k
}
3626
3627
}  // namespace ROCKSDB_NAMESPACE