From 5fafb16807b0c2cf8515dff31cfcc1ebfce8ac2b Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Sun, 26 Apr 2026 20:24:50 +0200 Subject: [PATCH] BufferProvider: decide to use only a simple heuristics for reservation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At the begin of each Render Node `pull()`, the render job is expected to ''announce'' a maximum expected allocation requirement to the `BufferProvider`, which information is passed-on to the `LocalMemPool` to determine to which degree it can be satisfied with allocations from that local pool. Without actual empiric observation, it is impossible to determine the relevance of this step, how it should be resolved heuristically against the known contents of the pool, and how much effort to spend on that step. Thus I'll settle for the ''simplest thing that can possibly work'' — which is to check only for close matches and rather rely on additional allocations, forwarded asynchronously, assuming that the clean-up heuristics will deal with excess allocations quickly. --- src/vault/mem/local-mem-pool.hpp | 50 +++++++++----- tests/vault/mem/local-mem-pool-test.cpp | 19 +++++- wiki/thinkPad.ichthyo.mm | 86 ++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 17 deletions(-) diff --git a/src/vault/mem/local-mem-pool.hpp b/src/vault/mem/local-mem-pool.hpp index 2e92fd0ec..5ac8ab8e1 100644 --- a/src/vault/mem/local-mem-pool.hpp +++ b/src/vault/mem/local-mem-pool.hpp @@ -79,12 +79,14 @@ namespace mem { 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 + const double CLOSE_MATCH = 0.2; ///< fraction of wasted memory that still counts as /good match/ }//(End) config parameters and internals using std::move; using std::forward; using util::unConst; + using util::isLimited; class PoolDiagnostic; @@ -164,10 +166,14 @@ namespace mem { * An allocation promised hereby will be marked as reserved internally, * and will thus not be considered for further `reserve()` requests. * @return number of allocations of the indicated size, - * that are available immediately and have been reserved now. - * @note irrespective of any reservation, an immediate request to - * \ref retrieve() an allocation will use available memory. - * There is no _handle_ to refer to some specific reservation. + * that are available immediately and have been reserved now. + * @note only allocations larger yet comparatively close to the desired + * size are considered; instead of reserving an over-allocation, it seems + * preferable to request a fitting new one asynchronously from the global + * memory pool, given that there is still some time left until it's actually + * needed. However, irrespective of any reservation, an immediate request to + * \ref retrieve() an allocation will use any locally available memory. + * There is no _handle_ to refer to some specific reservation. */ uint reserve (uint cnt, size_t sizRequest) @@ -179,15 +185,17 @@ namespace mem { break; else if (not (block.used or block.resd) - and block.alloc.siz == sizRequest) - { // perfect match, reserve this allocation + and isLimited(0.0, 1 - double(sizRequest)/block.alloc.siz, CLOSE_MATCH)) + { // good match, reserve this allocation block.resd = true; ++reserved; } - if (reserved < cnt) - { // now look for imperfect matches - TODO ("how to select best matches?"); - } + /* Note we did not consider to use much larger blocks + * even while they might be used to satisfy the request. + * In theory, we could even sort matches by amount of waste, + * but it's doubtful this would be better than requesting + * a really fitting new allocation from the global manager. + */ return reserved; } @@ -264,7 +272,7 @@ namespace mem { uint removed{0}; for (auto pos = blocks_.begin() ; pos != blocks_.end() - ; ++pos) + ; ) if (cnt == 0) break; else @@ -272,10 +280,13 @@ namespace mem { and siz == pos->alloc.siz) { consumer (pos->alloc.mem, pos->alloc.siz); - blocks_.erase (pos); + pos = blocks_.erase (pos); ++removed; --cnt; } + else + ++pos; + return removed; } @@ -296,16 +307,19 @@ namespace mem { 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); + pos = blocks_.erase (pos); ++removed; } else - pos->score -= killLevel; + { + pos->score -= killLevel; + ++pos; + } // recalibrate all score levels maxScore_ -= killLevel; return removed; @@ -403,6 +417,12 @@ namespace mem { return memPool_.empty(); } + size_t + size() + { + return memPool_.size(); + } + size_t cnt (size_t siz) { diff --git a/tests/vault/mem/local-mem-pool-test.cpp b/tests/vault/mem/local-mem-pool-test.cpp index 710249cd8..81b23b25a 100644 --- a/tests/vault/mem/local-mem-pool-test.cpp +++ b/tests/vault/mem/local-mem-pool-test.cpp @@ -20,13 +20,17 @@ #include "test/test-helper.hpp" //#include "vault/mem/engine-buffer-metadata.hpp" #include "vault/mem/local-mem-pool.hpp" +#include "lib/iter-stack.hpp" //#include "lib/depend-inject.hpp" //#include "lib/thread.hpp" //#include "lib/error.hpp" +#include "lib/format-util.hpp" +#include "test/diagnostic-output.hpp" //using std::this_thread::yield; //using lib::Thread; +using util::join; namespace vault { namespace mem { @@ -117,7 +121,7 @@ namespace test { pool.add (MEM3, SIZ30); CHECK (pool.canServe(SIZ20)); CHECK (pool.canServe(SIZ10)); - CHECK (pool.size() == 3); + CHECK (watch(pool).size() == 3); CHECK (watch(pool).cnt(SIZ10) == 1); CHECK (watch(pool).cnt(SIZ20) == 1); CHECK (watch(pool).cnt(SIZ30) == 1); @@ -130,10 +134,23 @@ namespace test { auto [m2,s2] = pool.retrieve (SIZ10); CHECK (SIZ10 == s2); CHECK (MEM1 == m2); + CHECK (watch(pool).size() == 3); CHECK (watch(pool).cntFree() == 1); CHECK (pool.canServe(SIZ10)); CHECK (pool.canServe(SIZ20)); CHECK (pool.canServe(SIZ30)); + + pool.reAdd (m2); + pool.reAdd (m1); + CHECK (watch(pool).cntFree() == 3); + + lib::IterQueue returned; + uint cnt = pool.cleanup(0.5 + ,[&](auto, size_t siz){ returned.feed (siz);}); + CHECK (cnt == 1); + CHECK (join(returned) == "30"_expect); + CHECK (watch(pool).cntFree() == 2); + CHECK (watch(pool).size() == 2); } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 20c8df119..a9881a103 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -145909,7 +145909,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -145985,6 +145985,90 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + +

+ sonst besteht die Gefahr, sehr große Blöcke für kleine Anforderungen zu verschwenden und dann nachfolgend größere Allokationen anforern zu müssen, obwohl eigentlich genügend Storage lokal schon da wäre +

+ + +
+
+ + + + + + +

+ Schwierige Entscheidung: Laufzeit-Trend ist nicht bekannt +

+ + +
+ + + + + +

+ Insofern weiß man nicht einmal, ob der Aufwand hier bedeutsam ist, oder man einfach einen Vector sortieren könnte +

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

+ Das Problem hierbei ist, daß wir dafür bereits Aufwand treiben müssen, der u.U den Aufwand für das Sortieren übersteigt +

+ + +
+
+ + + + + + +

+ Da wir letztlich sowiso einen Pool-Allokator haben, kann es durchaus sinnvoll sein, neue Allokationen zu holen, und die unpassenden alten Allokationen zeitnah aber asynchron zum Pool zurückzuschicken +

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