From 7448b81d497ae1bb6aed3222cf5ffc60827136d3 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Thu, 14 May 2026 19:30:22 +0200 Subject: [PATCH] Library: improve initial population of ScopedCollection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a collection with unique-ownership, and the ability to create all content objects in ''RAII style'' from the constructor call, using a ''population functor''. It is often used to maintain a collection of resource handles, and especially to start a series of threads (for testing). With the variadic λ-captures from C++20, it is possible to rewrite the most frequently used population functors to be easier to use. Notably we can now bind constructor arguments, that will be passed to each of the created elements — a prominent example being the thread function when launching several threads. Furthermore, I added a constructor variant that consumes a Lumiera Iterator. Adding this kind of generic variants is possible now with C++20, since overload resolution takes concepts into account. --- doc/technical/howto/crackNuts.txt | 74 +++++- src/lib/scoped-collection.hpp | 218 +++++++++++------- src/lib/thread.hpp | 12 + tests/lib/local-slice-test.cpp | 26 +-- tests/lib/scoped-collection-test.cpp | 35 +-- tests/lib/sync-classlock-test.cpp | 40 ++-- .../vault/mem/engine-buffer-manager-test.cpp | 6 +- wiki/thinkPad.ichthyo.mm | 122 ++++++---- 8 files changed, 358 insertions(+), 175 deletions(-) diff --git a/doc/technical/howto/crackNuts.txt b/doc/technical/howto/crackNuts.txt index e66c488ac..d92ac46f2 100644 --- a/doc/technical/howto/crackNuts.txt +++ b/doc/technical/howto/crackNuts.txt @@ -88,6 +88,33 @@ test with random numbers:: several threads; each thread should use its own generator then (yet this can be handled sloppy if the _quality_ of the random number actually does not matter!) +special test setup:: + It can be very helpful to use _policy based design_ for some complex evaluation or algorithm, + even in cases where it is clear that the actual usage will only ever involve one specific data type + and configuration. Not only does such a structured approach help with clarifying the design itself, + since the core of the evaluation logic can be abstracted from data representation, but only the + testing and verification of correctness is simplified, as the test can be written to use simple, + human-readable data (strings, symbols), and, furthermore, many kinds of instrumentation can be + included into the test data to gain detailed insight what is happening in the test subject. + +verify behaviour sequence:: + Use the `lib::EventLog`: ++ +- within the test, notable points are marked into the log, possibly with arguments +- after the test, the Event Log can be verified with a search-sequence +- this way it is possible to ensure that some events did happen, and happened + in a very specific order, or that some event did _not_ happen after another event +- the `test::Tracker` defined in 'src/test/tracking-dummy.hpp' will mark all constructor, + destructor and copy events into an embedded Event Log instance. By inheriting or mix-in + from that type, the allocation and instance behaviour of some service can be scrutinised. + +verify incidences and concurrency:: + Aside from using some generic tracing utility, sometimes it is much easier to setup + a _specifically tailored instrumentation_. This can be achieved with 'incidence-count.hpp' ++ +- for instrumentation, the `markEnter()` and `markLeave()` functions need to be invoked +- after the test run, various statistics can be computed and evaluated, including + cumulated time and concurrency for the "cases" marked with enter / leave. Common Tasks @@ -109,6 +136,12 @@ structured data:: - also useful as output format, both for debugging and because it can be matched against an expected structure, which is generated with the DSL notation. - [yellow-background]#(planned)# will be able to map this from/to JSON easily. +scoped collection:: + unique-ownership collection that holds a sequence of objects in a preallocated fixed + storage block. Notably even non-copyable objects can be managed (which is not possible + with a `std::vector`), and type-erased instances of subclasses can be mixed. Furthermore + there is a constructor variant that allows to populate a `ScopedCollection` in RAII-style. + This can e.g. be used to fire-off a set of Threads or open a set of wrapped connections. Iterating ~~~~~~~~~ @@ -134,6 +167,26 @@ IterSource:: even the production-state can be hooked up into the IterSource instance (with a smart-ptr). This allows e.g. to send a Diff to the UI through the UI-Bus, while the actual generation and application both happen demand-driven or _lazy..._ +State Core:: + When some algorithm involves a statefull evaluation, it might be helpful to package that into + a _State Core_ container. This is a class that is expected to expose three operations, which + allow to build an iteration on top. The header 'iter-adapter.hpp' provides several adapters + and wrappers, notable `lib::IterStateWrapper` wraps any _state core_ and provides a + Lumiera Iterator interface; likewise, `IterExplorer` heavily relies on that concept and + is able to accept and iterate any state core in a pipeline. +special iterators:: + Some additional adaptors are provided as building block so that specific evaluations + can be made to conform with the »Lumiera Iterator« concept ++ +- several given iterators can be combined into a iteration over tuples with 'lib/iter-zip.hpp' +- there is also a variation 'lib::izip' that adds a running index into the _zipping operation_ +- IterExplorer provides a `flatten()` operation, that can be used to join an iterator-of-iterators + into a single seamless iteration lazily. +- when a STL container also supports _backwards iteration_, then a lumiera iterator can be + created that can _switch direction_ dynamically, with the help of 'iter-cursor.hpp' +- a STL »deque« container can be used both as a stack and a queue; the header 'iter-stack.hpp' + provides an iteration adapter for both usage schemes, so that either the stack or the queue + can be _"discharged"_ as a Lumiera Iterator (possibly with an IterExplorer pipeline) Formatting @@ -155,6 +208,25 @@ Formatting because there are way more capable solutions _out there_ ☺) +Setup and wiring +~~~~~~~~~~~~~~~~ +Generally speaking, we prefer _constructor wiring_ whenever possible, to avoid creating +unnecessary statefulness (as would be the case when connecting dependencies through setters). +This design also favours decomposition into self-contained components with a single purpose, +and simplifies error handling with the help of the RAII pattern. + +However, sometimes a piece of code depends on some remote and even global service; in these +cases, the `lib::Depend` framework can be used to access that service via type name. The +actual service can either be created on-demand as a singleton, or it can be installed +explicitly as a service instance. Furthermore, it is possible to "push aside" the +current service instance temporarily and replace it by a _test mock_. + +One point to note is that `lib::Depend` is a _monostate_, and not a _singleton_. +The actual service should be written as a free-standing class, that can be instantiated +directly for the purpose of testing. + + + Language constructs ------------------- Templates @@ -240,7 +312,7 @@ index and iterate over tuple elements:: This is another example of bridging the compile-time / runtime barrier, using a trampoline. Obviously such a solution requires that it is somehow possible to define a generic functor / λ that can accept any of the tuple elements. And, in order to build a runtime index access, we additionally - need to convert each of the tuple elements into a _common reconciled type. + need to convert each of the tuple elements into a _common reconciled type_. + - If such a type exists, can be figured out with the help of the `meta::CommonReturn` _metafunction_ - `lib::meta::TupleIdxAdaptor` provides the necessary machinery to define a subscript `operator[]` diff --git a/src/lib/scoped-collection.hpp b/src/lib/scoped-collection.hpp index a42948bb7..3934be30d 100644 --- a/src/lib/scoped-collection.hpp +++ b/src/lib/scoped-collection.hpp @@ -2,7 +2,7 @@ SCOPED-COLLECTION.hpp - managing a fixed collection of noncopyable polymorphic objects Copyright (C) - 2012, Hermann Vosseler + 2012,2026 Hermann Vosseler   **Lumiera** is free software; you can redistribute it and/or modify it   under the terms of the GNU General Public License as published by the @@ -13,8 +13,8 @@ /** @file scoped-collection.hpp ** Manage a collection of non-copyable polymorphic objects in compact storage. - ** This helper supports common situation where a service implementation has to - ** manages a fixed collection of implementation related sub-components, treated + ** This helper supports the common situation where a service implementation has + ** to manage a fixed collection of implementation related sub-components, treated ** with reference semantics. Typically, those objects are used polymorphically, ** and often even with _type erasure_ and added step by step through a builder. ** The storage to hold all those child objects is allocated in one chunk @@ -31,7 +31,7 @@ ** the managing scope is alive and able to access them. ** ** There are two different usage patterns for populating a ScopedCollection - ** - the "stack style" usage creates an empty container (using the one arg + ** - the **stack style** usage creates an empty container (using the one arg ** ctor to specify the maximum size only). The storage capacity to hold ** a number of objects, up to the predefined maximum, is (heap) allocated ** immediately at construction of the container, yet no child objects are @@ -39,11 +39,11 @@ ** by invoking #emplaceElement() to create a new element of the default ** type `I`) or #emplace(args) to create some subtype. This way, ** the container is gradually filled up. - ** - the "RAII style" usage strives to create all of the content objects - ** right away, immediately after the memory allocation. This usage pattern - ** avoids any kind of "lifecycle state". Either the container comes up sane - ** and fully populated, or the ctor call fails and any already created - ** objects are discarded. + ** - the **RAII-style** usage attempts to create all the contained objects + ** right away, immediately after memory allocation. This usage pattern + ** avoids any kind of _lifecycle state_. Either the container is in sane + ** state and fully populated from the outset, or the call to the ctor fails + ** and all objects that have already been created are discarded. ** @note intentionally there is no operation to discard individual objects, ** the only option is to #clear() the whole container. ** @note the container can hold instances of a subclass of the type defined @@ -69,7 +69,7 @@ #include "lib/iter-adapter.hpp" #include -#include +#include namespace lib { @@ -78,6 +78,7 @@ namespace lib { using LERR_(INDEX_BOUNDS); using LERR_(CAPACITY); + using meta::lum_iter; /** @@ -100,8 +101,8 @@ namespace lib { public: /** * Storage Frame to hold one Child object. - * The storage will be an heap allocated - * array of such Wrapper objects. + * The storage will be an heap allocated array + * of such Wrapper objects. * @note doesn't manage the Child */ class ElementHolder @@ -126,7 +127,6 @@ namespace lib { - /** place object of type TY, forwarding ctor arguments */ template TY& @@ -150,6 +150,9 @@ namespace lib { clear(); } + /** create an empty ScopedCollection with given max capacity + * @remark this ctor variant is intended for _stack-like_ usage + */ explicit ScopedCollection (size_t maxElements) : level_(0) @@ -164,7 +167,7 @@ namespace lib { * It gets an ElementHolder& as parameter, and should * use this to create an object of suitable I-subclass */ - template + template CTOR> ScopedCollection (size_t maxElements, CTOR builder) : level_(0) , capacity_(maxElements) @@ -190,22 +193,44 @@ namespace lib { populate_by (builder,instance); } + /** variation of RAII-style: "pull" elements from an iterator. + * Each element will be created directly by invoking either the copy ctor, + * or a suitable 1-argument ctor, depending on the type yielded by the iterator. + * No further elements will be created however, once the iterator is exhausted. + * @note anything in accordance to the Lumiera Forward concept is acceptable. + * This rules out just passing a plain STL iterator (because these can't + * tell for themselves when they're exhausted). Use an suitable iter-adapter + * instead, e.g. by invoking lib::iter_stl::eachElm(stl_container) + */ + template + ScopedCollection (size_t maxElements, IT&& iter) + : level_(0) + , capacity_(maxElements) + , elements_(new ElementHolder[maxElements]) + { + pull_from (std::forward (iter)); + } + /* == some pre-defined Builders == */ - class FillAll; ///< fills the ScopedCollection with default constructed I-instances + /** Builder: ills the given storage slot with a I-instance + * constructed from copies of the given arguments. + * @return builder-λ to pass into ScopeCollection's ctor. */ + template + static auto fill (ARGS&&... args); - template - class FillWith; ///< fills the ScopedCollection with default constructed TY-instances + /** Builder: fills the given storage slot with a TY-instance + * constructed from copies of the given arguments. */ + template + static auto fillWith (ARGS&&... args); - template - class PullFrom; ///< fills by copy-constructing values pulled from the iterator IT + /** Builder: fills the given storage slot from invoking the + * provided factory functor, with either an I-element or a + * I-subtype, depending on the functor's result type. + * @note the functor will be copied in the resulting λ */ + template FUN> + static auto invoke (FUN&& factory); - template - static PullFrom - pull (IT iter) ///< convenience shortcut to pull from any given Lumiera Forward Iterator - { - return PullFrom (iter); - } void clear() @@ -247,7 +272,7 @@ namespace lib { * ElementHolder#create function, which places a new element * into the storage frame passed as parameter. */ - template + template CTOR> void populate_by (CTOR builder) try { @@ -282,11 +307,46 @@ namespace lib { catch(...) { WARN (progress, "Failure while populating ScopedCollection. " - "All elements will be discarded"); + "All elements will be discarded."); clear(); throw; } + template + struct _BuildFromIter ///< @internal helper to determine what to build from an iterator + { + using Yield = iter::Yield; + using Value = meta::ValueTypeBinding::value_type; + using Type = std::conditional_t, Value, I>; + static_assert (std::is_constructible_v + ,"Iterator must produce something " + "that can be used to build contents of this container"); + }; + + /** variation of element initialisation, + * creating further elements from the results of an iterator, + * until either the container is filled up or the iterator exhausted. + */ + template + void + pull_from (IT iter) + try { + while (iter and level_ < capacity_) + { + using Element = _BuildFromIter::Type; + ElementHolder& storageFrame (elements_[level_]); + storageFrame.template create (*iter); + ++level_; + ++iter; + } } + catch(...) + { + WARN (progress, "Failure while pulling Iterator into ScopedCollection. " + "All elements will be discarded."); + clear(); + throw; + } + /** push a new element of default type @@ -340,8 +400,8 @@ namespace lib { size_t size () const { return level_; } - size_t capacity () const { return capacity_; } - bool empty () const { return 0 == level_; } + size_t capacity() const { return capacity_; } + bool empty() const { return 0 == level_; } // use in standard range for loops... @@ -354,7 +414,7 @@ namespace lib { private: /* ==== Storage: heap allocated array of element buffers ==== */ - typedef std::unique_ptr ElementStorage; + using ElementStorage = std::unique_ptr; size_t level_; size_t capacity_; @@ -417,66 +477,66 @@ namespace lib { /* === Supplement: pre-defined element builders === */ /** \par usage - * Pass an instance of this builder functor as 2nd parameter - * to ScopedCollections's ctor. (an anonymous instance is OK). + * On invocation, this function generates a builder-λ to pass as + * the 2nd parameter to ScopedCollections's ctor. When arguments are given, + * these are perfect-forwarded into a copy in the closure, which implies that + * all instances will receive either a copy or const-reference to these values. + * (It is not possible to pass references this way). * Using this variant of the ctor switches the collection to RAII-style: - * It will immediately try to create all the embedded objects, invoking this + * It will immediately try to create all the embedded objects, invoking the * builder functor for each "slot" to hold such an embedded object. Actually, * this "slot" is an ElementHolder instance, which provides functions for * placement-creating objects into this embedded buffer. + * @note allows also to default-create or copy-create */ template - class ScopedCollection::FillAll - { - public: - void - operator() (ScopedCollection::ElementHolder& storage) - { - storage.template create(); - } - }; + template + auto + ScopedCollection::fill (ARGS&&... args) + { + return [... args = std::forward (args)] + (ScopedCollection::ElementHolder& storage) + { + storage.template create (args...); + }; + } template - template - class ScopedCollection::FillWith - { - public: - void - operator() (ScopedCollection::ElementHolder& storage) - { - storage.template create(); - } - }; + template + auto + ScopedCollection::fillWith (ARGS&&... args) + { + return [... args = std::forward (args)] + (ScopedCollection::ElementHolder& storage) + { + storage.template create (args...); + }; + } - /** \par usage - * This variant allows to "pull" elements from an iterator. - * Actually, the collection will try to create each element right away, - * by invoking the copy ctor and passing the value yielded by the iterator. - * @note anything in accordance to the Lumiera Forward Iterator pattern is OK. - * This rules out just passing a plain STL iterator (because these can't - * tell for themselves when they're exhausted). Use an suitable iter-adapter - * instead, e.g. by invoking lib::iter_stl::eachElm(stl_container) + /** + * @remark this builder handles the technical aspect of creating elements + * into the slots of the ElementHolder storage, so that client code + * has only to define the constructor argument, which notably can + * also be a subclass of \a I (assuming the storage size suffices) + * @return builder-λ that can be passed into ScopeCollection's ctor. */ template - template - class ScopedCollection::PullFrom - { - IT iter_; - - using ElementType = meta::ValueTypeBinding::value_type; - - public: - PullFrom (IT source) - : iter_(source) - { } - - void - operator() (ScopedCollection::ElementHolder& storage) - { - storage.template create (*iter_); - ++iter_; - } - }; + template FUN> + auto + ScopedCollection::invoke (FUN&& factory) + { + using Res = std::invoke_result_t; + using Elem = std::conditional_t, Res, I>; + static_assert (std::is_constructible_v + ,"Iterator must produce something " + "that can be used to build contents of this container"); + + return [fac = std::forward (factory)] + (ScopedCollection::ElementHolder& storage) mutable + { + storage.template create (fac()); + }; + } diff --git a/src/lib/thread.hpp b/src/lib/thread.hpp index 107c9102e..7f12c954b 100644 --- a/src/lib/thread.hpp +++ b/src/lib/thread.hpp @@ -451,6 +451,8 @@ namespace lib { * and possibly configure further details, depending on the actual Policy used. * @remark the primary ThreadLifecycle-ctor accepts such a Launch-instance * and invokes a chain of λ-functions collected in the member #launch + * @note a Launch instance can be built by direct conversion from an arg-less + * thread function (because the ctor is _not_ explicit) */ struct Launch : util::MoveOnly @@ -571,6 +573,10 @@ namespace lib { * Primary constructor: Launch the new thread with flexible configuration. * @param launcher a #Launch builder with a λ-chain to configure and * finally trigger start of the thread + * @note The Launch instance can be built by implicit conversion from any + * arg-less invocable; this implies that ThreadLifecycle (and its + * relevant subclasses) can be built without much ado directly + * from a Lambda that defines the thread-function. */ ThreadLifecycle (Launch launcher) : Policy{launcher.id} @@ -644,6 +650,12 @@ namespace lib { * thread has already copied the arguments and indeed actively started to run. * @warning The destructor waits for a short grace period of 20ms, but calls * `std::terminate` afterwards, should the thread still be active then. + * @remark creating a Thread instance immediately starts the thread; + * it can be built from + * - any argument-less invocable or lambda (that is invoked in the thread) + * - a string argument for the thread-id, an invocable, further arguments... + * - a member pointer and further arguments; useful for derived classes + * - a ThreadLifecycle::Launch, which allows fine grained configuration */ class Thread : public thread::ThreadLifecycle diff --git a/tests/lib/local-slice-test.cpp b/tests/lib/local-slice-test.cpp index 36761565c..5cbf901e3 100644 --- a/tests/lib/local-slice-test.cpp +++ b/tests/lib/local-slice-test.cpp @@ -129,19 +129,19 @@ namespace test { // launch a significant number of threads, // each verifying the same invocation sequence - lib::ScopedCollection threads{NUM_THREADS}; - for (uint i=1; i<=NUM_THREADS; ++i) - threads.emplace ([&]{ - CHECK (i1 == myDummy.invoke()); - - sleep_for (100us); - - CHECK (i2 == myDummy.invoke()); - - sleep_for (100us); - - CHECK (i3 == myDummy.invoke()); - }); + using Threads = lib::ScopedCollection; + Threads threads{NUM_THREADS + ,Threads::fill ([&]{ + CHECK (i1 == myDummy.invoke()); + + sleep_for (100us); + + CHECK (i2 == myDummy.invoke()); + + sleep_for (100us); + + CHECK (i3 == myDummy.invoke()); + })}; while (explore(threads).has_any()) yield(); // wait for all threads to finish diff --git a/tests/lib/scoped-collection-test.cpp b/tests/lib/scoped-collection-test.cpp index 2efade94b..0f9c34a3a 100644 --- a/tests/lib/scoped-collection-test.cpp +++ b/tests/lib/scoped-collection-test.cpp @@ -379,9 +379,9 @@ namespace test{ { CHECK (0 == Dummy::checksum()); - CollD coll (25, CollD::FillAll() ); + CollD coll (25, CollD::fill() ); - CHECK (!isnil (coll)); + CHECK (not isnil (coll)); CHECK (25 == coll.size()); CHECK (0 != Dummy::checksum()); @@ -393,14 +393,16 @@ namespace test{ } + /** @test use a "populator" that creates a subclass and binds arguments */ void verify_subclassPopulator() { CHECK (0 == Dummy::checksum()); - CollD coll (25, CollD::FillWith() ); + const uint arg{42}; + CollD coll (25, CollD::fillWith (arg, 55) ); - CHECK (!isnil (coll)); + CHECK (not isnil (coll)); CHECK (25 == coll.size()); CHECK (0 != Dummy::checksum()); @@ -409,29 +411,32 @@ namespace test{ } + /** @test verify dynamic pulling of elements + * - the first step uses a "populator" that invokes a λ + * - the second step then uses the source container's iterator, + * which happens to be a _Lumiera Forward Iterator_ to pull + * copies into a second container. + * - even while the second container is dimensioned to be larger, + * only the number of elements yielded by the iterator are created. + */ void verify_iteratorPopulator() { typedef ScopedCollection CollI; - CollI source (25); - for (uint i=0; i < source.capacity(); ++i) - source.emplace(i); // holding the numbers 0..24 - - CollI coll (20, CollI::pull(source.begin())); - // this immediately pulls in the first 20 elements - CHECK (!isnil (coll)); + CollI source (20, CollI::invoke([i=0] mutable { return i++; })); + // filled with numbers 0..19 + CollI coll (30, source.begin()); // this immediately pulls in the first 20 elements + CHECK (not isnil (coll)); CHECK (20 == coll.size()); - CHECK (25 == source.size()); + CHECK (20 == source.size()); + CHECK (30 == coll.capacity()); for (uint i=0; i < coll.size(); ++i) { CHECK (coll[i] == i ); CHECK (coll[i] == source[i]); } - - // note: the iterator is assumed to deliver a sufficient amount of elements - VERIFY_ERROR (ITER_EXHAUST, CollI (50, CollI::pull (source.begin()))); } diff --git a/tests/lib/sync-classlock-test.cpp b/tests/lib/sync-classlock-test.cpp index 138b108ce..26cb8b19b 100644 --- a/tests/lib/sync-classlock-test.cpp +++ b/tests/lib/sync-classlock-test.cpp @@ -19,10 +19,12 @@ #include "test/run.hpp" #include "lib/sync-classlock.hpp" #include "lib/scoped-collection.hpp" +#include "lib/iter-explorer.hpp" #include "lib/thread.hpp" using test::Test; -//using vault::ThreadJoinable; //////////////////WIP +using lib::explore; +using std::this_thread::yield; namespace lib { namespace test { @@ -58,29 +60,25 @@ namespace test { auto gen = buildCappedSubSequence(defaultGen); int contended = 0; - using Threads = lib::ScopedCollection>; + using Threads = lib::ScopedCollection; - // Start a bunch of threads with random access pattern Threads threads{NUM_THREADS, - [&](Threads::ElementHolder& storage) - { - storage.create> ("Sync-ClassLock stress test" - ,[&]{ - for (uint i=0; i guard; - ++contended; - } - } - }); - } - }; + // Start a bunch of threads with random access pattern... + Threads::fill ("Sync-ClassLock stress test" + ,[&]{ + for (uint i=0; i guard; + ++contended; + } + } + })}; - for (auto& thread : threads) - thread.join(); // block until thread terminates // @suppress("Return value not evaluated") + while (explore(threads).has_any()) + yield(); // wait for all threads to terminate CHECK (contended == NUM_THREADS * NUM_LOOP, "ALARM: Lock failed, concurrent modification " diff --git a/tests/vault/mem/engine-buffer-manager-test.cpp b/tests/vault/mem/engine-buffer-manager-test.cpp index c5922f693..b68563451 100644 --- a/tests/vault/mem/engine-buffer-manager-test.cpp +++ b/tests/vault/mem/engine-buffer-manager-test.cpp @@ -225,11 +225,9 @@ namespace test { CHECK (myPool.empty()); // no further pending / duplicate Alloc in-queue }; - using Threads = lib::ScopedCollection; - auto startThread = [&](auto& storage){ storage.template create (workSeq); }; - // Start a collection of workers.... - Threads threads{NUM_THREADS, startThread}; + using Threads = lib::ScopedCollection; + Threads threads{NUM_THREADS, Threads::fill(workSeq)}; // after that barrier, all threads send back their allocation afterThread.sync(); diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index b9d1eab6f..265f4bb0a 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -145915,7 +145915,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -145924,8 +145925,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + @@ -145954,7 +145955,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -146162,8 +146163,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + @@ -146200,8 +146201,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + @@ -146212,7 +146213,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -146223,7 +146225,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -146367,7 +146369,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -146409,8 +146411,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + @@ -146431,7 +146433,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -146451,7 +146454,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -146473,7 +146476,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -146725,8 +146728,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)

- + @@ -146938,8 +146941,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) im Grunde müßte man nur den Zugang zum Memory-Manager selber locken, denn die ganzen Queue-Operationen sind ja schon lock-free und threadsafe; auch könnten diese Locks dann non-recursive sein

- - +
@@ -146949,8 +146951,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) ...andererseits ist jeder (Mutex)-Lock ein Kernel-Aufruf, und wir erwarten durchaus ganze Bündel an Requests. Außerdem wird mit wenigen Locks auf API-Level viel klarer, daß das Locking vollständig, und der Zustand konsistent ist.

- -
+
@@ -146967,16 +146968,15 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) Den Ausschlag gegeben hat aber eine andere Hinsicht: Klarheit und Wartbarkeit sind wichtiger, spezielle Performance-Tweaks sollte man empirisch beweisen.

- - +
- - + + @@ -147021,8 +147021,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + @@ -147035,16 +147034,16 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + - + - + @@ -147063,8 +147062,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) Jedenfalls in dem Setup, das ich jetzt hier für diesen neuen Test gebaut habe, verwenden alle Threads dasselbe Lamba für die Thread-Funktion gemeinsam. Das bedeutet, das makeRandomGen() wird tatsächlich nur einmal aufgerufen, und daher haben wir nur einen einzigen Seed (selbst wenn die Lambdas dann im Thread-Wrapper nochmal kopiert werden)

- - + @@ -147090,13 +147088,16 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + + + + - + @@ -147104,14 +147105,51 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + + + + + + + + + + + + + + + +

+ ...und das geht nicht mit dem fill()-Populator, denn dieser materialisiert alle Argumente als Kopien. Man könnte das ändern, müßte dann aber explizit ein perfect-forwarding-Tupel bauen. Das war mir jetzt alles zu mühsam +

+ + +
+
+ + + + + + +

+ das ist im Besonderen relevant, wenn die captures nicht-triviale Operationen in der Initialisierung verwenden. Da der fill()-Popoulator kopiert, würden sich alle Threads das gleiche Lambda teilen, und daher auf den gleichen Werten arbeiten. Das ist eine subtile Falle +

+ + +
+ +
+
+
@@ -147282,9 +147320,10 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
- - - + + + + @@ -172016,8 +172055,7 @@ std::cout << tmpl.render({"what", "World"}) << s ...nämlich indem der Job letztlich auf den synchronen Fall zurückgreift, dieser aber durch die gleiche Mechanik implementiert ist, wie der asynchrone Fall, und dabei gleich alle aufgesammelten Requests in den Queues mit verarbeitet. Dadruch wird effektiv ein Batch an Allokator-Aufwand in denjenigen Worker geschoben, der rein zufällig als erster auf eine nicht bediente Allokation stößt. Das Schöne an dieser Code-Struktur ist, daß die Möglichkeit offen bleibt, später zur Performance-Optimierung (oder nur in bestimmten Situationen) dennoch etwas speziell einzurichten / schedulen / auszuführen, ohne daß dazu der Code im EngineBufferManager geändert werden muß

- - +