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:
parent
9835e837c5
commit
7c44cac3df
5 changed files with 175 additions and 55 deletions
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -146571,6 +146571,92 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> 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>
|
||||
 — 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 »Race« — das heißt, diese Situation ist in keinster Weise regulär vorgesehen, und dürfrte auch auch schwierig zu reproduzieren sein; sie sollte daher nicht der Maßstab sein, an dem das Konstrukt insgeseamt ausgerichtet wird. Leider aber führt diese Situation zur <i>memory corruption,</i> wenn wir das bisher ausgearbeitete Konzept umsetzen
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
</richcontent>
|
||||
<node CREATED="1778372393169" ID="ID_1109770264" MODIFIED="1778372505015" TEXT="Schwierigkeit: der LocalMemPool weiß nicht, was er angefordert hat">
|
||||
<richcontent TYPE="NOTE"><html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
denn andernfalls könnte man versuchen, über eine Spezialbehandlung im Destruktor die gesendete Speicher-Anforderung irgendwie stornieren (wiewohl auch dabei noch ein Race auftreten könnte)
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
</richcontent>
|
||||
</node>
|
||||
<node CREATED="1778372507757" ID="ID_228507573" MODIFIED="1778372578208" TEXT="eine Möglichkeit wäre, eine zusätzliche Sicherung im empfangenden Code vorzusehen">
|
||||
<richcontent TYPE="NOTE"><html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
So ähnlich wie der ZombieCheck...
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
</richcontent>
|
||||
</node>
|
||||
<node CREATED="1778372538869" ID="ID_398624236" MODIFIED="1778373145721" TEXT="diese Sicherung müßte allerdings wirksam werden vor dem ersten tatsächlichen Speicherzugriff">
|
||||
<richcontent TYPE="NOTE"><html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
Erläuterung...
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
der Callback ruft direkt oder indirekt eine Funktion auf, die den Speicher entgegennimmt
|
||||
</li>
|
||||
<li>
|
||||
dieser Funktion wird eine "self"-Referenz übergeben, die aber zum Zeitpunkt des Aufrufes bereits nicht mehr valide sein könnte
|
||||
</li>
|
||||
<li>
|
||||
solange nur der Code läuft, und auf Parameter und statische Variable zugreift, ist diese Operation selbst dann noch fehlerfrei ausführbar, wenn die self-Referenz ungültig ist
|
||||
</li>
|
||||
<li>
|
||||
aber da die Queue an der self-Referenz hängt, würde beim ersten Zugriff bereits anderweitig verwendeter Speicher referenziert.
|
||||
</li>
|
||||
<li>
|
||||
man müßte also irgend eine Art von ID erzeugen, über die sich feststellen läßt, ob die self-Referenz noch gültig ist, beispielsweise als Eintrag in einer statischen Hashtable
|
||||
</li>
|
||||
<li>
|
||||
einfach ist das nicht, da auch dieser Check selber wieder threadsafe sein muß (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 << tmpl.render({"what", "World"}) << 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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue