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
+
+
+
+