From c4db787b25f9c96794d41c1f219f4ae480b96724 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Sat, 16 May 2026 23:06:30 +0200 Subject: [PATCH] BufferProvider: planning of heuristics and implementation Most relevant parts of the `LocalBufferStore` are already available and just need to be connected. One point poses a challenge however: when and how to trigger the heuristic clean-up of the local pool, so that ''on average neither too much memory is wasted, nor to much unnecessary allocations'' are performed... Signed-off-by: Ichthyostega --- src/vault/mem/heap-mem-buffer-store.cpp | 2 +- src/vault/mem/local-buffer-store.hpp | 33 ++ tests/vault/mem/local-buffer-store-test.cpp | 123 ++++++- wiki/thinkPad.ichthyo.mm | 334 ++++++++++++++++++-- 4 files changed, 458 insertions(+), 34 deletions(-) diff --git a/src/vault/mem/heap-mem-buffer-store.cpp b/src/vault/mem/heap-mem-buffer-store.cpp index aaaace954..aa0c03ffe 100644 --- a/src/vault/mem/heap-mem-buffer-store.cpp +++ b/src/vault/mem/heap-mem-buffer-store.cpp @@ -196,7 +196,7 @@ namespace mem { - /* ==== Implementation of the BufferProvider interface ==== */ + /* ==== Implementation of the BufferStore interface ==== */ /** @note pre-anouncement is ignored */ uint diff --git a/src/vault/mem/local-buffer-store.hpp b/src/vault/mem/local-buffer-store.hpp index 39806e1f8..df842973f 100644 --- a/src/vault/mem/local-buffer-store.hpp +++ b/src/vault/mem/local-buffer-store.hpp @@ -46,6 +46,39 @@ namespace mem { public: LocalBufferStore() { } + + + /* ==== Implementation of the BufferStore interface ==== */ + + uint + prepareBuffers (HashVal, uint numBuffers, size_t) override + { + UNIMPLEMENTED ("announce"); + } + + + /** API: create a new buffer allocation */ + BuffAlloc + provideBuffer (HashVal, size_t buffSiz, LocalTag specifics, int64_t) override + { + UNIMPLEMENTED ("retrieve alloc"); + } + + + /** API: mark a buffer as _emitted_ */ + void + mark_emitted (HashVal, BuffAlloc storageSlot) override + { + UNIMPLEMENTED ("emit"); + } + + + /** API: mark a buffer as officially discarded */ + void + detachBuffer (HashVal, BuffAlloc storageSlot) override + { + UNIMPLEMENTED ("reclaim alloc"); + } }; diff --git a/tests/vault/mem/local-buffer-store-test.cpp b/tests/vault/mem/local-buffer-store-test.cpp index 2b65822c7..739b4e067 100644 --- a/tests/vault/mem/local-buffer-store-test.cpp +++ b/tests/vault/mem/local-buffer-store-test.cpp @@ -20,13 +20,18 @@ #include "test/test-helper.hpp" //#include "vault/mem/engine-buffer-metadata.hpp" #include "vault/mem/local-buffer-store.hpp" -//#include "lib/depend-inject.hpp" -//#include "lib/thread.hpp" +#include "vault/mem/engine-buffer-manager.hpp" +#include "vault/mem/engine-buffer-allocator.hpp" +#include "lib/depend-inject.hpp" +#include "lib/thread.hpp" //#include "lib/error.hpp" +#include "lib/util.hpp" +#include -//using std::this_thread::yield; -//using lib::Thread; +using util::isSameAdr; +using std::this_thread::yield; +using lib::Thread; namespace vault { namespace mem { @@ -61,17 +66,99 @@ namespace test { virtual void run (Arg) { - seedRand(); +// seedRand(); - verify_DataTransfer(); - verify_lulz(); + verify_API(); + verify_announce(); + verify_allocate(); + verify_release(); + verify_heuristic(); } /** @test */ void - verify_DataTransfer() + verify_API() + { + // provide a »central BufferPool« (as a transient instance for this test) + lib::DependInject::Local globalPool; + + // provide the *test subject*, + // which implements the BufferStage API + // and uses a thread-local metadata table internally + LocalBufferStore localStore; + BufferProviderSetup::Store& storeAPI = localStore; + + const size_t BUFFSIZ = 555; + const HashVal TYPEID = 12345; + const size_t CNT = 5; + const int64_t ARG = 7; + const LocalTag MARK{42}; + + const size_t ALLOC = HeapBufferAllocator::numTiles(BUFFSIZ) * HeapBufferAllocator::TILE_SIZ; + + Thread testWorker{[&] /* === Play through one typical usage cycle === */ // ◁─────────────────┨ bla + { + uint avail = storeAPI.prepareBuffers(TYPEID, CNT, BUFFSIZ); + CHECK (0 == avail); + + BuffAlloc storageSlot = storeAPI.provideBuffer (TYPEID, BUFFSIZ, MARK, ARG); + auto& [storage,buffSiz,specifics] = storageSlot; + CHECK (storage); + CHECK (BUFFSIZ <= buffSiz); + CHECK (ALLOC == buffSiz); + CHECK (MARK == specifics); + + // can use that memory + char* buf = reinterpret_cast (storage); + std::string ranS{lib::randStr(555-1)}; + auto* p = std::copy (ranS.begin(), ranS.end(), buf); + CHECK (ranS.length() == size_t(p - buf)); + *p = '\0'; + CHECK (ranS == buf); + + storeAPI.mark_emitted (TYPEID, storageSlot); + storeAPI.detachBuffer (TYPEID, storageSlot); + + avail = storeAPI.prepareBuffers(TYPEID, 2, BUFFSIZ); + CHECK (1 == avail); + + BuffAlloc slot1 = storeAPI.provideBuffer (TYPEID, BUFFSIZ, MARK, ARG); + auto& [sto1,siz1,tag1] = slot1; + BuffAlloc slot2 = storeAPI.provideBuffer (TYPEID, BUFFSIZ, MARK, ARG); + auto& [sto2,siz2,tag2] = slot2; + CHECK (sto1); + CHECK (sto2); + CHECK (isSameAdr (storage, sto1)); + CHECK (not isSameAdr (storage, sto1)); + CHECK (BUFFSIZ <= siz1); + CHECK (BUFFSIZ <= siz2); + + storeAPI.detachBuffer (TYPEID, slot2); + storeAPI.detachBuffer (TYPEID, slot1); + }}; // LocalMemPool winds down at end of thread + while (testWorker) + yield(); // wait for worker to finish + + CHECK (globalPool); // the Mock was accessed (and thus instantiated) + + // before shutdown, LocalMemPool has sent back all allocations; + // these should sit now in the EngineBufferManager's input queue + CHECK ( CNT+1 == watch(*globalPool).numAllocs()); + CHECK ((CNT+1)*ALLOC == watch(*globalPool).bytesLeased()); + + // process in-queue and retrieve all allocations.. + globalPool->processPendingRequests(); + CHECK (0 == watch(*globalPool).bytesLeased()); + } + + + + /** @test + */ + void + verify_announce() { } @@ -80,7 +167,25 @@ namespace test { /** @test */ void - verify_lulz() + verify_allocate() + { + } + + + + /** @test + */ + void + verify_release() + { + } + + + + /** @test + */ + void + verify_heuristic() { } }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 265f4bb0a..6dff95641 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -143862,6 +143862,26 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + +

+ tatsächlich verfolge ich jetzt aber eine elegante andere Lösung.... +

+ +
+ + + +

+ ...wodurch die Worker-Threads diese Aufgabe probabilistisch mit erledigen: wannimmer ein Worker eine Allokation angefordert hat, diese aber noch nicht zugestellt wurde, macht er mit globalem Lock eine Runde der Allokations-Arbeit +

+ +
+ + +
@@ -143984,7 +144004,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -143994,9 +144014,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + - + @@ -144006,7 +144026,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -144019,7 +144039,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -144028,8 +144048,20 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)

+ - + + + + + +

+ ...habe mich ganz schön sauber in den internen Problemen und Widersprüchen des Design-Plans verstrickt, so daß ich letztlich die Struktur bereits weitgehend ausarbeiten mußte, um den Knoten aufzulösen... +

+ +
+ +
@@ -144813,6 +144845,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + @@ -144910,13 +144943,253 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + + + +

+ ...die nur die minimalen Informationen enthalten, also die Speicheradresse und -Größe. Solche Allokationen gehören  stets dem EngineBufferManager, können aber verschickt und damit »verliehen« werden +

+ +
+
+ +
- + + + + - + + + + + + + + + + + + + + +

+ Auch der LocalMemPool arbeitet auf dieser Basis: er hat eine lineare Liste (die als klein angenommen wird, so daß lineare Suche genügt), in der eine Allokation wiedergefunden wird +

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

+ Es würden dann oft gar keine Blöcke zurückgesandt, sofern die meisten freien Blöcke einen guten Score haben +

+ +
+
+ + + + +

+ ein reservierter (oder verwendeter) Block ist festgehalten, selbst wenn er keinen guten Score hat; erst nachdem er einmal tatsächlich gebraucht wurde, kommt er wieder in Zirkulation +

+ +
+
+ + + + +

+ Es müßt nach jeder de-Allokation die ganze Blockliste gescannt werden, wobei erwartungsgemäß nach den meisten Scans nichts zur Freigabe gefunden wird +

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

+ ...und würde letztlich doch wieder darauf hinauslaufen, daß man irgendwie einen wachsenden Leerlauf erkennt +

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

+ jede Integral-Berechnung bedingt mindestens einen vollen Scan — und dann hätte man auch einen Cleanup-Lauf machen können. Alternativ könnte man versuchen, Integrale (wie z.B. einen Durchschintts-Score) durch Fortschreiben mitzuführen. Das macht den Code aber komplex, schwer lesbar und fehleranfällig (weil man zuverlässig jeden Zugriff und jede Änderung erfassen muß) +

+ +
+
+ + + + +

+ Grade wenn die Berechnung kurzzeitige Speicherbedarfs-Spitzen aufweist, wäre es sogar schädlich, eine ungenutzte Reserve aggressiv wieder aufzuräumen +

+ +
+
+ + + + +

+ Idealerweise würde ein heuristischer Clean-up stets am Ende eines Job erfolgen, aber nicht irgendwo mittendrin +

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

+ denn sie findet ja die selten verwendeten Buffer — und bei hinreichend gleichmäßiger Verwendung findet sie gar nichts +

+ +
+
+ + + + +

+ um zu verhindern, daß wir andauernd scannen und auch voreilig Buffer wegräumen, die Kapazitätsspitzen abfangen könnten +

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

+ einfacher Trigger: prüfen wenn der Turnover auf Null fällt +

+ +
+
+ + + + +

+ heuristischer Trigger: Max-Score einbeziehen +

+ +
+ + + + +

+ Bei perfektem Match bekommt jeder Block einmal im Turnover-Zyklus die volle Punktzahl gutgeschrieben. Wenn dagegen ein Block dominiert, dann bekommt dieser T·Punktzahl +

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -145008,10 +145281,20 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + + + + + + + + + + @@ -145045,7 +145328,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -145264,7 +145547,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -145317,7 +145601,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -145510,7 +145795,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -145921,7 +146207,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -145932,7 +146219,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)

- zwar wird das niemals einer konkreten Allokation vorgreifen, sollte aber den internen Seiten-Effekt haben daß Pool-Eingräge »beiseitegelegt« werden.  Denn sonst würden die gleichen Einträge u.U. für nachfolgende Vorankündigungen nochmal verbucht werden, wodurch dann insgesamt zu wenig zusätzlicher Speicher angefordert wird. Das bedeutet, wir brauchen nicht nur eine Flag, sondern verschiedene Grade der Reservierung + zwar wird das niemals einer konkreten Allokation vorgreifen, sollte aber den internen Seiten-Effekt haben daß Pool-Einträge »beiseitegelegt« werden.  Denn sonst würden die gleichen Einträge u.U. für nachfolgende Vorankündigungen nochmal verbucht werden, wodurch dann insgesamt zu wenig zusätzlicher Speicher angefordert wird. Das bedeutet, wir brauchen nicht nur eine Flag, sondern verschiedene Grade der Reservierung

@@ -146161,7 +146448,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
- + + @@ -146819,8 +147107,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -146877,7 +147166,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
- + @@ -146888,6 +147177,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + @@ -147123,9 +147413,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

...und das geht nicht mit dem fill()-Populator, denn dieser materialisiert alle Argumente als Kopien. Man könnte das ändern, müßte dann aber explizit ein perfect-forwarding-Tupel bauen. Das war mir jetzt alles zu mühsam @@ -147136,9 +147424,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

das ist im Besonderen relevant, wenn die captures nicht-triviale Operationen in der Initialisierung verwenden. Da der fill()-Popoulator kopiert, würden sich alle Threads das gleiche Lambda teilen, und daher auf den gleichen Werten arbeiten. Das ist eine subtile Falle