diff --git a/admin/scons/Platform.py b/admin/scons/Platform.py index ea11fd9c3..fc308a45e 100644 --- a/admin/scons/Platform.py +++ b/admin/scons/Platform.py @@ -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).') diff --git a/doc/technical/build/BuildDependencies.txt b/doc/technical/build/BuildDependencies.txt index 6d363946f..29a1851d6 100644 --- a/doc/technical/build/BuildDependencies.txt +++ b/doc/technical/build/BuildDependencies.txt @@ -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) diff --git a/src/lib/linked-elements.hpp b/src/lib/linked-elements.hpp index dbba6f767..9a2419446 100644 --- a/src/lib/linked-elements.hpp +++ b/src/lib/linked-elements.hpp @@ -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 diff --git a/src/vault/mem/local-mem-pool.hpp b/src/vault/mem/local-mem-pool.hpp index 7f840e182..b4c22b547 100644 --- a/src/vault/mem/local-mem-pool.hpp +++ b/src/vault/mem/local-mem-pool.hpp @@ -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 +#include +#include +//#include +#include +#include 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); + + 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); + + using InQueue = boost::lockfree::queue; + using BlockList = std::list; + + 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 + /** + * 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 ////////TODO requires std::invocable + 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 ////////TODO requires std::invocable + 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 + Block const* + findMatch (PRED&& predicate) const + { + auto found = std::ranges::find_if (blocks_, forward (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 = █ + } + } + 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; }); } }; diff --git a/tests/vault/mem/local-mem-pool-test.cpp b/tests/vault/mem/local-mem-pool-test.cpp index f0e3edc71..dc3ce10b3 100644 --- a/tests/vault/mem/local-mem-pool-test.cpp +++ b/tests/vault/mem/local-mem-pool-test.cpp @@ -86,7 +86,7 @@ namespace test { CHECK (pool.canServe(SIZ20)); CHECK (pool.canServe(SIZ10)); - auto [mem,siz] = pool.retrieve (SIZ10); + auto [mem,siz] = pool.retrieve (SIZ10); CHECK (MEM1 == mem); CHECK (SIZ20 == siz); CHECK (not pool.canServe(SIZ10)); @@ -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()); } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 8670c8f4f..3ce5d4e26 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -145846,6 +145846,16 @@ 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 +

+ +
+
@@ -145866,14 +145876,64 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + + + + + + + + + + + + + + +

+ ...da es sich um einen Prototyp handelt, halte ich diese Vorteile für sehr wichtig; im Besonderen das mit dem single producer/consumer läß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önnte man den Allokations-Manager noch von verschiedenen Threads aus nutzen. +

+

+ +

+

+ Dazu kommt, daß ich generell eine Aversion gegen Ringbuffer habe, speziell dann, wenn man keine Garantie geben kann zur maximalen Befüllung. Es ärgert mich immer wieder, wenn »coole« Leute mit Ringbuffern um sich werfen. +

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

+ ...und zwar, weil sie bisher kein Entfernen von Einträgen unterstützen (und sinnvollerweise wäre auch nur ein pop() vom Anfang der Liste); außerdem wäre schwierig, daß man beim Entfernen die Ownership übertragen müßte, um eine Re-Allokation zu verhindern. +

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