From 6c165b16035db5274e10df6ed912fe8169f45a47 Mon Sep 17 00:00:00 2001 From: ADAM David Alan Martin Date: Thu, 5 Sep 2024 17:06:52 -0400 Subject: [PATCH] Blob based per-thread slab allocator This permits "stateless" allocators which grab memory from a `thread_local Alepha::Blob` instance. Each allocation sticks a malloc cookie of type `std::shared_ptr< Alepha::Blob::StorageReservation >` just before the base of the allocation. The allocator object knows that it needs to `reinterpret_cast` the malloc cookie into a shared pointer and run its destructor. This causes the Blob's underlying reference counted allocation to be tied to the lifetime of the allocated memory. The intent is to permit cheap allocation in one thread and deallocation in another. Each deallocation should be a single atomic dereference operation. Each allocation should be (usually) a bit of pointer arithmetic and a single atomic increment operation. This, hopefully, eliminates significant thread contention for the global allocation mechanism between various threads in an intensive multithreaded situation where each processing thread thread may independently retire data objects allocated by a single source. --- Memory/Blob.h | 10 +- Memory/CMakeLists.txt | 1 + Memory/DataChain.h | 4 +- Memory/ThreadSlab.h | 129 ++++++++++++++++++++++++++ Memory/ThreadSlab.test/0.cc | 49 ++++++++++ Memory/ThreadSlab.test/CMakeLists.txt | 1 + 6 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 Memory/ThreadSlab.h create mode 100644 Memory/ThreadSlab.test/0.cc create mode 100644 Memory/ThreadSlab.test/CMakeLists.txt diff --git a/Memory/Blob.h b/Memory/Blob.h index c24cd6b..5123be8 100644 --- a/Memory/Blob.h +++ b/Memory/Blob.h @@ -17,6 +17,8 @@ static_assert( __cplusplus > 2020'99 ); #include "Buffer.h" +// TODO: Put this into the `Alepha::Memory::` namespace. +// TODO: Consider whether a "republish" into `Alepha::` namespace is a good idea. namespace Alepha::Hydrogen ::detail:: Blob_m { inline namespace exports @@ -94,6 +96,9 @@ namespace Alepha::Hydrogen ::detail:: Blob_m public: ~Blob() { reset(); } + using StorageReservation= IndirectStorage; + const StorageReservation &reservation() const { return storage; } + auto swap_lens() noexcept { @@ -218,7 +223,10 @@ namespace Alepha::Hydrogen ::detail:: Blob_m * inside a large single physical backing. This helps maintain zero-copy semantics. * * @param amount The amount of data to carve off. - * @return A new `Blob` object referring to the same physical data, scoped to `amount` bytes. + * @param alignment The size alignment that the new base should be at (the extra padding is + * considered part of the resulting `Blob`). + * @return A new `Blob` object referring to the same physical data, scoped to `amount` bytes (with + * possible extra space, due to alignment). */ Blob carveHead( const std::size_t amount ) diff --git a/Memory/CMakeLists.txt b/Memory/CMakeLists.txt index 0964a08..cb3030b 100644 --- a/Memory/CMakeLists.txt +++ b/Memory/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory( Blob.test ) +add_subdirectory( ThreadSlab.test ) diff --git a/Memory/DataChain.h b/Memory/DataChain.h index 6f33edd..0f5853e 100644 --- a/Memory/DataChain.h +++ b/Memory/DataChain.h @@ -52,7 +52,7 @@ namespace Alepha::inline Cavorite ::detail:: DataChain_m friend DataChain; - explicit Iterator( const ChainIter pos, cosnt std::size_t offset ) noexcept : pos( pos ), offset( offset ) {} + explicit Iterator( const ChainIter pos, const std::size_t offset ) noexcept : pos( pos ), offset( offset ) {} public: auto @@ -165,7 +165,7 @@ namespace Alepha::inline Cavorite ::detail:: DataChain_m std::copy_n( std::prev( end(), amount ), amount, rv.byte_data() ); return rv; - + } }; }; } diff --git a/Memory/ThreadSlab.h b/Memory/ThreadSlab.h new file mode 100644 index 0000000..3b12023 --- /dev/null +++ b/Memory/ThreadSlab.h @@ -0,0 +1,129 @@ +static_assert( __cplusplus > 2020'99 ); + +#pragma once + +#include + +#include + +#include + +namespace Alepha::Hydrogen::Memory ::detail:: ThreadSlab_m +{ + inline namespace exports + { + template< typename T > + class ThreadSlab; + + using ThreadSlabString= std::basic_string< char, std::char_traits< char >, ThreadSlab< char > >; + } + + namespace C + { + const std::size_t slabSize= 64 * 1024 * 1024; + + const bool debug= false; + const bool debugLifecycle= false or C::debug; + const bool debugAllocation= false or C::debug; + const bool debugDeallocation= false or C::debug; + } + + template< typename T > + class exports::ThreadSlab + { + public: + inline static thread_local Blob slab; + + public: + using value_type= T; + using propagate_on_container_copy_assignment= std::true_type; + using propagate_on_container_move_assignment= std::true_type; + using propagate_on_container_swap= std::true_type; + using is_always_equal= std::true_type; + + ThreadSlab select_on_container_copy_construction() { auto rv= ThreadSlab{}; } + + ThreadSlab()= default; + + + ThreadSlab &operator= ( const ThreadSlab &other )= default; + + ThreadSlab( const ThreadSlab &other )= default; + + ThreadSlab( ThreadSlab &&other ) : ThreadSlab( std::as_const( other ) ) {} + + ~ThreadSlab() + { + if( C::debugLifecycle ) + { + std::cerr << "Reporting " << slab.reservation().use_count() << " living allocations when " + << (void *) this << " is retired." << std::endl; + } + } + + [[nodiscard]] T * + allocate( const std::size_t amt ) + { + // TODO: Alignment needs to be handled. + const std::size_t req= amt + sizeof( Blob::StorageReservation ); + + // TODO: Larger allocations may be worth bespoke allocations, if they're rare one-off cases + if( req > C::slabSize ) throw std::bad_alloc{}; //{ "Unable to allocate larger than the slab size." }; + if( slab.size() < req ) slab.reset( std::max( req, C::slabSize ) ); + + if( C::debugAllocation ) + { + std::cerr << "Reporting " << slab.reservation().use_count() << " living allocations when " + << (void *) this << " made an allocation." << std::endl; + } + + auto next= slab.carveHead( req + sizeof( Blob::StorageReservation ) ); + const auto rv= reinterpret_cast< T * >( &next.template as< Blob::StorageReservation >() + 1 ); + + // FIXME: The placement new here is potentially unaligned -- this may significantly impact + // performance. + new ( &next.template as< Blob::StorageReservation >() ) Blob::StorageReservation{ std::move( next.reservation() ) }; + + if( C::debugAllocation ) + { + std::cerr << "Reporting " << slab.reservation().use_count() << " living allocations when " + << (void *) this << " made an allocation." << std::endl; + } + + return rv; + } + + template< typename SP > + static void + destroy( SP *p ) + { + p->~SP(); + } + + void + deallocate( T *const p, const std::size_t /* ignored */ ) noexcept + { + if( C::debugDeallocation ) + { + std::cerr << "Reporting " << slab.reservation().use_count() << " living allocations when " + << (void *) this << " made a deallocation." << std::endl; + } + + auto *const hidden= reinterpret_cast< Blob::StorageReservation * >( p ) - 1; + destroy( hidden ); + + if( C::debugDeallocation ) + { + std::cerr << "Reporting " << slab.reservation().use_count() << " living allocations when " + << (void *) this << " made a deallocation." << std::endl; + } + } + + friend constexpr bool operator == ( const ThreadSlab &, const ThreadSlab & ) noexcept { return true; } + }; +} + +namespace Alepha::Hydrogen::Memory::inline exports::inline ThreadSlab_m +{ + using namespace detail::ThreadSlab_m::exports; +} diff --git a/Memory/ThreadSlab.test/0.cc b/Memory/ThreadSlab.test/0.cc new file mode 100644 index 0000000..675b9f8 --- /dev/null +++ b/Memory/ThreadSlab.test/0.cc @@ -0,0 +1,49 @@ +static_assert( __cplusplus > 2020'99 ); + +#include "../ThreadSlab.h" + +#include + +#include + +static auto init= Alepha::Utility::enroll <=[] +{ + using namespace Alepha::Testing::literals; + + using namespace Alepha::Memory::exports::ThreadSlab_m; + using String= ThreadSlabString; + + + "Check slab usage"_test <=[] + { + std::cout << "I see " << Alepha::Memory::ThreadSlab< char >::slab.reservation().use_count() << " reservations in a separate test." << +std::endl; + }; + + "Can we work with simple `ThreadSlabStrings` without errors?"_test <=[] + { + String s; + std::cerr << "s is empty" << std::endl; + + String s2= "Hello World"; + std::cerr << "small hello world string." << std::endl; + + String s3= s2 + ": and bob"; + + for( int i= 0; i < 10; ++i ) + { + std::cerr << "appended..." << std::endl; + s3= s3 + s3 + s2; + + s2= std::move( s3 ); + } + + std::cout << s3 << std::endl; + }; + + "Check slab usage"_test <=[] + { + std::cout << "I see " << Alepha::Memory::ThreadSlab< char >::slab.reservation().use_count() << " reservations in a separate test." << +std::endl; + }; +}; diff --git a/Memory/ThreadSlab.test/CMakeLists.txt b/Memory/ThreadSlab.test/CMakeLists.txt new file mode 100644 index 0000000..b099603 --- /dev/null +++ b/Memory/ThreadSlab.test/CMakeLists.txt @@ -0,0 +1 @@ +unit_test( 0 )