From bb164e37c8fe499cfc96e20df4f62aa6ed0d8523 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Thu, 13 Jun 2024 23:46:17 +0200 Subject: [PATCH] Library: allow for dynamic adjustments in `AllocationCluster` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a low-level interface to allow changing the size of the currently latest allocation in `AllocationCluster`; a client aware of this capability can perform a real »in-place re-alloc«, assuming the very specific usage constraints can be met. `lib::Several` will use this feature when attached to an `AllocationCluster`; with this special setup, an previously unknown number of non-copyable objects can be built without wasting any storage, as long as the storage reserve in the current extent of the `AllocationCluster` is sufficient. --- src/lib/allocation-cluster.hpp | 73 ++++++++++- src/lib/several-builder.hpp | 2 - tests/library/allocation-cluster-test.cpp | 87 +++++++++++-- wiki/thinkPad.ichthyo.mm | 149 ++++++++++++++++------ 4 files changed, 258 insertions(+), 53 deletions(-) diff --git a/src/lib/allocation-cluster.hpp b/src/lib/allocation-cluster.hpp index 4ecb4ec98..df44b620f 100644 --- a/src/lib/allocation-cluster.hpp +++ b/src/lib/allocation-cluster.hpp @@ -30,6 +30,13 @@ ** pattern. Optionally it is even possible to skip invocation of object ** destructors, making de-allocation highly efficient (typically the ** memory pages are already cache-cold when about to discarded). + ** \par using as STL allocator + ** AllocationCluster::Allocator is an adapter to expose the interface + ** expected by std::allocator_traits (and thus usable by all standard compliant + ** containers). With this usage, the container _manages_ the contained objects, + ** including the invocation of their destructors, while relying on the allocator + ** to allot and discard bare memory. However, to avoid invoking any destructors, + ** the container itself can be created with AllocationCluster::createDisposable. ** @warning deliberately *not threadsafe*. ** @remark confine usage to a single thread or use thread-local clusters. ** @see allocation-cluster-test.cpp @@ -52,6 +59,7 @@ namespace lib { namespace test { class AllocationCluster_test; } // declared friend for low-level-checks + namespace err = lumiera::error; /** @@ -77,17 +85,23 @@ namespace lib { void* pos{nullptr}; size_t rest{0}; + auto bytePos() { return static_cast (pos); } + void* allot (size_t bytes, size_t alignment) { void* loc = std::align (alignment, bytes, pos, rest); if (loc) { // requested allocation indeed fits in space - pos = static_cast(pos) + bytes; + pos = bytePos() + bytes; rest -= bytes; } return loc; } + + void adjustPos (int offset); + bool hasReserve (int offset) const; + bool matches_last_allocation (void const* loc, size_t siz) const; }; Storage storage_; @@ -131,6 +145,9 @@ namespace lib { Allocator getAllocator() { return this; } + bool canAdjust (void* loc, size_t oldSiz, size_t newSiz) const; + void doAdjust (void* loc, size_t oldSiz, size_t newSiz); + private: /** * portion out the requested amount of memory, @@ -195,7 +212,7 @@ namespace lib { * but *without invoking its destructor* on clean-up (for performance reasons). */ template - TY& + inline TY& AllocationCluster::createDisposable (ARGS&& ...args) { return * new(allot()) TY (std::forward (args)...); @@ -207,7 +224,7 @@ namespace lib { * @note whenever possible, the #createDisposable variant should be preferred */ template - TY& + inline TY& AllocationCluster::create (ARGS&& ...args) { if constexpr (std::is_trivial_v) @@ -220,5 +237,55 @@ namespace lib { } + /** + * Adjust the size of the latest raw memory allocation dynamically. + * @param loc an allocation provided by this AllocationCluster + * @param oldSiz the size requested for the allocation \a loc + * @param newSiz desired new size for this allocation + * @remarks since AllocationCluster must be used in a single threaded environment, + * the invoking code can sometimes arrange to adapt the latest allocation + * to a dynamically changing situation, like e.g. populating a container + * with a previously unknown number of elements. Obviously, the overall + * allocation must stay within the confines of the current extent; it + * is thus mandatory to [check](\ref canAdjust) the ability beforehand. + */ + inline void + AllocationCluster::doAdjust(void* loc, size_t oldSiz, size_t newSiz) + { + if (not canAdjust (loc,oldSiz,newSiz)) + throw err::Invalid {"AllocationCluster: unable to perform this allocation adjustment."}; + storage_.adjustPos (int(newSiz) - int(oldSiz)); + } + + inline bool + AllocationCluster::canAdjust(void* loc, size_t oldSiz, size_t newSiz) const + { + int offset{int(newSiz) - int(oldSiz)}; // is properly limited iff oldSiz is correct + return storage_.matches_last_allocation (loc, oldSiz) + and storage_.hasReserve (offset); + } + + inline void + AllocationCluster::Storage::adjustPos (int offset) ///< @warning be sure a negative offset is properly limited + { + REQUIRE (pos); + REQUIRE (hasReserve (rest)); + pos = bytePos() + offset; + rest -= offset; + } + + inline bool + AllocationCluster::Storage::hasReserve (int offset) const + { + return offset <= int(rest); + } + + inline bool + AllocationCluster::Storage::matches_last_allocation (void const* loc, size_t siz) const + { + return loc == static_cast (pos) - siz; + } + + } // namespace lib #endif /*LIB_ALLOCATION_CLUSTER_H*/ diff --git a/src/lib/several-builder.hpp b/src/lib/several-builder.hpp index aa65bfe63..8077da5d3 100644 --- a/src/lib/several-builder.hpp +++ b/src/lib/several-builder.hpp @@ -250,8 +250,6 @@ namespace lib { using Fac::Fac; // pass-through ctor - const bool isDisposable{false}; ///< memory must be explicitly deallocated - bool canExpand(size_t){ return false; } Bucket* diff --git a/tests/library/allocation-cluster-test.cpp b/tests/library/allocation-cluster-test.cpp index 65f28c5b8..325563314 100644 --- a/tests/library/allocation-cluster-test.cpp +++ b/tests/library/allocation-cluster-test.cpp @@ -154,6 +154,7 @@ namespace test { checkLifecycle(); verifyInternals(); use_as_Allocator(); + dynamicAdjustment(); } @@ -220,7 +221,6 @@ namespace test { * the additional metadata overhead is a power of two, exploiting contextual knowledge * about layout; moreover, a special usage-mode allows to skip invocation of destructors. * To document these machinations, change to internal data is explicitly verified here. - * @todo WIP 5/24 ✔ define ⟶ ✔ implement */ void verifyInternals() @@ -397,7 +397,7 @@ namespace test { AllocationCluster clu; CHECK (clu.numExtents() == 0); - VecI veci{clu.getAllocator()}; + VecI vecI{clu.getAllocator()}; // Since vector needs a contiguous allocation, // the maximum number of elements is limited by the Extent size (256 bytes - 2*sizeof(void*)) @@ -406,20 +406,91 @@ namespace test { const uint MAX = 64; for (uint i=1; i<=MAX; ++i) - veci.push_back(i); + vecI.push_back(i); CHECK (clu.numExtents() == 2); - CHECK (veci.capacity() == 64); + CHECK (vecI.capacity() == 64); // fill a set with random strings... - SetS sets{clu.getAllocator()}; + SetS setS{clu.getAllocator()}; for (uint i=0; i()); - CHECK (sets.size() > 0.9 * NUM_OBJECTS); + setS.emplace (test::randStr(32), clu.getAllocator()); + CHECK (setS.size() > 0.9 * NUM_OBJECTS); CHECK (clu.numExtents() > 200); // verify the data in the first allocation is intact - CHECK (explore(veci).resultSum() == sum(64)); + CHECK (explore(vecI).resultSum() == sum(64)); + } + + + /** @test verify the ability to adjust the latest allocation dynamically. + */ + void + dynamicAdjustment() + { + AllocationCluster clu; + auto& l1 = clu.create>(); + CHECK (clu.numExtents() == 1); + CHECK (clu.numBytes() == 12); + + auto& l2 = clu.create>(); + CHECK (clu.numExtents() == 1); + CHECK (clu.numBytes() == 17); + + CHECK ( clu.canAdjust (&l2, 5, 8)); // possible since l2 is verifiable as last allocation + CHECK ( clu.canAdjust (&l2, 5, 5)); // arbitrary adjustments are then possible + CHECK ( clu.canAdjust (&l2, 5, 2)); + CHECK ( clu.canAdjust (&l2, 5, 0)); // even shrinking to zero + CHECK (not clu.canAdjust (&l1, 12,24)); // but the preceding allocation can not be changed anymore + CHECK (not clu.canAdjust (&l2, 6, 8)); // similarly, reject requests when passing wrong original size + CHECK (not clu.canAdjust (&l2, 4, 8)); + CHECK (not clu.canAdjust (&l2, 5, 1000)); // also requests exceeding the remaining extent space are rejected + CHECK ( clu.canAdjust (&l1, 17,24)); // however, can not detect if a passed wrong size accidentally matches + + CHECK (clu.numExtents() == 1); + CHECK (clu.numBytes() == 17); + l1[11] = 11; // put some marker values into the storage + l2[0] = 5; + l2[1] = 4; + l2[2] = 3; + l2[3] = 2; + l2[4] = 1; + l2[5] = 55; // yes, even behind the valid range (subscript is unchecked) + l2[6] = 66; + + using LERR_(INVALID); + + VERIFY_ERROR (INVALID, clu.doAdjust(&l1, 12,24) ); + CHECK (clu.numExtents() == 1); + CHECK (clu.numBytes() == 17); + + // perform a size adjustment on the latest allocation + clu.doAdjust (&l2, 5,12); + CHECK (clu.numExtents() == 1); + CHECK (clu.numBytes() == 24); + // no memory corruption + CHECK (l1[11] == 11); + CHECK (l2[0] == 5); + CHECK (l2[1] == 4); + CHECK (l2[2] == 3); + CHECK (l2[3] == 2); + CHECK (l2[4] == 1); + CHECK (l2[5] == 55); + CHECK (l2[6] == 66); + + // scale down the latest allocation completely + clu.doAdjust (&l2, 12,0); + CHECK (clu.numExtents() == 1); + CHECK (clu.numBytes() == 12); + // no memory corruption + CHECK (l1[11] == 11); + CHECK (l2[0] == 5); + CHECK (l2[1] == 4); + CHECK (l2[2] == 3); + CHECK (l2[3] == 2); + CHECK (l2[4] == 1); + CHECK (l2[5] == 55); + CHECK (l2[6] == 66); } }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 3e4696cfc..14c74f958 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -81325,9 +81325,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - + + + @@ -81346,11 +81346,12 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + - - + + @@ -81383,7 +81384,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -81411,7 +81412,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -81502,7 +81503,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200

- ...man verwendet nur speziell im produktiven Einsatz im Node-Graph  einen besonderen Allocator, der zwar den Destruktor aufruft, aber den Speicher nicht freigibt; alloziert wird immer in einen kompakten Block hinein, der dann auf der Basis der Prozeß-Kenntnis als Ganzes verworfen und neu verwendet wird. + ...man verwendet nur speziell im produktiven Einsatz im Node-Graph einen besonderen Allocator, der zwar den Destruktor aufruft, aber den Speicher nicht freigibt; alloziert wird immer in einen kompakten Block hinein, der dann auf der Basis der Prozeß-Kenntnis als Ganzes verworfen und neu verwendet wird.

@@ -81661,7 +81662,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -82642,11 +82643,11 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + - - + + @@ -82831,9 +82832,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

sonst würde eine Spread-Vergrößerung @@ -82842,12 +82841,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
tatsächlich die Reserve verkleinern...

- -
+ - - - +
  • @@ -82876,8 +82872,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    Begründung: das ganze Thema »spread« ist extrem technisch und für den Nutzer normalerweise nicht nachvollziehbar, aber die Kapazität in Anzahl der freien Slots ist sehr wohl verständlich für den User; es wäre also ziemlich überraschend wenn — scheinbar ohne ersichtlichen Grund — plötzlich die Reserve-Kapazität verschwunden wäre.

    - - + @@ -82970,8 +82965,18 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    + + + + - + + + + + + + @@ -82994,16 +82999,13 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    - - - +

    denn der Allocation-Cluster weiß selber die Zahl seiner belegten Roh-Blöcke; zur de-Allokation muß ansonsten gar nichts gemacht werden

    - -
    +
    @@ -83192,8 +83194,11 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    - - + + + + + @@ -83220,9 +83225,12 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    - - - + + + + + + @@ -83231,8 +83239,31 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    - - + + + + + + + + + +

    + ...im Besonderen könnte man den std::common_type verwenden, um ein mögliches Interface zu inferieren, und zudem einen gemeinsamen Element-Typ zu erkennen... +

    + +
    +
    + + + + +

    + ...denn lib::Several ist ein ziemlich trickreicher low-level-Container; es könnte gefährlich werden, wenn man beliebige Typ-Parameter verwendet; insofern ist es nicht wünschenswert, dem User das explizite Wählen der Typ-Parameter zu ersparen (abgesehen von dem einfachen Fall mit der Initializer-list) +

    + +
    +
    @@ -83645,8 +83676,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    - - + + @@ -83666,8 +83697,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    - + + @@ -84302,6 +84334,39 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    + + + + + + +

    + Und zwar weil sich aus der konkreten Implementierung diese Möglichkeit einfach ergibt +

    + + +
    +
    + + + + + + + + + + + + + + + + + + + +
    @@ -84396,6 +84461,10 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
    + + + +