From bf446626a8b97ef151ad970c835dece3185c92de Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Mon, 27 Apr 2026 23:45:05 +0200 Subject: [PATCH] BufferProvider: draft for the basic global allocator interface This implementation and the accompanying test might seem pointless, yet the point is that the interface is aligned to the emerging concepts for memory allocators in Lumiera, while also directly delegating to the std::allocator for the time being. Before we're able to observe the actual allocator trend and behaviour, it is pretty much moot to engage into any kind of optimisation; the generic heap allocator performs quite well on recent C++ runtimes, and, furthermore, the most important performance tweak is to use a thread-local pool of recent allocations, which should allow our jobs to run without impeding each other. --- src/lib/util-quant.hpp | 50 +++- src/vault/mem/engine-buffer-manager.hpp | 131 +++++++-- src/vault/mem/heap-mem-buffer-store.cpp | 9 +- tests/47engine.tests | 4 + .../vault/mem/engine-buffer-manager-test.cpp | 131 +++++++++ wiki/thinkPad.ichthyo.mm | 263 ++++++++++++++++-- 6 files changed, 533 insertions(+), 55 deletions(-) create mode 100644 tests/vault/mem/engine-buffer-manager-test.cpp diff --git a/src/lib/util-quant.hpp b/src/lib/util-quant.hpp index a9f67f9eb..a171bce1c 100644 --- a/src/lib/util-quant.hpp +++ b/src/lib/util-quant.hpp @@ -20,6 +20,9 @@ #ifndef LIB_UTIL_QUANT_H #define LIB_UTIL_QUANT_H +#include "lib/integral.hpp" + +#include #include #include #include @@ -31,7 +34,7 @@ namespace util { template inline constexpr bool - isPow2 (N n) + isPow2 (N n) noexcept { return n > 0 and !(n & (n-1)); }; // at each power of 2, a new bit is set for the first time @@ -44,7 +47,8 @@ namespace util { I quot; I rem; - IDiv (I num, I den) + constexpr + IDiv (I num, I den) noexcept : quot(num/den) , rem(num - quot*den) { } @@ -54,7 +58,8 @@ namespace util { struct IDiv : div_t { - IDiv (int num, int den) + constexpr + IDiv (int num, int den) noexcept : div_t(div (num,den)) { } }; @@ -63,7 +68,8 @@ namespace util { struct IDiv : ldiv_t { - IDiv (long num, long den) + constexpr + IDiv (long num, long den) noexcept : ldiv_t(ldiv (num,den)) { } }; @@ -72,14 +78,15 @@ namespace util { struct IDiv : lldiv_t { - IDiv (llong num, llong den) + constexpr + IDiv (llong num, llong den) noexcept : lldiv_t(lldiv (num,den)) { } }; template - inline IDiv - iDiv (I num, I den) ///< support type inference and auto typing... + inline constexpr IDiv + iDiv (I num, I den) noexcept ///< support type inference and auto typing... { return IDiv{num,den}; } @@ -93,8 +100,8 @@ namespace util { * @see UtilFloordiv_test */ template - inline I - floordiv (I num, I den) + inline constexpr I + floordiv (I num, I den) noexcept { if (0 < (num^den)) return num/den; @@ -115,8 +122,8 @@ namespace util { * @see UtilFloorwarp_test */ template - inline IDiv - floorwrap (I num, I den) + inline constexpr IDiv + floorwrap (I num, I den) noexcept { IDiv res(num,den); if (0 > (num^den) and res.rem) @@ -129,6 +136,23 @@ namespace util { } + /** quantise towards ceiling. + * This function models to fit something into a tiled structure. + */ + template requires std::unsigned_integral + inline constexpr U + ceilDiv (U num, U den) noexcept + { + return (num + den - 1u) / den; + } + + static_assert (0 == ceilDiv (0u,5u)); + static_assert (1 == ceilDiv (1u,5u)); + static_assert (1 == ceilDiv (4u,5u)); + static_assert (1 == ceilDiv (5u,5u)); + static_assert (2 == ceilDiv (6u,5u)); + + /** * epsilon comparison of doubles. @@ -146,8 +170,8 @@ namespace util { * @see https://en.wikipedia.org/wiki/Unit_in_the_last_place * @todo 3/2024 seems we have solved this problem several times meanwhile /////////////////////////////////TICKET #1360 sort out floating-point rounding and precision */ - inline bool - almostEqual (double d1, double d2, unsigned int ulp =2) + inline constexpr bool + almostEqual (double d1, double d2, unsigned int ulp =2) noexcept { using std::fabs; return fabs (d1-d2) < DBL_EPSILON * fabs (d1+d2) * ulp diff --git a/src/vault/mem/engine-buffer-manager.hpp b/src/vault/mem/engine-buffer-manager.hpp index 62928edc8..520d0a225 100644 --- a/src/vault/mem/engine-buffer-manager.hpp +++ b/src/vault/mem/engine-buffer-manager.hpp @@ -13,12 +13,30 @@ /** @file engine-buffer-manager.hpp ** Central hub of buffer management within the Lumiera Render Engine. + ** Instantiated as global service and part of the Engine, this global + ** allocation coordinator is responsible to provide pre-allocated blocks + ** for render buffers. These allocations are categorised by their size, + ** and will be sent down to LocalMemPool instances running in each + ** worker thread. This involves asynchronous messaging to avoid + ** any blocking of work jobs due to central resource management. ** - ** @todo WIP-WIP-WIP 4/2026 + ** @todo WIP-WIP 4/2026 The intention is to use a family of pools eventually, + ** with some of the most relevant tile sizes. The EngineBufferManager + ** would then actually retain ownership of all allocations and just + ** _lease_ them to the LocalMemPool instances. Allocations would be + ** added in extents, likely also using some usage bitmap. However + ** building any such kind of optimised allocator requires to conduct + ** very detailed _and_ realistic performance observations; furthermore + ** it is likely that we'd need some dynamic scheme to select suitable + ** tile sizes and pool dimensions. Not sure if it is even possible + ** to improve performance by such a scheme; with some likelihood + ** the majority of the benefits will be reaped already just by + ** introducing thread-local pools without wasting too much memory. ** - ** @see buffer-provider.hpp + ** @see EnginePufferManager_test ** @see LocalBufferStore_test ** @see BufferProviderProtocol_test + ** @see buffer-provider.hpp */ #ifndef VAULT_MEM_ENGINE_BUFFR_MANAGA_H @@ -26,43 +44,120 @@ #include "lib/error.hpp" -#include "lib/hash-value.h" -#include "include/logging.h" +#include "lib/util-quant.hpp" +#include "lib/allocator-handle.hpp" +//#include "lib/hash-value.h" +//#include "include/logging.h" #include "vault/mem/buffhandle.hpp" -#include "vault/mem/type-handler.hpp" -#include "vault/mem/buffer-local-tag.hpp" +//#include "vault/mem/type-handler.hpp" +//#include "vault/mem/buffer-local-tag.hpp" #include "lib/nocopy.hpp" -#include "lib/util.hpp" +//#include "lib/util.hpp" -#include +#include +#include namespace vault{ namespace mem { - using lib::HashVal; - using util::unConst; +// using lib::HashVal; +// using util::unConst; namespace err = lumiera::error; - + using std::byte; - /** */ - - - - - namespace { // internal blah + namespace { // config and internal helpers... } - /* === deaf === */ + /* === Setup of »naïve« buffer allocation === */ + + /** + * A placeholder definition for the real buffer allocator: + * - allocates buffers aligned to a fixed #TILE_SIZ + * - passes any allocation request directly to the heap allocator + * - performs no state tracking and cleanup whatsoever. + */ + struct HeapBufferAllocator + { + static constexpr size_t TILE_SIZ = 64; + using BaseTile = std::array; + + using Allo = std::allocator; + using AlloT = std::allocator_traits; + + static_assert (util::isPow2 (sizeof(BaseTile))); + static_assert (lib::allo::is_Stateless_v); + + + struct Alloc + { + Buff* mem; + size_t siz; + }; + + static constexpr size_t + numTiles (size_t sizRequest) + { + return util::ceilDiv (sizRequest, sizeof(BaseTile)); + } + + static Buff* + mark_as_Buffer (void* mem) + { + return static_cast (mem); + } + + + /** + * Allocate continuous memory block large enough to fit. + * @throws std::bad_alloc when allocation fails + * @note client is responsible to \ref deallocate() + */ + Alloc + allocate (size_t sizRequest) + { + Allo allo; + auto cnt = numTiles (sizRequest); + auto loc = AlloT::allocate (allo, cnt); + return {mark_as_Buffer(loc) + ,cnt * TILE_SIZ + }; + } + + /** + * Deallocates a memory block that was previously + * [allocated](\ref allocate()) by this buffer allocator. + */ + void + deallocate (Alloc alloc) + { + Allo allo; + auto cnt = numTiles (alloc.siz); + auto mem = reinterpret_cast (alloc.mem); + AlloT::deallocate (allo, mem, cnt); + } + }; + + + /*********************************************************//** + * Core service of the Render Engine : global buffer manager. + */ + class EngineBufferManager + { + public: + using BufferAllocator = HeapBufferAllocator; + }; + + }} // namespace vault::mem #endif /*VAULT_MEM_ENGINE_BUFFR_MANAGA_H*/ diff --git a/src/vault/mem/heap-mem-buffer-store.cpp b/src/vault/mem/heap-mem-buffer-store.cpp index 887ecf56b..aaaace954 100644 --- a/src/vault/mem/heap-mem-buffer-store.cpp +++ b/src/vault/mem/heap-mem-buffer-store.cpp @@ -30,6 +30,7 @@ #include "lib/error.hpp" #include "lib/integral.hpp" +#include "lib/util-quant.hpp" #include "vault/mem/buffer-metadata.hpp" #include "lib/format-string.hpp" #include "lib/format-obj.hpp" @@ -50,16 +51,10 @@ namespace mem { using StorageWord = uint64_t; constexpr inline size_t WORD_SIZ = sizeof(StorageWord); - constexpr size_t - ceilDiv (size_t num, size_t den) noexcept - { - return (num + den - 1u) / den; - } - constexpr size_t wordCnt (size_t sizeRequest) noexcept { - return ceilDiv (sizeRequest, WORD_SIZ); + return util::ceilDiv (sizeRequest, WORD_SIZ); } static_assert (0 == wordCnt(0)); diff --git a/tests/47engine.tests b/tests/47engine.tests index de0a389b5..0612016a2 100644 --- a/tests/47engine.tests +++ b/tests/47engine.tests @@ -37,6 +37,10 @@ return: 0 END +TEST "Engine Buffer-Manager interface" EngineBufferManager_test < + +  **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 +  Free Software Foundation; either version 2 of the License, or (at your +  option) any later version. See the file COPYING for further details. + +* *****************************************************************/ + +/** @file engine-buffer-manager-test.cpp + ** unit test \ref EngineBufferManager_test + */ + + +#include "test/run.hpp" +#include "test/test-helper.hpp" +//#include "vault/mem/engine-buffer-metadata.hpp" +#include "vault/mem/engine-buffer-manager.hpp" +//#include "lib/depend-inject.hpp" +//#include "lib/thread.hpp" +//#include "lib/error.hpp" +#include "test/diagnostic-output.hpp"////////////TODO + + +//using std::this_thread::yield; +//using lib::Thread; + +namespace vault { +namespace mem { +namespace test { + +// using LERR_(LOGIC); +// using LERR_(LIFECYCLE); + + + namespace { // Test helper + +// template +// Buff* +// mark_as_Buffer(X& something) +// { +// return reinterpret_cast (std::addressof(something)); +// } +// + } + + + + + + + /*****************************************************************************//** + * @test Show how the central buffer manager of the Render Engine can be accessed. + */ + class EngineBufferManager_test : public Test + { + + virtual void + run (Arg) + { +// seedRand(); + + demonstrate_AllocatorInterface(); + verify_DataTransfer(); + verify_lulz(); + } + + + + /** @test show how to allocate buffer memory + * with the underlying base allocator. + * @remark 4/2026 for the first integration of the Render Engine, + * a simplified implementation is used that just delegates + * to the regular heap allocator (which actually performs + * quite well on modern C++ STDLib). The plan is to + * use some kind of tiling pool allocator eventually, + * contingent on the actual performance observations. + */ + void + demonstrate_AllocatorInterface() + { + using Allo = EngineBufferManager::BufferAllocator; + CHECK (lib::allo::is_Stateless_v); + + Allo allo; + auto a1 = allo.allocate (55); + auto a2 = allo.allocate (555); + CHECK (a1.mem); + CHECK (a2.mem); + CHECK (a1.siz == Allo::TILE_SIZ); + CHECK (a2.siz % Allo::TILE_SIZ == 0); + CHECK (a2.siz > 555); + + // can use that memory without blowing up.... + auto doooh = new(a1.mem) f128{std::numeric_limits::max()}; + *doooh /= 2; + CHECK (2 == std::numeric_limits::max() / *doooh); + + allo.deallocate (a1); + allo.deallocate (a2); + } + + + + /** @test */ + void + verify_DataTransfer() + { + } + + + + /** @test + */ + void + verify_lulz() + { + } + }; + + + /** Register this test class... */ + LAUNCHER (EngineBufferManager_test, "unit engine"); + + + +}}} // namespace vault::mem::test diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index fb4717fbb..591aea399 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -74191,8 +74191,9 @@ - + + @@ -74481,7 +74482,7 @@ - + @@ -145856,7 +145857,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -146091,10 +146093,191 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + - - + + + + + + + + + + + + +

+ ...das wäre nur der Fall, wenn ein Unit-Test „mal eben“ eine Render-Engine braucht. Denn sonst decken wir den Default-Fall bereits mit dem NaiveBufferSetup ab +

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

+ Und dann würde jeder Zugriff mit einer Exception scheitern.... +

+

+ So lange, bis man den statische Aufruf tätigt:  +

+

+ DependInject<Base>::useSingleton<Sub> +

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

+ Schon allein um die konfigurierbaren Aspekte klar herauszustellen, und um ggfs. Quellen dynamischer Konfiguration zu verdrahten +

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

+ ...das ist die übliche Cache-Line-Size auf x86_64 +

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

+ auch wenn es erst mal gedanklich schwerfällt, und für die zunächst angestrebte naive Implementierung wie ein sinnloser Tanz aussieht; denn dieses Framework ist gut durchdacht und sollte langsam zu einem Rückgrat für Allokationsvorgänge werden. Zur Erinnerung: dieses Konzept habe ich an die Stelle eines »globalen Allokators« gestellt. Zunächst hatten wir ja den Ansatz mit Christian's "Mempool" — und ich habe mich jahrelang mit der Planlosigkeit dieser Idee herumgeplagt, bis ich langsam verstanden habe, was daran schief ist.... +

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

+ Allokatoren sind composable +

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

+ weil sie für einen naiven Allokator nur schlecht zu implementieren ist +

+ + +
+ + + + + +

+ ...da wir ja letztlich alles an den globalen Allokator durchreichen wollen (weil der's sowiso besser kann), haben wir keinerlei Struktur mehr, die wir uns zunutze machen können ⟹ für jede de-Allokation müssen wir den Tracking-Eintrag suchen +

+ + +
+
+ + + + + + +

+ für eine Pool-Hierarchie dagegen ist eine effiziente Implementierung möglich +

+ + +
+ + + + + +

+ hierbei kann man sich die bekannte Element-Größe zunutze machen +

+
    +
  • + zunächst findet man den zuständigen Pool +
  • +
  • + sodann braucht es ggfs. einen Meta-Index oder eine sonstige Hilfstruktur, um ggfs. den jeweilgen Extent zu finden +
  • +
  • + im Extent muß man dann ein Bit in der Belegungs-Bitmap flippen +
  • +
+ + +
+
+
+
+
@@ -146102,20 +146285,66 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - - -

- zunächst wird nur diese genutzt -

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

+ Es ist nicht sinnvoll, eine »vorläufige« Implementierung eines heterogenen Memory-Managers zu machen. Da ist nämlich der Heap-Allokator  stets besser geeignet — ist inzwischen einigermaßen schwer geworden, den Heap-Allokator überhaupt noch zu schlagen. Mithin wird das auch gleich eine gute Baseline-Implementierung für Performance-Messungen +

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

+ Das ist nun wirklich mehr eine Demonstration, denn ich kann da praktisch nichts verifizieren (außer daß es keinen SEGFAULT gibt, wenn ich in den allozierten Speicher schreibe). Wenigstens ist damit schon mal sichergestellt, daß der Code durch den Compiler geht +

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

+ Das heißt, wir müssen uns darauf verlassen, daß der ganze komplexe Klapperatismus absolut korrekt funktioniert; wenn dem so ist, genügt es, einfach blindlings neue Allokationen an die lokalen Pools zu schicken und freizugeben was aus den lokalen Pools zurückgesendet wird. Das hat zur Konsequenz daß gewisse zusätzliche Konsistenzchecks eingebaut werden müssen, damit sich die Korrektheit auch erweisen läßt +

+ + +
+ + +
+