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')
if not conf.CheckCXXHeader('boost/format.hpp'):
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++'):
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
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
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
a priority. In the meantime, please report any problems
you encounter with Lumiera running on XWayland.
@ -97,7 +97,8 @@ Languages and Tools
* BOOST (listed below are the Debian package names)
- libboost-dev (at least *1.83*)
- libboost-program-options-dev
- notably including `boost::lockfree`
- libboost-program-options-dev (link dependency)
* Script languages
- Python (*3.10*) required for building (->SCons)

View file

@ -32,7 +32,7 @@
** under the assumption that the allocator is a _monostate_, and
** exploiting _empty-base-optimisation,_ footprint is minimal.
** @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
** several collections at the same time (unless they share all elements)
** @note as usual, any iterator relies on the continued existence and

View file

@ -32,16 +32,30 @@
#include "vault/mem/buffhandle.hpp" //////////////TODO
//#include "lib/local-slice.hpp"
//#include "lib/depend.hpp"
#include "lib/util.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 mem {
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;
@ -51,42 +65,229 @@ namespace mem {
*/
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:
LocalMemPool()
: inQueue_{INQUEUE_SIZ}
, blocks_{}
{ }
/* ===== Pool Access API ===== */
bool empty() const { return 0 == size(); }
size_t size() const { return 0; } ///////////////////////OOO
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
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
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)
{
return {nullptr, 0};
ingest();
Block* found = selectBestMatch (sizRequest);
if (not found)
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:
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;
};
@ -95,7 +296,9 @@ namespace mem {
class PoolDiagnostic
: util::MoveOnly
{
LocalMemPool memPool_;
using Block = LocalMemPool::Block;
LocalMemPool& memPool_;
public:
PoolDiagnostic (LocalMemPool& lmp)
: memPool_{lmp}
@ -104,13 +307,14 @@ namespace mem {
bool
isEmpty()
{
return true;
} ///////////////////OOO
return memPool_.empty();
}
size_t
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
verify_matchAlloc()
@ -106,7 +106,12 @@ namespace test {
CHECK (watch(pool).isEmpty());
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 (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">
<icon BUILTIN="yes"/>
<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="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"/>
@ -145866,14 +145876,64 @@ StM_bind(Builder&lt;R1&gt; b1, Extension&lt;R1,R2&gt; extension)
</node>
<node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1777050453251" ID="ID_1529547535" MODIFIED="1777066700207" TEXT="Implementieren">
<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="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 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="1777052679889" ID="ID_1809859332" MODIFIED="1777052689966" TEXT="simpleUse()"/>
<node CREATED="1777052647931" ID="ID_1721020388" MODIFIED="1777052658570" TEXT="verify_matchAlloc"/>
<node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1777052679889" ID="ID_1809859332" MODIFIED="1777084246555" TEXT="simpleUse()">
<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="1777052670304" ID="ID_924547131" MODIFIED="1777052674241" TEXT="verify_pruneAlloc"/>
</node>