Blender V5.0
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
8
9#include <atomic>
10#include <optional>
11
12#include "BLI_concurrent_map.hh"
13#include "BLI_memory_cache.hh"
14#include "BLI_memory_counter.hh"
15#include "BLI_mutex.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
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. */
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};
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 memory_cache::remove_if([](const GenericKey &) { return true; });
142}
143
144void remove_if(const FunctionRef<bool(const GenericKey &)> predicate)
145{
146 Cache &cache = get_cache();
147 std::lock_guard lock{cache.global_mutex};
148
149 /* Store predicate results to avoid assuming that the predicate is cheap and without side effects
150 * that must not happen more than once. */
151 Array<bool> predicate_results(cache.keys.size());
152
153 /* Recount memory of all elements that are not removed. */
154 cache.memory.reset();
156
157 for (const int64_t i : cache.keys.index_range()) {
158 const GenericKey &key = *cache.keys[i];
159 const bool ok_to_remove = predicate(key);
160 predicate_results[i] = ok_to_remove;
161
162 if (!ok_to_remove) {
163 /* The value is kept, so count its memory. */
165 if (cache.map.lookup(accessor, key)) {
166 accessor->second.value->count_memory(memory_counter);
167 continue;
168 }
170 }
171 /* The value should be removed. */
172 const bool success = cache.map.remove(key);
173 BLI_assert(success);
174 UNUSED_VARS_NDEBUG(success);
175 }
176 /* Remove all removed keys from the vector too. */
177 cache.keys.remove_if([&](const GenericKey *&key) {
178 const int64_t index = &key - cache.keys.data();
179 return predicate_results[index];
180 });
181 cache.size_in_bytes = cache.memory.total_bytes;
182}
183
184static void try_enforce_limit()
185{
186 Cache &cache = get_cache();
187 const int64_t old_size = cache.size_in_bytes.load(std::memory_order_relaxed);
188 const int64_t approximate_limit = cache.approximate_limit.load(std::memory_order_relaxed);
189 if (old_size < approximate_limit) {
190 /* Nothing to do, the current cache size is still within the right limits. */
191 return;
192 }
193
194 std::lock_guard lock{cache.global_mutex};
195
196 /* Gather all the keys with their latest usage times. */
198 for (const GenericKey *key : cache.keys) {
200 if (!cache.map.lookup(accessor, *key)) {
201 continue;
202 }
203 keys_with_time.append({accessor->second.last_use_time, key});
204 }
205 /* Sort the items so that the newest keys come first. */
206 std::sort(keys_with_time.begin(), keys_with_time.end());
207 std::reverse(keys_with_time.begin(), keys_with_time.end());
208
209 /* Count used memory starting at the most recently touched element. Stop at the element when the
210 * amount became larger than the capacity. */
211 cache.memory.reset();
212 std::optional<int> first_bad_index;
213 {
215 for (const int i : keys_with_time.index_range()) {
216 const GenericKey &key = *keys_with_time[i].second;
218 if (!cache.map.lookup(accessor, key)) {
219 continue;
220 }
221 accessor->second.value->count_memory(memory_counter);
222 /* Undershoot a little bit. This typically results in more things being freed that have not
223 * been used in a while. The benefit is that we have to do the decision what to free less
224 * often than if we were always just freeing the minimum amount necessary. */
225 if (cache.memory.total_bytes <= approximate_limit * 0.75) {
226 continue;
227 }
228 first_bad_index = i;
229 break;
230 }
231 }
232 if (!first_bad_index) {
233 return;
234 }
235
236 /* Avoid recounting memory if the last item is not way too large and the overshoot is still ok.
237 * The alternative would be to subtract the last item from the counted memory again, but removing
238 * from #MemoryCount is not implemented yet. */
239 bool need_memory_recount = false;
240 if (cache.memory.total_bytes < approximate_limit * 1.1) {
241 *first_bad_index += 1;
242 if (*first_bad_index == keys_with_time.size()) {
243 return;
244 }
245 }
246 else {
247 need_memory_recount = true;
248 }
249
250 /* Remove elements that don't fit anymore. */
251 for (const int i : keys_with_time.index_range().drop_front(*first_bad_index)) {
252 const GenericKey &key = *keys_with_time[i].second;
253 cache.map.remove(key);
254 }
255
256 /* Update keys vector. */
257 cache.keys.clear();
258 for (const int i : keys_with_time.index_range().take_front(*first_bad_index)) {
259 cache.keys.append(keys_with_time[i].second);
260 }
261
262 if (need_memory_recount) {
263 /* Recount memory another time, because the last count does not accurately represent the actual
264 * value. */
265 cache.memory.reset();
267 for (const int i : keys_with_time.index_range().take_front(*first_bad_index)) {
268 const GenericKey &key = *keys_with_time[i].second;
270 if (!cache.map.lookup(accessor, key)) {
271 continue;
272 }
273 accessor->second.value->count_memory(memory_counter);
274 }
275 }
276 cache.size_in_bytes = cache.memory.total_bytes;
277}
278
279} // namespace blender::memory_cache
#define BLI_assert_unreachable()
Definition BLI_assert.h:93
#define BLI_assert(a)
Definition BLI_assert.h:46
#define UNUSED_VARS_NDEBUG(...)
volatile int lock
long long int int64_t
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)
void remove_if(FunctionRef< bool(const GenericKey &)> predicate)
static void set_new_logical_time(const StoredValue &stored_value, const int64_t new_time)
static Cache & get_cache()
ConcurrentMap< std::reference_wrapper< const GenericKey >, StoredValue > CacheMap
std::mutex Mutex
Definition BLI_mutex.hh:47
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
i
Definition text_draw.cc:230