BufferProvider: devise a heuristic for buffer selection

- take into account how much memory will be wasted when
  using some allocation to satisfy a (smaller) allocation request
- furthermore prefer allocations used frequently in the past
- when an allocation is used, tag it with a score that also
  reflects this degree of waste
- select buffers with low score first for clean-up
This commit is contained in:
Fischlurch 2026-04-25 23:56:13 +02:00
parent dd37600c4d
commit b59cb12974
3 changed files with 202 additions and 19 deletions

View file

@ -13,6 +13,31 @@
/** @file local-mem-pool.hpp /** @file local-mem-pool.hpp
** A local cache of buffer allocations to facilitate reuse of memory in a worker. ** 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-mem-pool-test.cpp
** @see local-buffer-store.hpp ** @see local-buffer-store.hpp
@ -51,6 +76,10 @@ namespace mem {
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 }//(End) config parameters and internals
using std::move; using std::move;
@ -92,6 +121,8 @@ namespace mem {
InQueue inQueue_; InQueue inQueue_;
BlockList blocks_; BlockList blocks_;
int32_t maxScore_{1};
public: public:
LocalMemPool() LocalMemPool()
: inQueue_{INQUEUE_SIZ} : inQueue_{INQUEUE_SIZ}
@ -138,11 +169,26 @@ namespace mem {
* \ref retrieve() an allocation will use available memory. * \ref retrieve() an allocation will use available memory.
* There is no _handle_ to refer to some specific reservation. * There is no _handle_ to refer to some specific reservation.
*/ */
size_t uint
reserve (uint cnt, size_t sizRequest) reserve (uint cnt, size_t sizRequest)
{ {
ingest(); 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 else
{ {
ENSURE (not found->used); ENSURE (not found->used);
found->score += 1; ///////////////////TODO
found->resd = false; found->resd = false;
found->used = true; found->used = true;
return found->alloc; return found->alloc;
@ -212,11 +257,26 @@ namespace mem {
* @return actual number of allocations removed from the pool. * @return actual number of allocations removed from the pool.
*/ */
template<class FUN> ////////TODO requires std::invocable<FUN,Buff*,size_t> template<class FUN> ////////TODO requires std::invocable<FUN,Buff*,size_t>
size_t uint
yield (uint cnt, size_t siz, FUN&& consumer) yield (uint cnt, size_t siz, FUN&& consumer)
{ {
ingest(); 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,12 +288,29 @@ namespace mem {
* @return actual number of allocations removed from the pool. * @return actual number of allocations removed from the pool.
*/ */
template<class FUN> ////////TODO requires std::invocable<FUN,Buff*,size_t> template<class FUN> ////////TODO requires std::invocable<FUN,Buff*,size_t>
size_t uint
cleanup (double degree, FUN&& consumer) cleanup (double degree, FUN&& consumer)
{ {
ingest(); 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: private:
void void
@ -274,18 +351,32 @@ namespace mem {
selectBestMatch (size_t sizRequest) selectBestMatch (size_t sizRequest)
{ {
Block* match{nullptr}; Block* match{nullptr};
size_t waste(-1); double bestSelector{0.0};
for (Block& block : blocks_) for (Block& block : blocks_)
if (not block.used if (not block.used)
and sizRequest <= block.alloc.siz)
{ {
size_t wasted = block.alloc.siz - sizRequest; if (sizRequest > block.alloc.siz)
if (wasted < waste) block.score -= UNUSABLE_MALUS;
else
{ {
waste = wasted; 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 = &block; match = &block;
} }
} }
}
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; return match;
} }
@ -293,6 +384,8 @@ namespace mem {
}; };
class PoolDiagnostic class PoolDiagnostic
: util::MoveOnly : util::MoveOnly
{ {
@ -316,6 +409,13 @@ namespace mem {
return std::ranges::count_if (memPool_.blocks_ return std::ranges::count_if (memPool_.blocks_
,[siz](Block const& b){ return siz == b.alloc.siz; }); ,[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; });
}
}; };

View file

@ -112,6 +112,28 @@ namespace test {
CHECK (pool.canServe(SIZ10)); // query causes a scan and retrieval from the in-queu CHECK (pool.canServe(SIZ10)); // query causes a scan and retrieval from the in-queu
CHECK (watch(pool).cnt(SIZ10) == 1); CHECK (watch(pool).cnt(SIZ10) == 1);
CHECK (not watch(pool).isEmpty()); 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));
} }

View file

@ -145876,9 +145876,10 @@ StM_bind(Builder&lt;R1&gt; b1, Extension&lt;R1,R2&gt; extension)
</node> </node>
<node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1777050453251" ID="ID_1529547535" MODIFIED="1777066700207" TEXT="Implementieren"> <node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1777050453251" ID="ID_1529547535" MODIFIED="1777066700207" TEXT="Implementieren">
<icon BUILTIN="pencil"/> <icon BUILTIN="pencil"/>
<node CREATED="1777052048904" ID="ID_1403107789" MODIFIED="1777052073352" TEXT="die Eingangs-Queue wird komplett (transparent) integriert"> <node COLOR="#338800" CREATED="1777052048904" ID="ID_1403107789" MODIFIED="1777084853433" TEXT="die Eingangs-Queue wird komplett (transparent) integriert">
<icon BUILTIN="button_ok"/>
<node BACKGROUND_COLOR="#c8c0b6" COLOR="#435e98" CREATED="1777078419796" ID="ID_1966959598" MODIFIED="1777084265167" TEXT="verwende boost::lockfree::queue"/> <node BACKGROUND_COLOR="#c8c0b6" COLOR="#435e98" CREATED="1777078419796" ID="ID_1966959598" MODIFIED="1777084265167" TEXT="verwende boost::lockfree::queue"/>
<node BACKGROUND_COLOR="#f0d5c5" COLOR="#990033" CREATED="1777078428098" ID="ID_1939142827" MODIFIED="1777078435459" TEXT="ist das die richtige Wahl?"> <node BACKGROUND_COLOR="#e0ceaa" COLOR="#470f69" CREATED="1777078428098" ID="ID_1939142827" MODIFIED="1777084838373" TEXT="ist das die richtige Wahl?">
<icon BUILTIN="help"/> <icon BUILTIN="help"/>
<node CREATED="1777078440070" ID="ID_885005752" MODIFIED="1777078470502" TEXT="verlangt &#xbb;trivial assignment&#xab; und Destructor"/> <node CREATED="1777078440070" ID="ID_885005752" MODIFIED="1777078470502" TEXT="verlangt &#xbb;trivial assignment&#xab; und Destructor"/>
<node CREATED="1777078471365" ID="ID_769328039" MODIFIED="1777078487222" TEXT="hei&#xdf;t im Klartext: darf keine default-Initialiser haben"> <node CREATED="1777078471365" ID="ID_769328039" MODIFIED="1777078487222" TEXT="hei&#xdf;t im Klartext: darf keine default-Initialiser haben">
@ -145887,7 +145888,7 @@ StM_bind(Builder&lt;R1&gt; b1, Extension&lt;R1,R2&gt; extension)
</node> </node>
<node COLOR="#435e98" CREATED="1777078635327" ID="ID_238173965" MODIFIED="1777084271658" TEXT="mu&#xdf; mir selber eine passende Struct bauen"/> <node COLOR="#435e98" CREATED="1777078635327" ID="ID_238173965" MODIFIED="1777084271658" TEXT="mu&#xdf; mir selber eine passende Struct bauen"/>
</node> </node>
<node CREATED="1777078489787" ID="ID_1970735736" MODIFIED="1777084225822" TEXT="Vorteile"> <node BACKGROUND_COLOR="#c8c0b6" COLOR="#435e98" CREATED="1777078489787" ID="ID_1970735736" MODIFIED="1777084845753" TEXT="Vorteile">
<richcontent TYPE="NOTE"><html> <richcontent TYPE="NOTE"><html>
<head/> <head/>
<body> <body>
@ -145909,7 +145910,9 @@ StM_bind(Builder&lt;R1&gt; b1, Extension&lt;R1,R2&gt; extension)
</node> </node>
</node> </node>
<node CREATED="1777052358645" ID="ID_1367907395" MODIFIED="1777052396869" TEXT="zum Aufr&#xe4;umen wird ein Empf&#xe4;nger-&#x3bb; &#xfc;bergeben"/> <node CREATED="1777052358645" ID="ID_1367907395" MODIFIED="1777052396869" TEXT="zum Aufr&#xe4;umen wird ein Empf&#xe4;nger-&#x3bb; &#xfc;bergeben"/>
<node CREATED="1777052497154" ID="ID_1906132363" MODIFIED="1777052517239" TEXT="vorerst den default(-Heap)-Allokator f&#xfc;r alles verwenden"/> <node BACKGROUND_COLOR="#e0ceaa" COLOR="#620dbd" CREATED="1777052497154" ID="ID_1906132363" MODIFIED="1777084874533" TEXT="vorerst den default(-Heap)-Allokator f&#xfc;r alles verwenden">
<icon BUILTIN="yes"/>
</node>
<node CREATED="1777073876105" ID="ID_1153882258" MODIFIED="1777073995987" TEXT="stelle fest: LinkedElements sind nicht gut geeignet"> <node CREATED="1777073876105" ID="ID_1153882258" MODIFIED="1777073995987" TEXT="stelle fest: LinkedElements sind nicht gut geeignet">
<richcontent TYPE="NOTE"><html> <richcontent TYPE="NOTE"><html>
<head/> <head/>
@ -145924,6 +145927,64 @@ StM_bind(Builder&lt;R1&gt; b1, Extension&lt;R1,R2&gt; extension)
</node> </node>
<node CREATED="1777074521660" ID="ID_1763982951" MODIFIED="1777074532534" TEXT="grunds&#xe4;tzlich: gehe von einer kleinen Anzahl an Eintr&#xe4;gen aus"/> <node CREATED="1777074521660" ID="ID_1763982951" MODIFIED="1777074532534" TEXT="grunds&#xe4;tzlich: gehe von einer kleinen Anzahl an Eintr&#xe4;gen aus"/>
</node> </node>
<node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1777159795008" ID="ID_851031995" MODIFIED="1777168836233" TEXT="Heuristik zum Aufr&#xe4;umen schaffen">
<icon BUILTIN="pencil"/>
<node CREATED="1777160636295" ID="ID_1174478817" MODIFIED="1777160642510" TEXT="gew&#xfc;nschter Effekt">
<node CREATED="1777160710978" ID="ID_393248452" MODIFIED="1777160729106" TEXT="m&#xf6;glichst wenige und m&#xf6;glichst gut passende Buffer lokal halten"/>
<node CREATED="1777162022979" ID="ID_763110420" MODIFIED="1777162035764" TEXT="Prio-1 ist stets den am besten passenden Buffer zu verwenden"/>
<node CREATED="1777162072416" ID="ID_133387683" MODIFIED="1777162520272" TEXT="allerdings soll unter einem gewissen &#xbb;misfit&#xab;-Level dann MRU wichtiger werden">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
denn sonst h&#228;lt nach einiger Zeit jeder Pool eine Menge selten genutzter Buffer, obwohl man mit einem Bruchteil davon auch &#252;ber die Runden kommen k&#246;nnte
</p>
</body>
</html>
</richcontent>
</node>
</node>
<node CREATED="1777162521383" ID="ID_215039061" MODIFIED="1777162527306" TEXT="Ansatz">
<node CREATED="1777162528726" ID="ID_875736163" MODIFIED="1777162538808" TEXT="vergebe einen Score f&#xfc;r erfolgreichen Match"/>
<node CREATED="1777162540196" ID="ID_1866543398" MODIFIED="1777162577399" TEXT="reduziere den Score f&#xfc;r zu gro&#xdf;en Buffer"/>
<node CREATED="1777162589638" ID="ID_175029995" MODIFIED="1777162673190" TEXT="ziehe Punkte ab f&#xfc;r zu kleinen (unbauchbaren) Buffer"/>
<node CREATED="1777162681518" ID="ID_1045190433" MODIFIED="1777162711729" TEXT="clean-up verwirft Buffer,,,">
<node CREATED="1777162712892" ID="ID_1388888751" MODIFIED="1777162719520" TEXT="die nicht reserviert oder belegt sind"/>
<node CREATED="1777162720405" ID="ID_718342875" MODIFIED="1777162812203" TEXT="deren Score prozentual unterhalb dem Max-Score liegen">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
wobei dieser Prozentsatz aus dem <font face="Monospaced" color="#683d3d">degree</font>&#160;- Parameter abgeleittet wird (mit umgekehrter Orientierung)
</p>
</body>
</html>
</richcontent>
</node>
</node>
<node CREATED="1777162841060" ID="ID_1652390427" MODIFIED="1777162844852" TEXT="Punkts&#xe4;tze">
<node CREATED="1777162851571" ID="ID_1498523122" MODIFIED="1777162858662" TEXT="+10 Punkte f&#xfc;r erfolgreichen Gebrauch"/>
<node CREATED="1777162861786" ID="ID_990114851" MODIFIED="1777162887674" TEXT="bis zu 9 Punkte Abzug davon f&#xfc;r zu gro&#xdf;en Buffer"/>
<node CREATED="1777162888442" ID="ID_416693693" MODIFIED="1777162913895" TEXT="jeder unbrauchbare Buffer bekommt einen Malus von -2"/>
<node CREATED="1777162914715" ID="ID_577486589" MODIFIED="1777162923685" TEXT="Score kann nicht negativ werden"/>
</node>
</node>
<node CREATED="1777162988481" ID="ID_1170538606" MODIFIED="1777162995671" TEXT="Implementierung">
<node CREATED="1777162997152" ID="ID_833672862" MODIFIED="1777163012266" TEXT="berechne f&#xfc;r jeden Buffer einen waste-Score (0..9)"/>
<node CREATED="1777163103258" ID="ID_1723954782" MODIFIED="1777163118028" TEXT="oder besser: eine matchQuality 1..10"/>
<node CREATED="1777164416380" ID="ID_740262476" MODIFIED="1777164435189" TEXT="auf diese wende ich den allgemeinen Score graduell an">
<node CREATED="1777164437720" ID="ID_712610709" MODIFIED="1777164452490" TEXT="und zwar mit steigendem Gewicht f&#xfc;r schlechteren Match"/>
<node CREATED="1777164453966" ID="ID_1158139935" MODIFIED="1777164476724" TEXT="und als Malus f&#xfc;r seltener verwendete Buffer"/>
</node>
<node CREATED="1777164501370" ID="ID_1271851142" MODIFIED="1777164513738" TEXT="daraus ergibt sich ein transienter selectionScore"/>
<node CREATED="1777164514849" ID="ID_489761092" MODIFIED="1777164525297" TEXT="und gew&#xe4;hlt wird nach maximalem selectionScore"/>
<node CREATED="1777164528024" ID="ID_1047876896" MODIFIED="1777164541639" TEXT="dieser gew&#xe4;hlte Buffer bekommt dann einen Score-Update"/>
</node>
</node>
</node> </node>
<node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1777050457640" ID="ID_55967690" MODIFIED="1777084249904" TEXT="Dokumentieren per Test"> <node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1777050457640" ID="ID_55967690" MODIFIED="1777084249904" TEXT="Dokumentieren per Test">
<icon BUILTIN="pencil"/> <icon BUILTIN="pencil"/>