lumiera_/src/lib/allocation-cluster.hpp
Ichthyostega 806db414dd Copyright: clarify and simplify the file headers
* Lumiera source code always was copyrighted by individual contributors
 * there is no entity "Lumiera.org" which holds any copyrights
 * Lumiera source code is provided under the GPL Version 2+

== Explanations ==
Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above.
For this to become legally effective, the ''File COPYING in the root directory is sufficient.''

The licensing header in each file is not strictly necessary, yet considered good practice;
attaching a licence notice increases the likeliness that this information is retained
in case someone extracts individual code files. However, it is not by the presence of some
text, that legally binding licensing terms become effective; rather the fact matters that a
given piece of code was provably copyrighted and published under a license. Even reformatting
the code, renaming some variables or deleting parts of the code will not alter this legal
situation, but rather creates a derivative work, which is likewise covered by the GPL!

The most relevant information in the file header is the notice regarding the
time of the first individual copyright claim. By virtue of this initial copyright,
the first author is entitled to choose the terms of licensing. All further
modifications are permitted and covered by the License. The specific wording
or format of the copyright header is not legally relevant, as long as the
intention to publish under the GPL remains clear. The extended wording was
based on a recommendation by the FSF. It can be shortened, because the full terms
of the license are provided alongside the distribution, in the file COPYING.
2024-11-17 23:42:55 +01:00

384 lines
13 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
ALLOCATION-CLUSTER.hpp - allocating and owning a pile of objects
Copyright (C)
2008, Hermann Vosseler <Ichthyostega@web.de>
  **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 allocation-cluster.hpp
** Memory management for the low-level model (render nodes network).
** The model is organised into temporal segments, which are considered
** to be structurally constant and uniform. The objects within each
** segment are strongly interconnected, and thus each segment is created
** within a single build process and is replaced or released as a whole.
** AllocationCluster implements memory management to support this usage
** 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.
** \par dynamic adjustments
** Under controlled conditions, it is possible to change the size of the latest
** raw allocation handed out, within the limits of the available reserve in the
** current memory extent. Obviously, this is a dangerous low-level feature, yet
** offers some flexibility for containers and allocation schemes built on top.
** @warning deliberately *not threadsafe*.
** @remark confine usage to a single thread or use thread-local clusters.
** @see allocation-cluster-test.cpp
** @see builder::ToolFactory
** @see linked-elements.hpp
*/
#ifndef LIB_ALLOCATION_CLUSTER_H
#define LIB_ALLOCATION_CLUSTER_H
#include "lib/error.hpp"
#include "lib/nocopy.hpp"
#include <type_traits>
#include <utility>
#include <memory>
namespace lib {
namespace test { class AllocationCluster_test; } // declared friend for low-level-checks
namespace err = lumiera::error;
/**
* A pile of objects sharing common allocation and lifecycle.
* AllocationCluster owns a heterogeneous collection of objects of various types.
* Typically, allocation happens during a short time span when building a new segment,
* and objects are used together until the segment is discarded. The primary leverage
* is to bulk-allocate memory, and to avoid invoking destructors (and thus to access
* a lot of _cache-cold memory pages_ on clean-up). A Stdlib compliant #Allocator
* is provided for use with STL containers. The actual allocation uses heap memory
* in _extents_ of hard-wired size, maintained by the accompanying StorageManager.
* @warning use #createDisposable whenever possible, but be sure to understand
* the ramifications of _not invoking_ an object's destructor.
*/
class AllocationCluster
: util::MoveOnly
{
class StorageManager;
/** maintaining the Allocation */
struct Storage
{
void* pos{nullptr};
size_t rest{0};
auto bytePos() { return static_cast<std::byte*> (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 = 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_;
public:
AllocationCluster ();
~AllocationCluster () noexcept;
/** hard wired size of storage extents */
static size_t constexpr EXTENT_SIZ = 256;
static size_t constexpr max_size();
/* === diagnostics === */
size_t numExtents() const;
size_t numBytes() const;
template<class TY, typename...ARGS>
TY& create (ARGS&& ...);
template<class TY, typename...ARGS>
TY& createDisposable (ARGS&& ...);
template<typename X>
struct Allocator
{
using value_type = X;
[[nodiscard]] X* allocate (size_t n) { return mother_->allot<X>(n); }
void deallocate (X*, size_t) noexcept { /* rejoice */ }
Allocator(AllocationCluster* m) : mother_{m} { }
// standard copy acceptable
template<typename T>
Allocator(Allocator<T> const& o) : mother_{o.mother_} { }
template<typename T>
bool operator== (Allocator<T> const& o) const { return mother_ == o.mother_; }
AllocationCluster* mother_;
};
template<typename X>
Allocator<X> 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,
* possibly claiming a new pool block.
*/
void*
allotMemory (size_t bytes, size_t alignment)
{
__enforce_limits (bytes, alignment);
void* loc = storage_.allot(bytes, alignment);
if (loc) return loc;
expandStorage (bytes);
return allotMemory (bytes, alignment);
}
template<typename X>
X*
allot (size_t cnt =1)
{
return static_cast<X*> (allotMemory (cnt * sizeof(X), alignof(X)));
}
class Destructor
: util::NonCopyable
{
public:
virtual ~Destructor(); ///< this is an interface
Destructor* next{nullptr};// intrusive linked list...
};
/** @internal storage frame with the actual payload object,
* which can be attached to a list of destructors to invoke
*/
template<typename X>
struct AllocationWithDestructor
: Destructor
{
X payload;
template<typename...ARGS>
AllocationWithDestructor (ARGS&& ...args)
: payload(std::forward<ARGS> (args)...)
{ }
};
void expandStorage (size_t);
void registerDestructor (Destructor&);
void __enforce_limits (size_t,size_t);
friend class test::AllocationCluster_test;
};
//-----implementation-details------------------------
/**
* Maximum individual allocation size that can be handled.
* @remark AllocationCluser expands its storage buffer in steps
* of fixed sized _tiles_ or _extents._ Doing so can be beneficial
* when clusters are frequently created and thrown away (which is the
* intended usage pattern). However, using such extents is inherently
* wasteful, and thus the size must be rather tightly limited.
*/
size_t constexpr
AllocationCluster::max_size()
{
size_t ADMIN_OVERHEAD = 2 * sizeof(void*);
return EXTENT_SIZ - ADMIN_OVERHEAD;
}
/**
* Factory function: place a new instance into this AllocationCluster,
* but *without invoking its destructor* on clean-up (for performance reasons).
*/
template<class TY, typename...ARGS>
inline TY&
AllocationCluster::createDisposable (ARGS&& ...args)
{
return * new(allot<TY>()) TY (std::forward<ARGS> (args)...);
}
/**
* Factory function: place a new instance into this AllocationCluster;
* the object will be properly destroyed when the cluster goes out of scope.
* @note whenever possible, the #createDisposable variant should be preferred
*/
template<class TY, typename...ARGS>
inline TY&
AllocationCluster::create (ARGS&& ...args)
{
if constexpr (std::is_trivial_v<TY>)
return createDisposable<TY> (std::forward<ARGS> (args)...);
using Frame = AllocationWithDestructor<TY>;
auto& frame = createDisposable<Frame> (std::forward<ARGS> (args)...);
registerDestructor (frame);
return frame.payload;
}
/**
* 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 (offset));
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<std::byte const*> (pos) - siz;
}
//-----Policies-to-use-AllocationCluster------------------------
namespace allo { // Setup for custom allocator policies
// Forward declaration: configuration policy for lib::SeveralBuilder
template<class I, class E, template<typename> class ALO>
struct AllocationPolicy;
template<template<typename> class ALO, typename...ARGS>
struct SetupSeveral;
/**
* Specialisation to use lib::Several with storage managed by an
* AllocationCluster instance, which must be provided as argument.
* \code
* AllocationCluster clu;
* using Data = ....
* Several<Data> elms = makeSeveral<Data>()
* .withAllocator(clu)
* .fillElm(5)
* .build();
* \endcode
*/
template<>
struct SetupSeveral<std::void_t, lib::AllocationCluster&>
{
template<typename X>
using Adapter = typename AllocationCluster::template Allocator<X>;
template<class I, class E>
struct Policy
: AllocationPolicy<I,E,Adapter>
{
using Base = AllocationPolicy<I,E,Adapter>;
using Bucket = typename Base::Bucket;
/** @warning allocation size is severely limited in AllocationCluster. */
size_t static constexpr ALLOC_LIMIT = AllocationCluster::max_size();
Policy (AllocationCluster& clu)
: AllocationPolicy<I,E,Adapter> (clu.getAllocator<std::byte>())
{ }
bool
canExpand (Bucket* bucket, size_t request)
{
if (not bucket) return false;
size_t currSize = bucket->getAllocSize();
long delta = long(request) - long(bucket->buffSiz);
return this->mother_->canAdjust (bucket,currSize, currSize+delta);
}
Bucket*
realloc (Bucket* bucket, size_t cnt, size_t spread)
{
size_t request = cnt*spread;
REQUIRE (request);
if (not canExpand (bucket,request))
return Base::realloc (bucket,cnt,spread);
size_t currSize = bucket->getAllocSize();
size_t delta = request - bucket->buffSiz;
this->mother_->doAdjust (bucket, currSize, currSize+delta);
bucket->buffSiz += delta;
ENSURE (bucket->buffSiz == request);
return bucket;
}
};
};
//
}//(End)Allocator configuration
} // namespace lib
#endif /*LIB_ALLOCATION_CLUSTER_H*/