nk 1.0.0
C++ Numerical Computing Runtime
Loading...
Searching...
No Matches
caching_memory_resource.hpp
1// core/include/nk/core/caching_memory_resource.hpp
2//
3// A memory_resource that recycles freed *large* blocks instead of returning
4// them to the OS. Blocks of an identical (size, alignment) are pooled on a
5// free list, so a later allocation of that size reuses warm, already-committed
6// pages — eliminating the repeated VirtualAlloc/mmap + demand-zero page faults
7// that dominate big transient-array churn.
8//
9// Motivating case: the interpreter evaluating an expression like
10// `1./(1+exp(-k*(x-mid)))` over a 10^7-element array materialises several
11// 90+ MB temporaries per statement. With a plain malloc-backed resource each
12// temporary is a fresh OS reservation whose every page soft-faults on first
13// touch (~tens of ms for ~90 MB on Windows); freeing returns it to the OS, so
14// the next identical temporary faults all over again. Recycling the block
15// keeps the pages resident, turning the per-temporary cost into a memset of
16// already-committed memory (which Value::matrix performs anyway).
17//
18// Small allocations (< minCacheBytes) and any excess beyond maxCachedBytes
19// pass straight through to the upstream resource, so the cache never hoards
20// tiny blocks and its resident footprint stays bounded.
21//
22// Thread-safe: the free list is mutex-guarded, but ONLY large
23// (>= minCacheBytes) allocate/deallocate calls take the lock — small
24// allocations go straight to the (thread-safe) upstream with no locking, so
25// the hot small-object path is uncontended.
26
27#pragma once
28
29#include <cstddef>
30#include <memory_resource>
31#include <mutex>
32#include <unordered_map>
33#include <utility>
34#include <vector>
35
36namespace nk {
37
38class CachingMemoryResource : public std::pmr::memory_resource {
39public:
40 explicit CachingMemoryResource(
41 std::pmr::memory_resource *upstream,
42 std::size_t minCacheBytes = 256u * 1024u, // cache >= 256 KiB
43 std::size_t maxCachedBytes = 1024u * 1024u * 1024u) // cap 1 GiB resident
44 noexcept
45 : upstream_(upstream ? upstream : std::pmr::new_delete_resource()),
46 minCache_(minCacheBytes), maxCached_(maxCachedBytes) {}
47
48 ~CachingMemoryResource() override { releaseCached(); }
49
50 CachingMemoryResource(const CachingMemoryResource &) = delete;
51 CachingMemoryResource &operator=(const CachingMemoryResource &) = delete;
52
53 // Return every pooled block to upstream (e.g. to reclaim memory at idle).
54 void releaseCached() {
55 std::lock_guard<std::mutex> lk(mu_);
56 for (auto &kv : free_)
57 for (void *p : kv.second)
58 upstream_->deallocate(p, kv.first.first, kv.first.second);
59 free_.clear();
60 cachedBytes_ = 0;
61 }
62
63private:
64 using Key = std::pair<std::size_t, std::size_t>; // (bytes, alignment)
65 struct KeyHash {
66 std::size_t operator()(const Key &k) const noexcept {
67 return k.first * 1000003u ^ (k.second << 1);
68 }
69 };
70
71 void *do_allocate(std::size_t bytes, std::size_t align) override {
72 if (bytes >= minCache_) {
73 std::lock_guard<std::mutex> lk(mu_);
74 auto it = free_.find(Key{bytes, align});
75 if (it != free_.end() && !it->second.empty()) {
76 void *p = it->second.back();
77 it->second.pop_back();
78 cachedBytes_ -= bytes;
79 return p;
80 }
81 }
82 return upstream_->allocate(bytes, align);
83 }
84
85 void do_deallocate(void *p, std::size_t bytes, std::size_t align) override {
86 if (bytes >= minCache_) {
87 std::lock_guard<std::mutex> lk(mu_);
88 if (cachedBytes_ + bytes <= maxCached_) {
89 free_[Key{bytes, align}].push_back(p);
90 cachedBytes_ += bytes;
91 return;
92 }
93 }
94 upstream_->deallocate(p, bytes, align);
95 }
96
97 bool do_is_equal(const std::pmr::memory_resource &o) const noexcept override {
98 return this == &o;
99 }
100
101 std::pmr::memory_resource *upstream_;
102 std::size_t minCache_;
103 std::size_t maxCached_;
104 std::mutex mu_;
105 std::unordered_map<Key, std::vector<void *>, KeyHash> free_;
106 std::size_t cachedBytes_ = 0;
107};
108
109// Process-wide cached resource wrapping new_delete_resource. Used as the
110// implicit allocator for default-constructed Engines so the big transient
111// arrays the interpreter produces recycle their backing storage. Embedders
112// that pass their own memory_resource keep full control and are NOT wrapped.
113inline std::pmr::memory_resource *defaultCachedResource() {
114 static CachingMemoryResource instance(std::pmr::new_delete_resource());
115 return &instance;
116}
117
118} // namespace nk