diff --git a/src/vault/mem/local-mem-pool.hpp b/src/vault/mem/local-mem-pool.hpp index b4c22b547..2e92fd0ec 100644 --- a/src/vault/mem/local-mem-pool.hpp +++ b/src/vault/mem/local-mem-pool.hpp @@ -13,6 +13,31 @@ /** @file local-mem-pool.hpp ** A local cache of buffer allocations to facilitate reuse of memory in a worker. + ** Since the Lumiera Render Engine relies on scheduled jobs running concurrently, + ** it is essential to avoid any kind of locking that might prevent the calculations + ** from moving past each other. One crucial functionality that might lead to such + ** a subtle cross-wise obstruction is the handling of buffer memory allocations. + ** Each worker thread in the engine thus needs a (small) thread-local pool of + ** buffer allocations to handle all foreseeable allocation requests. Whenever + ** a render job starts, the information regarding a _maximal allocation footprint_ + ** can be retrieved from the ProcNode, since this information is determined solely + ** by the expected call topology. Based on this information, additional memory + ** can be requested from a global buffer memory allocator; and while any new + ** allocation will take some time, the alloted buffer entries can be sent down + ** to the worker's local allocation pool through a lock-free queue, so that — + ** hopefully — these entries have already arrived at the point when the memory + ** is actually required from within the recursive Render Node `pull()` call. + ** + ** LocalMemPool is responsible for receiving those memory provisions and for + ** managing and assigning available buffer blocks, drawing from the pool of + ** locally known allocations. For every new allocation request, an attempt + ** is made to find a best match within the available allotments of memory. + ** In the exceptional case however when no suitable prepared allocation + ** can be found locally, new memory must be acquired right away, since + ** render jobs are assumed to run through without blocking waits. + ** + ** To avoid the hazard of the pool accumulating an increasing number of surplus + ** allocations over time, less frequently used blocks are culled using a heuristic. ** ** @see local-mem-pool-test.cpp ** @see local-buffer-store.hpp @@ -49,7 +74,11 @@ namespace mem { namespace { // Configuration tuning parameters - const size_t INQUEUE_SIZ = 30; ///< initial size of the lock-free provision queue + const size_t INQUEUE_SIZ = 30; ///< initial size of the lock-free provision queue + + const uint MATCH_SCORE = 10; ///< score to add when a buffer can be used to satisfy a request + const uint UNUSABLE_MALUS = 2; ///< reduce score whenever a buffer is too small to be useful + const double USAGE_WEIGHT = 0.9; ///< degree to which very frequent usage counteracts waste of memory }//(End) config parameters and internals @@ -92,6 +121,8 @@ namespace mem { InQueue inQueue_; BlockList blocks_; + int32_t maxScore_{1}; + public: LocalMemPool() : inQueue_{INQUEUE_SIZ} @@ -138,11 +169,26 @@ namespace mem { * \ref retrieve() an allocation will use available memory. * There is no _handle_ to refer to some specific reservation. */ - size_t + uint reserve (uint cnt, size_t sizRequest) { ingest(); - return 0; ///////////////////////////////////////OOO + uint reserved{0}; + for (Block& block : blocks_) + if (reserved >= cnt) + break; + else + if (not (block.used or block.resd) + and block.alloc.siz == sizRequest) + { // perfect match, reserve this allocation + block.resd = true; + ++reserved; + } + if (reserved < cnt) + { // now look for imperfect matches + TODO ("how to select best matches?"); + } + return reserved; } /** @@ -198,7 +244,6 @@ namespace mem { else { ENSURE (not found->used); - found->score += 1; ///////////////////TODO found->resd = false; found->used = true; return found->alloc; @@ -212,11 +257,26 @@ namespace mem { * @return actual number of allocations removed from the pool. */ template ////////TODO requires std::invocable - size_t + uint yield (uint cnt, size_t siz, FUN&& consumer) { ingest(); - return 0;///////////////////////OOO + uint removed{0}; + for (auto pos = blocks_.begin() + ; pos != blocks_.end() + ; ++pos) + if (cnt == 0) + break; + else + if (not pos->used + and siz == pos->alloc.siz) + { + consumer (pos->alloc.mem, pos->alloc.siz); + blocks_.erase (pos); + ++removed; + --cnt; + } + return removed; } /** @@ -228,13 +288,30 @@ namespace mem { * @return actual number of allocations removed from the pool. */ template ////////TODO requires std::invocable - size_t + uint cleanup (double degree, FUN&& consumer) { ingest(); - return 0;///////////////////////OOO + uint removed{0}; + int32_t killLevel = degree * maxScore_; + for (auto pos = blocks_.begin() + ; pos != blocks_.end() + ; ++pos) + if (not (pos->used or pos->resd) + and pos->score <= killLevel) + { // send this allocation away and remove it from the pool + consumer (pos->alloc.mem, pos->alloc.siz); + blocks_.erase (pos); + ++removed; + } + else + pos->score -= killLevel; + // recalibrate all score levels + maxScore_ -= killLevel; + return removed; } + private: void ingest() @@ -274,18 +351,32 @@ namespace mem { selectBestMatch (size_t sizRequest) { Block* match{nullptr}; - size_t waste(-1); + double bestSelector{0.0}; for (Block& block : blocks_) - if (not block.used - and sizRequest <= block.alloc.siz) + if (not block.used) { - size_t wasted = block.alloc.siz - sizRequest; - if (wasted < waste) + if (sizRequest > block.alloc.siz) + block.score -= UNUSABLE_MALUS; + else { - waste = wasted; - match = █ + double wasted = block.alloc.siz - sizRequest; + double wasteScore = wasted / block.alloc.siz; + double usageFact = block.score / maxScore_; + double selector = 1.0 - wasteScore * (1.0 - usageFact * USAGE_WEIGHT); + if (selector > bestSelector) + { + bestSelector = selector; + match = █ + } } } + if (match) + { + ENSURE (match->alloc.siz >= sizRequest); + match->score += int(MATCH_SCORE * (1 - (1 - double(sizRequest) / match->alloc.siz))); + if (match->score > maxScore_) // reduce score by waste factor as malus + maxScore_ = match->score; + } return match; } @@ -293,6 +384,8 @@ namespace mem { }; + + class PoolDiagnostic : util::MoveOnly { @@ -316,6 +409,13 @@ namespace mem { return std::ranges::count_if (memPool_.blocks_ ,[siz](Block const& b){ return siz == b.alloc.siz; }); } + + size_t + cntFree() + { + return std::ranges::count_if (memPool_.blocks_ + ,[](Block const& b){ return not b.used; }); + } }; diff --git a/tests/vault/mem/local-mem-pool-test.cpp b/tests/vault/mem/local-mem-pool-test.cpp index dc3ce10b3..710249cd8 100644 --- a/tests/vault/mem/local-mem-pool-test.cpp +++ b/tests/vault/mem/local-mem-pool-test.cpp @@ -112,6 +112,28 @@ namespace test { CHECK (pool.canServe(SIZ10)); // query causes a scan and retrieval from the in-queu CHECK (watch(pool).cnt(SIZ10) == 1); CHECK (not watch(pool).isEmpty()); + + pool.add (MEM2, SIZ20); + pool.add (MEM3, SIZ30); + CHECK (pool.canServe(SIZ20)); + CHECK (pool.canServe(SIZ10)); + CHECK (pool.size() == 3); + CHECK (watch(pool).cnt(SIZ10) == 1); + CHECK (watch(pool).cnt(SIZ20) == 1); + CHECK (watch(pool).cnt(SIZ30) == 1); + CHECK (watch(pool).cntFree() == 3); + + auto [m1,s1] = pool.retrieve (SIZ20 -2); + CHECK (SIZ20 == s1); + CHECK (MEM2 == m1); + CHECK (watch(pool).cntFree() == 2); + auto [m2,s2] = pool.retrieve (SIZ10); + CHECK (SIZ10 == s2); + CHECK (MEM1 == m2); + CHECK (watch(pool).cntFree() == 1); + CHECK (pool.canServe(SIZ10)); + CHECK (pool.canServe(SIZ20)); + CHECK (pool.canServe(SIZ30)); } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 3ce5d4e26..20c8df119 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -145876,9 +145876,10 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + - + @@ -145887,7 +145888,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -145909,7 +145910,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + + @@ -145924,6 +145927,64 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + + + +

+ denn sonst hält nach einiger Zeit jeder Pool eine Menge selten genutzter Buffer, obwohl man mit einem Bruchteil davon auch über die Runden kommen könnte +

+ + +
+
+
+ + + + + + + + + + + + +

+ wobei dieser Prozentsatz aus dem degree - Parameter abgeleittet wird (mit umgekehrter Orientierung) +

+ + +
+
+
+ + + + + + + + + + + + + + + + + + +