BufferProvider: setup implementation structure for the pool

- use boost::lockfree to implement the in-queue with new allocations
- use a std::list with an internal administrative record
- automatically ingest the in-queue prior to accessing the pool
This commit is contained in:
Fischlurch 2026-04-24 23:36:07 +02:00
parent e328018b05
commit dd37600c4d
6 changed files with 296 additions and 24 deletions

View file

@ -97,6 +97,8 @@ def configure(env):
problems.append('We need boost::lexical_cast') problems.append('We need boost::lexical_cast')
if not conf.CheckCXXHeader('boost/format.hpp'): if not conf.CheckCXXHeader('boost/format.hpp'):
problems.append('We need boost::format (header).') problems.append('We need boost::format (header).')
if not conf.CheckCXXHeader('boost/lockfree/queue.hpp'):
problems.append('We need boost::lockfree (header).')
if not conf.CheckLibWithHeader('boost_program_options','boost/program_options.hpp','C++'): if not conf.CheckLibWithHeader('boost_program_options','boost/program_options.hpp','C++'):
problems.append('We need boost::program_options (including binary lib for linking).') problems.append('We need boost::program_options (including binary lib for linking).')

View file

@ -40,7 +40,7 @@ Lumiera expects a ``standard'' desktop installation running a XServer.footnote:[
Wayland is ``just around the corner'' since 10 years now. Well, recently it Wayland is ``just around the corner'' since 10 years now. Well, recently it
seems it is _really_ just around the corner. We are aware of the necessary seems it is _really_ just around the corner. We are aware of the necessary
changes, which are not very complicated to implement (since we are using changes, which are not very complicated to implement (since we are using
GTK-3). However, as long as Wayland simply crashes or hands in our GTK-3). However, as long as Wayland simply crashes or hangs in our
everyday work setup with XFCE, we will not consider this topic everyday work setup with XFCE, we will not consider this topic
a priority. In the meantime, please report any problems a priority. In the meantime, please report any problems
you encounter with Lumiera running on XWayland. you encounter with Lumiera running on XWayland.
@ -97,7 +97,8 @@ Languages and Tools
* BOOST (listed below are the Debian package names) * BOOST (listed below are the Debian package names)
- libboost-dev (at least *1.83*) - libboost-dev (at least *1.83*)
- libboost-program-options-dev - notably including `boost::lockfree`
- libboost-program-options-dev (link dependency)
* Script languages * Script languages
- Python (*3.10*) required for building (->SCons) - Python (*3.10*) required for building (->SCons)

View file

@ -32,7 +32,7 @@
** under the assumption that the allocator is a _monostate_, and ** under the assumption that the allocator is a _monostate_, and
** exploiting _empty-base-optimisation,_ footprint is minimal. ** exploiting _empty-base-optimisation,_ footprint is minimal.
** @note this linked list container is _intrusive_ and thus needs the help ** @note this linked list container is _intrusive_ and thus needs the help
** of the element type, which must *provide a pointer member* `next`. ** of the element type, which must **provide a pointer member** `next`.
** Consequently, each such node element can not be member in ** Consequently, each such node element can not be member in
** several collections at the same time (unless they share all elements) ** several collections at the same time (unless they share all elements)
** @note as usual, any iterator relies on the continued existence and ** @note as usual, any iterator relies on the continued existence and

View file

@ -32,16 +32,30 @@
#include "vault/mem/buffhandle.hpp" //////////////TODO #include "vault/mem/buffhandle.hpp" //////////////TODO
//#include "lib/local-slice.hpp" //#include "lib/local-slice.hpp"
//#include "lib/depend.hpp" //#include "lib/depend.hpp"
#include "lib/util.hpp"
#include "lib/nocopy.hpp" #include "lib/nocopy.hpp"
#include "lib/meta/trait.hpp"//////////////TODO
#include <tuple> #include <boost/lockfree/queue.hpp>
#include <algorithm>
//#include <tuple>
#include <utility>
#include <list>
namespace vault { namespace vault {
namespace mem { namespace mem {
namespace err = lumiera::error; namespace err = lumiera::error;
using std::tuple; namespace { // Configuration tuning parameters
const size_t INQUEUE_SIZ = 30; ///< initial size of the lock-free provision queue
}//(End) config parameters and internals
using std::move;
using std::forward;
using util::unConst;
class PoolDiagnostic; class PoolDiagnostic;
@ -51,42 +65,229 @@ namespace mem {
*/ */
class LocalMemPool class LocalMemPool
{ {
struct Alloc
{
Buff* mem;
size_t siz;
};
static_assert (std::is_trivially_copy_assignable_v<Alloc>);
struct Block
{
Alloc alloc{nullptr,0};
int32_t score{0};
bool used :1 {false};
bool resd :1 {false};
Block() = default;
Block(Alloc&& alloc)
: alloc{move(alloc)}
{ }
};
static_assert (std::is_trivially_copy_assignable_v<Block>);
using InQueue = boost::lockfree::queue<Alloc>;
using BlockList = std::list<Block>;
InQueue inQueue_;
BlockList blocks_;
public: public:
LocalMemPool() LocalMemPool()
: inQueue_{INQUEUE_SIZ}
, blocks_{}
{ } { }
/* ===== Pool Access API ===== */ /* ===== Pool Access API ===== */
bool empty() const { return 0 == size(); }
size_t size() const { return 0; } ///////////////////////OOO
bool bool
canServe (size_t sizRequest) empty() const
{ {
return false; ///////////////////////////////////////OOO return blocks_.empty()
and inQueue_.empty();
} }
/**
* @return overall number of descriptors in the pool, including used ones
* @warning this operation has linear complexity
*/
size_t
size() const
{
return blocks_.size();
}
/** Query if this pool can provide some chunk of memory right away. */
bool
canServe (size_t sizRequest) const
{
unConst(this)->ingest();
return bool(anyFree (sizRequest));
}
/**
* Determine to what extent the indicated allocation request
* could be handled, given the current memory available in the pool.
* 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.
*/
size_t
reserve (uint cnt, size_t sizRequest)
{
ingest();
return 0; ///////////////////////////////////////OOO
}
/**
* Transfer discretionary power over the given allocation to this pool.
* @note this call is thread-safe and asynchronous; the pool will pick up
* the given information on the next request to serve or reserve memory.
* @remark the \a mem _must not be touched_ by any other part of the system,
* unless it is [yielded](\ref yield()) or disposed as part of clean-up.
*/
void void
add (Buff* mem, size_t siz) add (Buff* mem, size_t siz)
{ {
TODO ("add new alloc to pool"); inQueue_.push ({mem,siz});
} }
/**
* Return an used memory block back to the pool
* @param mem a chunk of memory previously handed out by \ref retrieve()
* @throw err::Logic in case the indicated allocation is not known
* @warning **not threadsafe** — invoke from worker thread only
*/
void void
reAdd (Buff* mem) reAdd (Buff* mem)
{ {
TODO ("return known alloc into pool"); ingest();
Block* block = UN_CONST (anyMatch (mem));
if (not block)
throw err::Logic{"returning unknown allocation to LocalMemPool"};
if (not block->used)
throw err::Logic{"returning known allocation that is not marked as used"};
block->used = false;
ENSURE (not block->resd);
} }
tuple<Buff*,size_t> /**
* Acquire a block of memory for active use
* @param sizRequest minimum size of storage expected
* @return a chunk of memory of indicated size, which may be larger
* than requested. The answer can be `(nullptr, 0)` in case
* the request can not be satisfied
* @note the allocation is marked as used internally,
* and must be returned to this pool after use.
* Memory can only be disposed after \ref yield()
* @warning **not threadsafe** — invoke from worker thread only
*/
Alloc
retrieve (size_t sizRequest) retrieve (size_t sizRequest)
{ {
ingest();
Block* found = selectBestMatch (sizRequest);
if (not found)
return {nullptr, 0}; return {nullptr, 0};
else
{
ENSURE (not found->used);
found->score += 1; ///////////////////TODO
found->resd = false;
found->used = true;
return found->alloc;
}
}
/**
* Yield control over the indicated number of allocations.
* @tparam FUN a consumer to receive the allocations removed from the pool
* @param siz precise size of the memory block(s) to yield
* @return actual number of allocations removed from the pool.
*/
template<class FUN> ////////TODO requires std::invocable<FUN,Buff*,size_t>
size_t
yield (uint cnt, size_t siz, FUN&& consumer)
{
ingest();
return 0;///////////////////////OOO
}
/**
* Perform a heuristic clean-up of the pool, based on usage statistics.
* @tparam FUN a consumer to receive the allocations removed from the pool
* @param degree a measure of how aggressively the clean-up should be performed;
* 1.0 indicates that every allocation must be released, whereas
* \a degree ≡ 0.5 retains all matches that were frequently successful.
* @return actual number of allocations removed from the pool.
*/
template<class FUN> ////////TODO requires std::invocable<FUN,Buff*,size_t>
size_t
cleanup (double degree, FUN&& consumer)
{
ingest();
return 0;///////////////////////OOO
} }
private: private:
void
ingest()
{
inQueue_.consume_all ([this](Alloc alloc)
{
blocks_.emplace_front (move(alloc));
});
}
template<class PRED>
Block const*
findMatch (PRED&& predicate) const
{
auto found = std::ranges::find_if (blocks_, forward<PRED> (predicate));
return found!=blocks_.end()? & *found
: nullptr;
}
Block const*
anyFree (size_t sizRequest) const
{
return findMatch ([sizRequest](Block const& b)
{
return not b.used
and sizRequest <= b.alloc.siz;
});
}
Block const*
anyMatch (Buff* mem) const
{
return findMatch ([mem](Block const& b){ return mem == b.alloc.mem; });
}
Block*
selectBestMatch (size_t sizRequest)
{
Block* match{nullptr};
size_t waste(-1);
for (Block& block : blocks_)
if (not block.used
and sizRequest <= block.alloc.siz)
{
size_t wasted = block.alloc.siz - sizRequest;
if (wasted < waste)
{
waste = wasted;
match = &block;
}
}
return match;
}
friend class PoolDiagnostic; friend class PoolDiagnostic;
}; };
@ -95,7 +296,9 @@ namespace mem {
class PoolDiagnostic class PoolDiagnostic
: util::MoveOnly : util::MoveOnly
{ {
LocalMemPool memPool_; using Block = LocalMemPool::Block;
LocalMemPool& memPool_;
public: public:
PoolDiagnostic (LocalMemPool& lmp) PoolDiagnostic (LocalMemPool& lmp)
: memPool_{lmp} : memPool_{lmp}
@ -104,13 +307,14 @@ namespace mem {
bool bool
isEmpty() isEmpty()
{ {
return true; return memPool_.empty();
} ///////////////////OOO }
size_t size_t
cnt (size_t siz) cnt (size_t siz)
{ {
return 0; ////////////////////OOO return std::ranges::count_if (memPool_.blocks_
,[siz](Block const& b){ return siz == b.alloc.siz; });
} }
}; };

View file

@ -97,7 +97,7 @@ namespace test {
/** @test /** @test allocations are accounted by size
*/ */
void void
verify_matchAlloc() verify_matchAlloc()
@ -106,7 +106,12 @@ namespace test {
CHECK (watch(pool).isEmpty()); CHECK (watch(pool).isEmpty());
pool.add (MEM1, SIZ10); pool.add (MEM1, SIZ10);
CHECK (watch(pool).cnt(SIZ10) == 0); // new allotment not ingested yet,....
CHECK (not watch(pool).isEmpty()); // however, there is an entry in the in-queue
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());
} }

View file

@ -145846,6 +145846,16 @@ StM_bind(Builder&lt;R1&gt; b1, Extension&lt;R1,R2&gt; extension)
<node CREATED="1777050440887" ID="ID_1006627416" MODIFIED="1777066692859" TEXT="Verhalten spezifizieren"> <node CREATED="1777050440887" ID="ID_1006627416" MODIFIED="1777066692859" TEXT="Verhalten spezifizieren">
<icon BUILTIN="yes"/> <icon BUILTIN="yes"/>
<node CREATED="1777050520227" ID="ID_796153621" MODIFIED="1777050524094" TEXT="Use-Cases"> <node CREATED="1777050520227" ID="ID_796153621" MODIFIED="1777050524094" TEXT="Use-Cases">
<node CREATED="1777068198295" ID="ID_1010651012" MODIFIED="1777068390012" TEXT="Allokationsbedarf ank&#xfc;ndigen">
<richcontent TYPE="NOTE"><html>
<head/>
<body>
<p>
zwar wird das niemals einer konkreten Allokation vorgreifen, sollte aber <i>den internen Seiten-Effekt haben da&#223; Pool-Eingr&#228;ge &#187;beiseitegelegt&#171; werden.</i>&#160; Denn sonst w&#252;rden die gleichen Eintr&#228;ge u.U. f&#252;r nachfolgende Vorank&#252;ndigungen nochmal verbucht werden, wodurch dann insgesamt zu wenig zus&#228;tzlicher Speicher angefordert wird. Das bedeutet, wir brauchen nicht nur eine Flag, sondern verschiedene Grade der Reservierung
</p>
</body>
</html></richcontent>
</node>
<node CREATED="1777051227060" ID="ID_847462254" MODIFIED="1777051382666" TEXT="Allokation in den Pool &#xfc;bernehmen"/> <node CREATED="1777051227060" ID="ID_847462254" MODIFIED="1777051382666" TEXT="Allokation in den Pool &#xfc;bernehmen"/>
<node CREATED="1777051616687" ID="ID_142096290" MODIFIED="1777051638112" TEXT="Allokation aus dem Pool entnehmen"/> <node CREATED="1777051616687" ID="ID_142096290" MODIFIED="1777051638112" TEXT="Allokation aus dem Pool entnehmen"/>
<node CREATED="1777051746358" ID="ID_658492297" MODIFIED="1777051798530" TEXT="Allokation in den Pool zur&#xfc;ckgeben"/> <node CREATED="1777051746358" ID="ID_658492297" MODIFIED="1777051798530" TEXT="Allokation in den Pool zur&#xfc;ckgeben"/>
@ -145866,14 +145876,64 @@ 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 CREATED="1777052048904" ID="ID_1403107789" MODIFIED="1777052073352" TEXT="die Eingangs-Queue wird komplett (transparent) integriert">
<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?">
<icon BUILTIN="help"/>
<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 BACKGROUND_COLOR="#e0ceaa" COLOR="#690f14" CREATED="1777078531972" ID="ID_921695652" LINK="https://stackoverflow.com/a/38780211/444796" MODIFIED="1777084274162" TEXT="Konkret: kein std::tuple">
<icon BUILTIN="messagebox_warning"/>
</node>
<node COLOR="#435e98" CREATED="1777078635327" ID="ID_238173965" MODIFIED="1777084271658" TEXT="mu&#xdf; mir selber eine passende Struct bauen"/>
</node>
<node CREATED="1777078489787" ID="ID_1970735736" MODIFIED="1777084225822" TEXT="Vorteile">
<richcontent TYPE="NOTE"><html>
<head/>
<body>
<p>
...da es sich um einen Prototyp handelt, halte ich diese Vorteile f&#252;r sehr wichtig; im Besonderen das mit dem single producer/consumer l&#228;&#223;t sich nicht ohne Weiteres sicherstellen (es ergibt sich hier nur indirekt, indem man die Klasse dann thread_local definiert und auf der anderen Seite einen zentralen Allokations-Manager als Singleton hat. Aber selbst dann k&#246;nnte man den Allokations-Manager noch von verschiedenen Threads aus nutzen.
</p>
<p>
</p>
<p>
Dazu kommt, da&#223; ich generell eine Aversion gegen Ringbuffer habe, speziell dann, wenn man keine Garantie geben kann zur maximalen Bef&#252;llung. Es &#228;rgert mich immer wieder, wenn &#187;coole&#171; Leute mit Ringbuffern um sich werfen.
</p>
</body>
</html></richcontent>
<icon BUILTIN="forward"/>
<node CREATED="1777078498513" ID="ID_1558383335" MODIFIED="1777078503445" TEXT="kann dynamisch wachsen"/>
<node CREATED="1777078504193" ID="ID_1364741326" MODIFIED="1777078513262" TEXT="kann mit mehreren Producern/Consumern umgehen"/>
</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 CREATED="1777052497154" ID="ID_1906132363" MODIFIED="1777052517239" TEXT="vorerst den default(-Heap)-Allokator f&#xfc;r alles verwenden"/>
<node CREATED="1777073876105" ID="ID_1153882258" MODIFIED="1777073995987" TEXT="stelle fest: LinkedElements sind nicht gut geeignet">
<richcontent TYPE="NOTE"><html>
<head/>
<body>
<p>
...und zwar, weil sie bisher kein Entfernen von Eintr&#228;gen unterst&#252;tzen (und sinnvollerweise w&#228;re auch nur ein pop() vom Anfang der Liste); au&#223;erdem w&#228;re schwierig, da&#223; man beim Entfernen die Ownership &#252;bertragen m&#252;&#223;te, um eine Re-Allokation zu verhindern.
</p>
</body>
</html></richcontent>
<node COLOR="#338800" CREATED="1777074497539" ID="ID_1587444173" MODIFIED="1777084232736" TEXT="sollte eine std::list verwenden (double-linked)">
<icon BUILTIN="yes"/>
</node> </node>
<node CREATED="1777050457640" ID="ID_55967690" MODIFIED="1777050461566" TEXT="Dokumentieren per Test"> <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="1777050457640" ID="ID_55967690" MODIFIED="1777084249904" TEXT="Dokumentieren per Test">
<icon BUILTIN="pencil"/>
<node CREATED="1777050471715" ID="ID_694034532" MODIFIED="1777050476406" TEXT="LocalMemPool_test"/> <node CREATED="1777050471715" ID="ID_694034532" MODIFIED="1777050476406" TEXT="LocalMemPool_test"/>
<node CREATED="1777052679889" ID="ID_1809859332" MODIFIED="1777052689966" TEXT="simpleUse()"/> <node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1777052679889" ID="ID_1809859332" MODIFIED="1777084246555" TEXT="simpleUse()">
<node CREATED="1777052647931" ID="ID_1721020388" MODIFIED="1777052658570" TEXT="verify_matchAlloc"/> <icon BUILTIN="pencil"/>
</node>
<node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1777052647931" ID="ID_1721020388" MODIFIED="1777084246554" TEXT="verify_matchAlloc">
<icon BUILTIN="pencil"/>
</node>
<node CREATED="1777052660303" ID="ID_1192169892" MODIFIED="1777052665482" TEXT="verify_selectAlloc"/> <node CREATED="1777052660303" ID="ID_1192169892" MODIFIED="1777052665482" TEXT="verify_selectAlloc"/>
<node CREATED="1777052670304" ID="ID_924547131" MODIFIED="1777052674241" TEXT="verify_pruneAlloc"/> <node CREATED="1777052670304" ID="ID_924547131" MODIFIED="1777052674241" TEXT="verify_pruneAlloc"/>
</node> </node>