From 8de3fe21bb2b1944b6dd35c351fbd3bf7f8aba21 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Fri, 24 Nov 2023 15:43:26 +0100 Subject: [PATCH] Chain-Load: detect small-object optimisation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Helper function to find out of two objects are located "close to each other" -- which can be used as heuristics to distinguish heap vs. stack storage - further investigation shows that libstdc++ applies the small-object optimisation for functor up to »two slots« in size -- but only if the copy-ctor is trivial. Thus a lambda capturing a shared_ptr by value will *always* be maintained in heap storage (and LazyInit must be redesigned accordingly)... - the verify_inlineStorage() unit test will now trigger if some implementation does not apply small-object optimisation under these minimal assumptions --- src/lib/lazy-init.hpp | 13 +++--- src/lib/util.hpp | 29 +++++++++++- tests/library/lazy-init-test.cpp | 75 +++++++++++++++----------------- wiki/thinkPad.ichthyo.mm | 36 ++++++++++----- 4 files changed, 93 insertions(+), 60 deletions(-) diff --git a/src/lib/lazy-init.hpp b/src/lib/lazy-init.hpp index c1e99ec28..d6fcfac45 100644 --- a/src/lib/lazy-init.hpp +++ b/src/lib/lazy-init.hpp @@ -105,18 +105,15 @@ namespace lib { template class TrojanFun { - template - using Manager = std::shared_ptr; - template static auto - buildTrapActivator (Manager&& managedDelegate, _Fun) + buildTrapActivator (DEL* delegate, _Fun) { - return [chain = move(managedDelegate)] + return [delegate] (ARGS ...args) -> RET { - auto currLocation = &chain; - auto& functor = (*chain) (currLocation); + auto currLocation = &delegate; + auto& functor = (*delegate) (currLocation); // return functor (forward (args)...); }; @@ -146,7 +143,7 @@ namespace lib { REQUIRE (delegate); - return buildTrapActivator (Manager{delegate}, _Fun()); + return buildTrapActivator (delegate, _Fun()); } }; diff --git a/src/lib/util.hpp b/src/lib/util.hpp index 981f3aaa5..56a6a2aa9 100644 --- a/src/lib/util.hpp +++ b/src/lib/util.hpp @@ -357,7 +357,6 @@ namespace util { { return static_cast (std::addressof(x)); } - template inline const void* getAddr (X* x) @@ -365,6 +364,34 @@ namespace util { return static_cast (x); } + /** the addressable memory »slot« — platform dependent */ + template + inline size_t + slotNr (X const& x) + { + return reinterpret_cast (std::addressof(x)) / sizeof(size_t); + } + template + inline size_t + slotNr (X const* x) + { + return reinterpret_cast (x) / sizeof(size_t);; + } + + + /** determine heuristically if two objects + * are located „close to each other“ in memory. + * @remark can be used to find out about heap vs. stack allocation + */ + template + inline bool + isCloseBy (A&& a, B&& b, size_t consideredNearby =50) + { + size_t loc1 = slotNr (std::forward (a)); + size_t loc2 = slotNr (std::forward (b)); + size_t dist = loc2 > loc1? loc2-loc1:loc1-loc2; + return dist < consideredNearby; + } diff --git a/tests/library/lazy-init-test.cpp b/tests/library/lazy-init-test.cpp index 34d63f261..8702deec8 100644 --- a/tests/library/lazy-init-test.cpp +++ b/tests/library/lazy-init-test.cpp @@ -30,7 +30,7 @@ #include "lib/lazy-init.hpp" //#include "lib/format-string.hpp" //#include "lib/test/test-helper.hpp" -#include "lib/test/testdummy.hpp" +//#include "lib/test/testdummy.hpp" #include "lib/test/diagnostic-output.hpp" /////////////////////TODO TODOH #include "lib/util.hpp" @@ -140,7 +140,7 @@ namespace test{ // now (finally) build the »trap function«, // taking ownership of the heap-allocated delegate copy auto trojanLambda = TrojanFun::generateTrap (delP); - CHECK (sizeof(trojanLambda) == 2*sizeof(size_t)); + CHECK (sizeof(trojanLambda) == sizeof(size_t)); // on invocation... // - it captures its current location @@ -162,50 +162,43 @@ namespace test{ /** @test verify that std::function indeed stores a simple functor inline + * @remark The implementation of LazyInit relies crucially on a known optimisation + * in the standard library ─ which unfortunately is not guaranteed by the standard: + * Typically, std::function will apply _small object optimisation_ to place a very + * small functor directly into the wrapper, if the payload has a trivial copy-ctor. + * Libstdc++ is known to be rather restrictive, other implementations trade increased + * storage size of std::function against more optimisation possibilities. + * LazyInit exploits this optimisation to „spy“ about the current object location, + * to allow executing the lazy initialisation on first use, without further help + * by client code. This trickery seems to be the only way, since λ-capture by reference + * is broken after copying or moving the host object (which is required for DSL use). + * In case this turns out to be fragile, LazyInit should become a "LateInit" and needs + * help by the client or the user to trigger initialisation; alternatively the DSL + * could be split off into a separate builder object distinct from RandomDraw. */ void verify_inlineStorage() { - Dummy::checksum() = 0; - using DummyManager = std::shared_ptr; +// char payload[24];// ◁─────────────────────────────── use this to make the test fail.... + const char* payload = "Outer Space"; + auto lambda = [payload]{ return RawAddr(&payload); }; - DummyManager dummy{new Dummy}; - long currSum = Dummy::checksum(); - CHECK (currSum > 0); - CHECK (currSum == dummy->getVal()); - CHECK (1 == dummy.use_count()); - { - // --- nested Scope --- - auto lambda = [dummy]{ return &dummy; }; - CHECK (2 == dummy.use_count()); - CHECK (currSum == Dummy::checksum()); - - RawAddr location = lambda(); - CHECK (location == &lambda); - - std::function funWrap{lambda}; - CHECK (funWrap); - CHECK (not isSameObject (funWrap, lambda)); - CHECK (3 == dummy.use_count()); - CHECK (currSum == Dummy::checksum()); - -SHOW_EXPR(location) - location = funWrap(); -SHOW_EXPR(location) -SHOW_EXPR(RawAddr(&funWrap)) -SHOW_EXPR(RawAddr(dummy.get())) - - std::function fuckWrap{[location]{ return &location; }}; - CHECK (fuckWrap); -SHOW_EXPR(RawAddr(&location)) -SHOW_EXPR(RawAddr(fuckWrap())) -SHOW_EXPR(RawAddr(&fuckWrap)) - } - CHECK (1 == dummy.use_count()); - CHECK (currSum == Dummy::checksum()); - dummy.reset(); - CHECK (0 == dummy.use_count()); - CHECK (0 == Dummy::checksum()); + RawAddr location = lambda(); + CHECK (location == &lambda); + + std::function funWrap{lambda}; + CHECK (funWrap); + CHECK (not isSameObject (funWrap, lambda)); + + location = funWrap(); + CHECK (util::isCloseBy (location, funWrap)); + // if »small object optimisation« was used, + // the lambda will be copied directly into the std:function; + // otherwise it will be heap allocated and this test fails. + + // for context: these are considered "close by", + // since both are sitting right here in the same stack frame + CHECK (util::isCloseBy (funWrap, lambda)); } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index f212e3a5a..9949bfa8e 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -97285,9 +97285,15 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + + + + + + + @@ -97315,29 +97321,39 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- +

- Ergebnis: funktioniert nur mit 1 »Slot« + Ergebnis: funktioniert nicht mit shared_ptr

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