Blender V4.3
memory_cache.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2024 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
9#include <atomic>
10#include <mutex>
11
12#include "BLI_concurrent_map.hh"
13#include "BLI_memory_cache.hh"
14#include "BLI_memory_counter.hh"
15#include "BLI_task.hh"
16
17namespace blender::memory_cache {
18
26 std::shared_ptr<const GenericKey> key;
28 std::shared_ptr<CachedValue> value;
31};
32
34
35struct Cache {
37
38 std::atomic<int64_t> logical_time = 0;
39 std::atomic<int64_t> approximate_limit = 1024 * 1024 * 1024;
44 std::atomic<int64_t> size_in_bytes = 0;
45
46 std::mutex global_mutex;
54};
55
57{
58 static Cache cache;
59 return cache;
60}
61
62static void try_enforce_limit();
63
64static void set_new_logical_time(const StoredValue &stored_value, const int64_t new_time)
65{
66 /* Don't want to use `std::atomic` directly in the struct, because that makes it
67 * non-movable. Could also use a non-const accessor, but that may degrade performance more.
68 * It's not necessary for correctness that the time is exactly the right value. */
69 reinterpret_cast<std::atomic<int64_t> *>(const_cast<int64_t *>(&stored_value.last_use_time))
70 ->store(new_time, std::memory_order_relaxed);
71 static_assert(sizeof(int64_t) == sizeof(std::atomic<int64_t>));
72}
73
74std::shared_ptr<CachedValue> get_base(const GenericKey &key,
75 const FunctionRef<std::unique_ptr<CachedValue>()> compute_fn)
76{
77 Cache &cache = get_cache();
78 /* "Touch" the cached value so that we know that it is still used. This makes it less likely that
79 * it is removed. */
80 const int64_t new_time = cache.logical_time.fetch_add(1, std::memory_order_relaxed);
81 {
82 /* Fast path when the value is already cached. */
84 if (cache.map.lookup(accessor, std::ref(key))) {
85 set_new_logical_time(accessor->second, new_time);
86 return accessor->second.value;
87 }
88 }
89
90 /* Compute value while no locks are held to avoid potential for dead-locks. Not using a lock also
91 * means that the value may be computed more than once, but that's still better than locking all
92 * the time. It may be possible to implement something smarter in the future. */
93 std::shared_ptr<CachedValue> result = compute_fn();
94 /* Result should be valid. Use exception to propagate error if necessary. */
95 BLI_assert(result);
96
97 {
99 const bool newly_inserted = cache.map.add(accessor, std::ref(key));
100 if (!newly_inserted) {
101 /* The value is available already. It was computed unnecessarily. Use the value created by
102 * the other thread instead. */
103 return accessor->second.value;
104 }
105 /* We want to store the key in the map, but the reference we got passed in may go out of scope.
106 * So make a storable copy of it that we use in the map. */
107 accessor->second.key = key.to_storable();
108 /* Modifying the key should be fine because the new key is equal to the original key. */
109 const_cast<std::reference_wrapper<const GenericKey> &>(accessor->first) = std::ref(
110 *accessor->second.key);
111
112 /* Store the value. Don't move, because we still want to return the value from the function. */
113 accessor->second.value = result;
114 /* Set initial logical time for the new cached entry. */
115 set_new_logical_time(accessor->second, new_time);
116
117 {
118 /* Update global data of the cache. */
119 std::lock_guard lock{cache.global_mutex};
120 memory_counter::MemoryCounter memory_counter{cache.memory};
121 accessor->second.value->count_memory(memory_counter);
122 cache.keys.append(&accessor->first.get());
123 cache.size_in_bytes = cache.memory.total_bytes;
124 }
125 }
126 /* Potentially free elements from the cache. Note, even if this would free the value we just
127 * added, it would still work correctly, because we already have a shared_ptr to it. */
129 return result;
130}
131
132void set_approximate_size_limit(const int64_t limit_in_bytes)
133{
134 Cache &cache = get_cache();
135 cache.approximate_limit = limit_in_bytes;
137}
138
139void clear()
140{
141 Cache &cache = get_cache();
142 std::lock_guard lock{cache.global_mutex};
143
144 /* It's not possible to just call a map.clear() method because that is not thread-safe. */
145 for (const GenericKey *key : cache.keys) {
146 const bool success = cache.map.remove(*key);
147 BLI_assert(success);
148 UNUSED_VARS_NDEBUG(success);
149 }
150 cache.keys.clear();
151 cache.size_in_bytes = 0;
152 cache.memory.reset();
153}
154
155static void try_enforce_limit()
156{
157 Cache &cache = get_cache();
158 const int64_t old_size = cache.size_in_bytes.load(std::memory_order_relaxed);
159 const int64_t approximate_limit = cache.approximate_limit.load(std::memory_order_relaxed);
160 if (old_size < approximate_limit) {
161 /* Nothing to do, the current cache size is still within the right limits. */
162 return;
163 }
164
165 std::lock_guard lock{cache.global_mutex};
166
167 /* Gather all the keys with their latest usage times. */
169 for (const GenericKey *key : cache.keys) {
171 if (!cache.map.lookup(accessor, *key)) {
172 continue;
173 }
174 keys_with_time.append({accessor->second.last_use_time, key});
175 }
176 /* Sort the items so that the newest keys come first. */
177 std::sort(keys_with_time.begin(), keys_with_time.end());
178 std::reverse(keys_with_time.begin(), keys_with_time.end());
179
180 /* Count used memory starting at the most recently touched element. Stop at the element when the
181 * amount became larger than the capacity. */
182 cache.memory.reset();
183 std::optional<int> first_bad_index;
184 {
185 MemoryCounter memory_counter{cache.memory};
186 for (const int i : keys_with_time.index_range()) {
187 const GenericKey &key = *keys_with_time[i].second;
189 if (!cache.map.lookup(accessor, key)) {
190 continue;
191 }
192 accessor->second.value->count_memory(memory_counter);
193 /* Undershoot a little bit. This typically results in more things being freed that have not
194 * been used in a while. The benefit is that we have to do the decision what to free less
195 * often than if we were always just freeing the minimum amount necessary. */
196 if (cache.memory.total_bytes <= approximate_limit * 0.75) {
197 continue;
198 }
199 first_bad_index = i;
200 break;
201 }
202 }
203 if (!first_bad_index) {
204 return;
205 }
206
207 /* Avoid recounting memory if the last item is not way too large and the overshoot is still ok.
208 * The alternative would be to subtract the last item from the counted memory again, but removing
209 * from #MemoryCount is not implemented yet. */
210 bool need_memory_recount = false;
211 if (cache.memory.total_bytes < approximate_limit * 1.1) {
212 *first_bad_index += 1;
213 if (*first_bad_index == keys_with_time.size()) {
214 return;
215 }
216 }
217 else {
218 need_memory_recount = true;
219 }
220
221 /* Remove elements that don't fit anymore. */
222 for (const int i : keys_with_time.index_range().drop_front(*first_bad_index)) {
223 const GenericKey &key = *keys_with_time[i].second;
224 cache.map.remove(key);
225 }
226
227 /* Update keys vector. */
228 cache.keys.clear();
229 for (const int i : keys_with_time.index_range().take_front(*first_bad_index)) {
230 cache.keys.append(keys_with_time[i].second);
231 }
232
233 if (need_memory_recount) {
234 /* Recount memory another time, because the last count does not accurately represent the actual
235 * value. */
236 cache.memory.reset();
237 MemoryCounter memory_counter{cache.memory};
238 for (const int i : keys_with_time.index_range().take_front(*first_bad_index)) {
239 const GenericKey &key = *keys_with_time[i].second;
241 if (!cache.map.lookup(accessor, key)) {
242 continue;
243 }
244 accessor->second.value->count_memory(memory_counter);
245 }
246 }
247 cache.size_in_bytes = cache.memory.total_bytes;
248}
249
250} // namespace blender::memory_cache
#define BLI_assert(a)
Definition BLI_assert.h:50
#define UNUSED_VARS_NDEBUG(...)
volatile int lock
bool lookup(Accessor &accessor, const Key &key)
bool remove(const Key &key)
bool add(Accessor &accessor, const Key &key)
virtual std::unique_ptr< GenericKey > to_storable() const =0
constexpr IndexRange take_front(int64_t n) const
constexpr IndexRange drop_front(int64_t n) const
int64_t size() const
void append(const T &value)
IndexRange index_range() const
static void try_enforce_limit()
void set_approximate_size_limit(int64_t limit_in_bytes)
std::shared_ptr< CachedValue > get_base(const GenericKey &key, FunctionRef< std::unique_ptr< CachedValue >()> compute_fn)
static void set_new_logical_time(const StoredValue &stored_value, const int64_t new_time)
static Cache & get_cache()
__int64 int64_t
Definition stdint.h:89
Vector< const GenericKey * > keys
std::atomic< int64_t > approximate_limit
std::atomic< int64_t > size_in_bytes
std::atomic< int64_t > logical_time
std::shared_ptr< CachedValue > value
std::shared_ptr< const GenericKey > key