BufferProvider: setup communication structure for passing allocations

- use a base class with the lock-free queue as building block
- both `LocalMemPool` and `EngineBufferManager` are built on top,
  allowing them to exchange `Alloc` entries asynchronously
- ''Allocation requests'' include a receiver callback,
  so that a newly allocated memory block can be dispatched
This commit is contained in:
Fischlurch 2026-05-09 23:14:04 +02:00
parent 9835e837c5
commit 7c44cac3df
5 changed files with 175 additions and 55 deletions

View file

@ -13,12 +13,11 @@
/** @file engine-buffer-allocator.hpp
** Implementation of render buffer allocation for the Lumiera Engine.
** Instantiated as global service and part of the Engine, this global
** allocation coordinator is responsible to provide pre-allocated blocks
** for render buffers. These allocations are categorised by their size,
** and will be sent down to LocalMemPool instances running in each
** worker thread. This involves asynchronous messaging to avoid
** any blocking of work jobs due to central resource management.
** Memory for render buffer is allocated in blocks with a size aligned
** to the typical cache-line granularity. These allocations are maintained
** in thread-local pools and re-used to the extent possible. Excess blocks
** are sent back and collected by the EngineBufferManager, which serves
** thus as an background collector and manager for the buffer memory.
**
** @todo WIP-WIP 4/2026 The intention is to use a family of pools eventually,
** with some of the most relevant tile sizes. The EngineBufferManager
@ -46,7 +45,7 @@
#include "lib/util-quant.hpp"
#include "lib/allocator-handle.hpp"
//#include "lib/hash-value.h"
#include "vault/mem/buffhandle.hpp"
#include "vault/mem/engine-buffer-manager.hpp"
//#include "vault/mem/type-handler.hpp"
//#include "vault/mem/buffer-local-tag.hpp"
#include "lib/nocopy.hpp"
@ -93,12 +92,6 @@ namespace mem {
static_assert (lib::allo::is_Stateless_v<Allo>);
struct Alloc
{
Buff* mem;
size_t siz;
};
static constexpr size_t
numTiles (size_t sizRequest)
{

View file

@ -15,29 +15,17 @@
/** @file engine-buffer-manager.cpp
** Implementation of render buffer allocation for the Lumiera Engine.
** Instantiated as global service and part of the Engine, this global
** allocation coordinator is responsible to provide pre-allocated blocks
** for render buffers. These allocations are categorised by their size,
** and will be sent down to LocalMemPool instances running in each
** worker thread. This involves asynchronous messaging to avoid
** any blocking of work jobs due to central resource management.
**
** @todo WIP-WIP 4/2026 The intention is to use a family of pools eventually,
** with some of the most relevant tile sizes. The EngineBufferManager
** would then actually retain ownership of all allocations and just
** _lease_ them to the LocalMemPool instances. Allocations would be
** added in extents, likely also using some usage bitmap. However
** building any such kind of optimised allocator requires to conduct
** very detailed _and_ realistic performance observations; furthermore
** it is likely that we'd need some dynamic scheme to select suitable
** tile sizes and pool dimensions. Not sure if it is even possible
** to improve performance by such a scheme; with some likelihood
** the majority of the benefits will be reaped already just by
** introducing thread-local pools without wasting too much memory.
** buffer coordinator uses the [actual allocator](\ref engine-buffer-allocator.hpp)
** internally to maintain a pool of usage buffer memory blocks. Furthermore,
** both a synchronous and an asynchronous interface is provided to request
** allocations, that will be sent as \ref Alloc entries to some local memory
** pool, that is an \ref AllocReceiver, thereby passing control over these
** blocks of memory. The contract is that the local memory user has to
** return those blocks reliably to the EngineBuffrerManager, which
** itself is also an \ref AllocReceiver.
**
** @see EnginePufferManager_test
** @see LocalBufferStore_test
** @see BufferProviderProtocol_test
** @see buffer-provider.hpp
*/
@ -82,6 +70,18 @@ namespace mem {
/* === EngineBufferManager implementation === */
void
EngineBufferManager::handleAllocRequests()
{
requestQueue_.consume_all ([this](AllocRequest request)
{
REQUIRE (request.receiver);
BufferAllocator allocator;
Alloc alloc = allocator.allocate (request.sizRequest);
ENSURE (alloc.mem);
request.receiver->supply (alloc);
});
}
}} // namespace vault::mem

View file

@ -20,18 +20,15 @@
** worker thread. This involves asynchronous messaging to avoid
** any blocking of work jobs due to central resource management.
**
** @todo WIP-WIP 4/2026 The intention is to use a family of pools eventually,
** with some of the most relevant tile sizes. The EngineBufferManager
** would then actually retain ownership of all allocations and just
** _lease_ them to the LocalMemPool instances. Allocations would be
** added in extents, likely also using some usage bitmap. However
** building any such kind of optimised allocator requires to conduct
** very detailed _and_ realistic performance observations; furthermore
** it is likely that we'd need some dynamic scheme to select suitable
** tile sizes and pool dimensions. Not sure if it is even possible
** to improve performance by such a scheme; with some likelihood
** the majority of the benefits will be reaped already just by
** introducing thread-local pools without wasting too much memory.
** Besides the interface of the EngineBufferManager, this header also
** defines some common structures, like the \ref mem::Alloc, and the
** baseclass \ref AllocReceiver, with a lock-free inbound queue
** so that allocations can be supplied to a local memory manager
** and picked up from within the worker thread later.
** @remark both the LocalMemPool (for each worker thread) and the
** EngineBufferManager, as global memory service, are based on
** such an input queue, allowing to transfer control over some
** allocation from the central allocator to a local memory pool.
**
** @see EnginePufferManager_test
** @see LocalBufferStore_test
@ -50,6 +47,7 @@
#include "lib/nocopy.hpp"
//#include "lib/util.hpp"
#include <boost/lockfree/queue.hpp>
namespace vault{
@ -64,6 +62,8 @@ namespace mem {
namespace { // config and internal helpers...
const size_t INQUEUE_SIZ = 30; ///< initial size of the lock-free provision queue
}
struct Alloc
@ -73,14 +73,60 @@ namespace mem {
};
/**
* Base building block: a memory handler
* that can receive memory allocations for further use.
*/
class AllocReceiver
: util::NonCopyable
{
protected:
using InQueue = boost::lockfree::queue<Alloc>;
InQueue inQueue_;
AllocReceiver()
: inQueue_{INQUEUE_SIZ}
{ }
public:
/** Transfer control over the given allocation.
* @remark the \ref Alloc is placed into a lock-free queue;
* implementation code from a subclass will have to
* receive and dispatch those entries */
void
supply (Alloc const& alloc)
{
inQueue_.push (alloc);
}
};
/*********************************************************//**
* Core service of the Render Engine : global buffer manager.
*/
class EngineBufferManager
: util::NonCopyable
: public AllocReceiver
{
struct AllocRequest
{
AllocReceiver* receiver;
size_t sizRequest;
};
using RequestQueue = boost::lockfree::queue<AllocRequest>;
RequestQueue requestQueue_;
public:
EngineBufferManager()
: AllocReceiver{}
, requestQueue_{INQUEUE_SIZ}
{ }
private:
/** process all pending requests in the queue */
void handleAllocRequests();
};

View file

@ -75,10 +75,8 @@
#include "lib/error.hpp"
#include "vault/mem/engine-buffer-manager.hpp"
#include "lib/nocopy.hpp"
#include "lib/util.hpp"
#include <boost/lockfree/queue.hpp>
#include <algorithm>
#include <utility>
#include <string>
@ -91,8 +89,6 @@ namespace mem {
namespace { // Configuration tuning parameters
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 MISFIT_PENALTY = 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
@ -126,6 +122,7 @@ namespace mem {
* for use in a massive multithreaded environment.
*/
class LocalMemPool
: public AllocReceiver
{
struct Block
{
@ -140,17 +137,15 @@ namespace mem {
{ }
};
using InQueue = boost::lockfree::queue<Alloc>;
using BlockList = std::list<Block>;
InQueue inQueue_;
BlockList blocks_;
int32_t maxScore_{1};
public:
LocalMemPool()
: inQueue_{INQUEUE_SIZ}
: AllocReceiver{}
, blocks_{}
{ }
@ -232,7 +227,7 @@ namespace mem {
void
supply (Buff* mem, size_t siz)
{
inQueue_.push ({mem,siz});
AllocReceiver::supply ({mem,siz});
}
/**

View file

@ -146571,6 +146571,92 @@ StM_bind(Builder&lt;R1&gt; b1, Extension&lt;R1,R2&gt; extension)
<icon BUILTIN="clanbomber"/>
</node>
</node>
<node CREATED="1778372162541" ID="ID_1423477043" MODIFIED="1778372391306">
<richcontent TYPE="NODE"><html>
<head>
</head>
<body>
<p>
das ist zwar nur ein Grenzfall
</p>
<p>
&#160;&#8212; aber er stellt das gesamte Konzept in Frage
</p>
</body>
</html>
</richcontent>
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
Es handelt sich ganz klassisch um einen &#187;Race&#171; &#8212; das hei&#223;t, diese Situation ist in keinster Weise regul&#228;r vorgesehen, und d&#252;rfrte auch auch schwierig zu reproduzieren sein; sie sollte daher nicht der Ma&#223;stab sein, an dem das Konstrukt insgeseamt ausgerichtet wird. Leider aber f&#252;hrt diese Situation zur <i>memory corruption,</i>&#160;wenn wir das bisher ausgearbeitete Konzept umsetzen
</p>
</body>
</html>
</richcontent>
<node CREATED="1778372393169" ID="ID_1109770264" MODIFIED="1778372505015" TEXT="Schwierigkeit: der LocalMemPool wei&#xdf; nicht, was er angefordert hat">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
denn andernfalls k&#246;nnte man versuchen, &#252;ber eine Spezialbehandlung im Destruktor die gesendete Speicher-Anforderung irgendwie stornieren (wiewohl auch dabei noch ein Race auftreten k&#246;nnte)
</p>
</body>
</html>
</richcontent>
</node>
<node CREATED="1778372507757" ID="ID_228507573" MODIFIED="1778372578208" TEXT="eine M&#xf6;glichkeit w&#xe4;re, eine zus&#xe4;tzliche Sicherung im empfangenden Code vorzusehen">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
So &#228;hnlich wie der ZombieCheck...
</p>
</body>
</html>
</richcontent>
</node>
<node CREATED="1778372538869" ID="ID_398624236" MODIFIED="1778373145721" TEXT="diese Sicherung m&#xfc;&#xdf;te allerdings wirksam werden vor dem ersten tats&#xe4;chlichen Speicherzugriff">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
Erl&#228;uterung...
</p>
<ul>
<li>
der Callback ruft direkt oder indirekt eine Funktion auf, die den Speicher entgegennimmt
</li>
<li>
dieser Funktion wird eine &quot;self&quot;-Referenz &#252;bergeben, die aber zum Zeitpunkt des Aufrufes bereits nicht mehr valide sein k&#246;nnte
</li>
<li>
solange nur der Code l&#228;uft, und auf Parameter und statische Variable zugreift, ist diese Operation selbst dann noch fehlerfrei ausf&#252;hrbar, wenn die self-Referenz ung&#252;ltig ist
</li>
<li>
aber da die Queue an der self-Referenz h&#228;ngt, w&#252;rde beim ersten Zugriff bereits anderweitig verwendeter Speicher referenziert.
</li>
<li>
man m&#252;&#223;te also irgend eine Art von ID erzeugen, &#252;ber die sich feststellen l&#228;&#223;t, ob die self-Referenz noch g&#252;ltig ist, beispielsweise als Eintrag in einer statischen Hashtable
</li>
<li>
einfach ist das nicht, da auch dieser Check selber wieder threadsafe sein mu&#223; (allerdings ist der relevante Codepfad weder kritisch, noch strongly contended)
</li>
</ul>
</body>
</html>
</richcontent>
</node>
</node>
</node>
</node>
</node>
@ -171636,8 +171722,8 @@ std::cout &lt;&lt; tmpl.render({&quot;what&quot;, &quot;World&quot;}) &lt;&lt; s
</node>
<node CREATED="1720994187035" ID="ID_1817562586" MODIFIED="1720994190143" TEXT="Basis-Dienste">
<node CREATED="1778113273194" ID="ID_513273421" MODIFIED="1778113528245" TEXT="Async-IO">
<linktarget COLOR="#323ccd" DESTINATION="ID_513273421" ENDARROW="Default" ENDINCLINATION="-1630;-298;" ID="Arrow_ID_1806410644" SOURCE="ID_719985737" STARTARROW="None" STARTINCLINATION="-1524;159;"/>
<linktarget COLOR="#392ba1" DESTINATION="ID_513273421" ENDARROW="Default" ENDINCLINATION="-1790;-510;" ID="Arrow_ID_1192232127" SOURCE="ID_977105409" STARTARROW="None" STARTINCLINATION="2404;307;"/>
<linktarget COLOR="#323ccd" DESTINATION="ID_513273421" ENDARROW="Default" ENDINCLINATION="-1630;-298;" ID="Arrow_ID_1806410644" SOURCE="ID_719985737" STARTARROW="None" STARTINCLINATION="-1524;159;"/>
</node>
</node>
</node>