diff --git a/src/lib/idi/genfunc.cpp b/src/lib/idi/genfunc.cpp index 46a9a561e..5a626d434 100644 --- a/src/lib/idi/genfunc.cpp +++ b/src/lib/idi/genfunc.cpp @@ -62,5 +62,11 @@ namespace idi { } //(End)integration helpers... + TypedCounter& + sharedInstanceCounter() + { // Meyer's Singleton + static TypedCounter instanceCounter; + return instanceCounter; + } }} // namespace lib::idi diff --git a/src/lib/idi/genfunc.hpp b/src/lib/idi/genfunc.hpp index 97872eba2..49934836d 100644 --- a/src/lib/idi/genfunc.hpp +++ b/src/lib/idi/genfunc.hpp @@ -128,6 +128,9 @@ namespace idi { } + TypedCounter& sharedInstanceCounter(); + + /** build a per-type identifier, with type prefix and running counter. * @return a type based prefix, followed by an instance number * @note we use the short prefix without namespace, not necessarily unique @@ -141,8 +144,7 @@ namespace idi { inline string generateSymbolicID() { - static TypedCounter instanceCounter; - return format::instance_format (namePrefix(), instanceCounter.inc()); + return format::instance_format (namePrefix(), sharedInstanceCounter().inc()); } /** build a long type based identifier, with running counter and custom prefix. @@ -156,8 +158,7 @@ namespace idi { inline string generateExtendedID(string prefix ="") { - static TypedCounter instanceCounter; - return format::instance_format (prefix + typeFullID(), instanceCounter.inc()); + return format::instance_format (prefix + typeFullID(), sharedInstanceCounter().inc()); } /** diff --git a/src/lib/typed-counter.hpp b/src/lib/typed-counter.hpp index e2bc74497..fc1d80716 100644 --- a/src/lib/typed-counter.hpp +++ b/src/lib/typed-counter.hpp @@ -44,7 +44,10 @@ ** actually to verify this by real measurements (as of 2011) ** @todo 2010 ... this is the first, preliminary version of a facility, ** which is expected to get quite important for custom allocation management. - ** + ** @remark in 2023 changed partially to use atomics; measurements indicate however + ** that the impact of locking technology is negligible. In concurrent use, + ** the wait times are dominating. In single threaded use, both using Atomics + ** or a nonrecursive mutex yield amortised invocation times around 60ns. ** @see typed-counter-test.cpp ** @see TypedAllocationManager ** @see AllocationCluster (custom allocation scheme using a similar idea inline) @@ -59,9 +62,9 @@ #include "lib/error.hpp" #include "lib/sync-classlock.hpp" -#include -#include #include +#include +#include namespace util { @@ -72,15 +75,15 @@ namespace lib { typedef size_t IxID; //////////////////////TICKET #863 - using std::vector; + using std::deque; using std::string; - /** + /** * Provide type-IDs for a specific context. * This facility allows to access a numeric ID for each * given distinct type. Type-IDs may be used e.g. for - * dispatcher tables or for custom allocators. + * dispatcher tables or for custom allocators. * The type-IDs generated here are not completely global though. * Rather, they are tied to a specific type context, e.g. a class * implementing a custom allocator. These typed contexts are @@ -133,13 +136,19 @@ namespace lib { - /** + /** * Utility providing a set of counters, each tied to a specific type. + * The actual allocation of id numbers is delegated to TypedContext. + * Such a counter is used to build [symbolic instance IDs](\ref lib::meta::generateSymbolicID) + * with a per-type running counter. Such IDs are used for lib::idi::EntryID and for lib::diff::GenNode. + * @warning the index space for typeIDs is application global; the more distinct types are used, the more + * slots will be present in _each instance of TypedCounter._ As of 2023 we are using < 30 distinct + * types for these use cases, and thus the wasted memory is not much of a concern. */ class TypedCounter : public Sync<> { - mutable vector counters_; + mutable deque counters_; template IxID @@ -147,42 +156,37 @@ namespace lib { { IxID typeID = TypedContext::ID::get(); if (size() < typeID) - counters_.resize (typeID); + { // protect against concurrent slot allocations + Lock sync(this); + if (size() < typeID) + counters_.resize (typeID); + } - ENSURE (counters_.capacity() >= typeID); + ENSURE (counters_.size() >= typeID); return (typeID - 1); } public: - TypedCounter() - { - counters_.reserve(5); // pre-allocated 5 slots - } - - template - long + int64_t get() const { - Lock sync(this); - return counters_[slot()]; + return counters_[slot()].load(std::memory_order_relaxed); } template - long + int64_t inc() - { - Lock sync(this); - return ++counters_[slot()]; + { // yields the value seen previously + return 1 + counters_[slot()].fetch_add(+1, std::memory_order_relaxed); } template - long + int64_t dec() { - Lock sync(this); - return --counters_[slot()]; + return -1 + counters_[slot()].fetch_add(-1, std::memory_order_relaxed); } @@ -198,6 +202,11 @@ namespace lib { * Utility to produce member IDs * for objects belonging to a "Family", * as defined by a distinguishing type. + * Within each family, each new instance of + * FamilyMember holds a new distinct id number. + * @remark this builds a structure similar to TypedContext, + * however the second level is not assigned _per type_ + * but rather _per instance_ of FamilyMember */ template class FamilyMember @@ -211,7 +220,7 @@ namespace lib { static size_t allocateNextMember() { - return memberCounter.fetch_add(+1, std::memory_order_relaxed); + return 1 + memberCounter.fetch_add(+1, std::memory_order_relaxed); } public: diff --git a/tests/11concurrency.tests b/tests/11concurrency.tests index 1a92da15e..40ee2df2e 100644 --- a/tests/11concurrency.tests +++ b/tests/11concurrency.tests @@ -32,7 +32,7 @@ return: 0 END -TEST "Create 20 Threads passing context" ThreadWrapper_test < #include -#include -#include +#include +#include namespace lib { namespace test{ - - using vault::ThreadJoinable; - using util::for_each; using util::isnil; - using std::placeholders::_1; - using std::bind; - using std::ref; - using std::vector; using std::rand; - namespace { // test data and helpers... - - const uint MAX_FAMILIES = 4; ///< maximum separate "families", each sharing a TypedCounter - const uint MAX_MEMBERS = 10; ///< maximum members per family (member == test thread) - const uint MAX_ITERATIONS = 50; ///< maximum iterations within a single test thread - const uint MAX_DELAY_ms = 3; ///< maximum delay between check iterations - - /* Hint: number of threads = MEMBERS * FAMILIES - * - * The values set here are fairly conservative, - * but increasing the number of threads causes the test suite - * to fail frequently. Please increase these values e.g. - * to 20 and 50 for a more thorough stress test! - */ - - - /** - * Interface to a family of dummy types - */ - class DummyType - { - public: - virtual ~DummyType() { } - - /** core test operation: do a random increment or decrement - * on the provided TypedCounter instance, and also save an - * account to a local embedded checksum for verification */ - virtual void doCount (TypedCounter&) =0; - - virtual void collect_externalCount (TypedCounter&) =0; - virtual void collect_internalCount () =0; - }; - - - /* === Checksums === */ - - long sum_TypedCounter_; ///< Sum1: calculated from TypedCounter - long sum_internal_; ///< Sum2: control value calculated from Dummy::localChecksum_ - - void - accountExternal (DummyType& target, TypedCounter& counter_to_use) - { - target.collect_externalCount (counter_to_use); - } - void - accountInternal (DummyType& target) - { - target.collect_internalCount(); - } - - - /** - * To actually drive the TypedCounter invocations, we need a family - * of different (but of course related) types. Actually, we use these - * subclasses here also to carry out the invocations and the accounting - * to build up the checksums for verification. - */ - template - class Dummy - : public DummyType - , public lib::Sync<> - { - long localChecksum_; - - void - record_internal (int increment) - { - Lock protect(this); - localChecksum_ += increment; - } - - - void - doCount (TypedCounter& counter) - { - // note: deliberately *not* synchronised - - if (rand() % 2) - { - counter.inc(); - record_internal (+1); - } - else - { - counter.dec(); - record_internal (-1); - } - } - - void - collect_externalCount (TypedCounter& counter) - { - // Lock not necessary, because of invocation sequence - sum_TypedCounter_ += counter.get(); - } - - void - collect_internalCount () - { - sum_internal_ += localChecksum_; - } - - public: - Dummy() : localChecksum_(0) {} - }; - - - - - - - /** - * Collection of target functions, - * to be invoked during the test run - */ - struct DummyTarget - { - typedef ScopedPtrVect TargetVect; - - TargetVect targets_; - - DummyTarget () - : targets_(10) - { - targets_.manage(new Dummy<0>); - targets_.manage(new Dummy<1>); - targets_.manage(new Dummy<2>); - targets_.manage(new Dummy<3>); - targets_.manage(new Dummy<4>); - targets_.manage(new Dummy<5>); - targets_.manage(new Dummy<6>); - targets_.manage(new Dummy<7>); - targets_.manage(new Dummy<8>); - targets_.manage(new Dummy<9>); - } - - - /** entry point for the SingleCheck instances - * to trigger off a single invocation - */ - void - torture (TypedCounter& counter_to_use) - { - uint victim = (rand() % 10); - targets_[victim].doCount (counter_to_use); - } - - - typedef TargetVect::iterator iterator; - - /** allow Iteration over all targets in the TargetVect */ - iterator begin() { return targets_.begin(); } - iterator end() { return targets_.end(); } - }; - - DummyTarget targetCollection; - - - - /** - * Each single check runs in a separate thread - * and performs a random sequence of increments - * and decrements on random targets. - */ - class SingleCheck - : ThreadJoinable - { - public: - SingleCheck (TypedCounter& counter_to_use) - : ThreadJoinable("TypedCounter_test worker Thread" - , bind (&SingleCheck::runCheckSequence, this, ref(counter_to_use), (rand() % MAX_ITERATIONS)) - ) - { } - - ~SingleCheck () { this->join(); } - - - private: - void runCheckSequence(TypedCounter& counter, uint iterations) - { - do - { - usleep (1000 * (rand() % MAX_DELAY_ms)); - targetCollection.torture (counter); - } - while (iterations--); - } - }; - - - /** - * Family of individual checks, sharing - * a common TypedCounter instance. - */ - struct TestFamily - { - TypedCounter ourCounter_; - ScopedPtrVect checks_; - - TestFamily() - : checks_(MAX_MEMBERS) - { - uint members (1 + rand() % MAX_MEMBERS); - while (members--) - checks_.manage (new SingleCheck (ourCounter_)); - } - - ~TestFamily() - { - checks_.clear(); // blocks until all test threads finished - account(); - } - - void - account() - { - for_each ( targetCollection - , bind (accountExternal, _1, ourCounter_) - ); - } - }; - - - /** a series of independent context sets */ - typedef ScopedPtrVect FamilyTable; - + namespace { // test parametrisation... + const uint MAX_INDEX = 10; ///< number of distinct types / counters + const uint NUM_THREADS = 100; ///< number of threads to run in parallel + const uint NUM_ITERATIONS = 10000; ///< number of repeated random accesses per Thread } /***********************************************************************************//** - * @test build multiple sets of type-based contexts and run a simple counting operation - * in each of them concurrently. Check the proper allocation of type-IDs in each - * context and verify correct counting operation by checksum. - * + * @test verify the TypedCounter, which allows to maintain a counter-per-type. + * - demonstrate behaviour + * - concurrent test * @see TypedAllocationManager * @see typed-counter.hpp */ @@ -319,7 +85,7 @@ namespace test{ void - simpleUsageTest () + simpleUsageTest() { TypedCounter myCounter; CHECK (isnil (myCounter)); @@ -365,23 +131,69 @@ namespace test{ } - void - tortureTest () + + /** parametrised marker type to designate a counter to be incremented */ + template + struct Dummy { }; + + template + static void + increment (TypedCounter& counter) + { + counter.inc>(); + } + + /** + * Helper for #tortureTest(): + * Build a table of functors, where the i-th entry invokes the function + * increment(), which leads to incrementing the counter for Dummy. + */ + template + static auto + buildOperatorsTable(std::index_sequence) + { + using Operator = void(*)(TypedCounter&); + return std::array{increment...}; + } + + template + static size_t + sumAllCounters(TypedCounter& counter, std::index_sequence) + { + return (counter.get>() + ... ); + } + + + + /** + * @test verify TypedCounter concurrency safety + * - use a set of types `Dummy` to access a corresponding counter + * - run a large number of threads in parallel, each incrementing + * a randomly picked counter; this is achieved by using a table + * of »increment operators«, where each one is tied to a specific + * Dummy. + */ + void + tortureTest() { std::srand (::time (NULL)); - sum_TypedCounter_ = 0; - sum_internal_ = 0; - uint num_Families (1 + rand() % MAX_FAMILIES); + using IDX = std::make_index_sequence; + auto operators = buildOperatorsTable(IDX{}); - FamilyTable testFamilies(num_Families); - for (uint i=0; i size_t + { + uint i = rand() % MAX_INDEX; + operators[i](testCounter); + return 1; + }; - for_each (targetCollection, accountInternal); - CHECK (sum_TypedCounter_ == sum_internal_); + threadBenchmark (testSubject, NUM_ITERATIONS); + + size_t expectedIncrements = NUM_THREADS * NUM_ITERATIONS; + CHECK (sumAllCounters(testCounter, IDX{}) == expectedIncrements); } }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index f0e283e78..c6931daf2 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -80522,7 +80522,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -80538,8 +80538,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + @@ -80886,6 +80887,19 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + + + +

+ CHECK: session-command-function-test.cpp:425: thread_1: perform_massivelyParallel: (testCommandState - prevState == Time(expectedOffset)) +

+ +
+ +
@@ -80898,7 +80912,170 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + + + + + + + + + + + + + + +

+ würde es nicht genügen, +

+

+ einen einzigen Kontext concurrent zu testen? +

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

+ ...das bedeutet, es wird erst zur Laufzeit bestimmt, in welcher Reihenfolge die Counter für die Typen alloziert werden; damit kommt es zu Beginn zu einer aggressiven contention auf slot<X>. Einschränkung: in dieser Form wirkt dieser Test nur beim ersten Lauf innerhalb einer Programm-Instanz, weil danach die Slots belegt sind. In der Praxis stellt das keine Einschränkung dar +

+ +
+
+ + + + + + +

+ ...denn es muß eine hohe Wahrscheinlichkeit geben, daß gleichzeitig zwei Threads auf den gleichen Counter zugreifen ⟹ es muß deutlich mehr Threads geben als counter. Das ist aber schwierig, weil die Zahl der Cores beschränkt ist. Hier hilft nur (a) sehr viel zu viele Threads verwenden und (b) diese lang laufen lassen. +

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

+ das heißt ich muß keinen abstrakten Typ mehr konstruieren und daraus abgeleitete Dummy<i>, weil letzten Endes nur zwei Operationen notwendig sind: den Zähler inkrementieren und am Ende den Zählerstand auslesen +

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

+ das gilt aber nur, wenn nicht bereits summAllCounters() aufgerufen wurde! +

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