std.allocators.region
RegionAllocator is a memory allocator based on segmented stacks. A segmented stack is similar to a regular stack in that memory is allocated and freed in last in, first out order. When memory is requested from a segmented stack, it first checks whether enough space is available in the current segment, and if so bumps the stack pointer and returns. If not, a new segment is allocated. When memory is freed, the stack pointer is bumped down, and multiple allocations can be freed in a single operation. If the last segment is no longer in use, it may be returned to where it was allocated from or retained for future use. Synopsis:void fun() { // Create a new RegionAllocator using the default thread-local stack. auto alloc = newRegionAllocator(); // Allocate a temporary array on the RegionAllocator stack. auto arr = alloc.uninitializedArray!(double[][][])(8, 6, 7); assert(arr.length == 8); assert(arr[0].length == 6); assert(arr[0][0].length == 7); // When alloc goes out of scope, all memory allocated by it is freed. // If alloc has been copied, then the memory is freed when all copies // have gone out of scope. }A segmented stack may be created manually using the two-argument overload of newRegionAllocator. Alternatively, a default thread-local stack may be automatically lazily created using the zero-argument overload of this function. RegionAllocator has the following advantages compared to allocation on the call stack:
-
1. Pointers to memory allocated on a RegionAllocator stack are still
valid when the function they were allocated from returns, unless the
last instance of the RegionAllocator object they were allocated from
goes out of scope. Functions can be written to create and return data
structures on the RegionAllocator stack.
2. It's possible to have more than one stack per thread.
3. Since it is a segmented stack, large allocations can be performed with no
danger of stack overflow errors.
4. It guarantees 16-byte alignment of all allocated objects.
-
1. The extra checks due to stack segmentation make allocation slightly
slower and prevent simultanelous deallocation of several objects from
being a single decrement of the stack pointer.
2. Memory allocated on RegionAllocator is accessed through an extra layer of
pointer indirection compared to memory on the call stack.
-
1. Both allocation and deallocation are extremely fast. Most allocations
consist of verifying enough space is available, bumping up a pointer and
a performing a few cheap bookkeeping operations. Most deallocations
consist bumping down a pointer and performing a few cheap bookkeeping
operations.
2. The segmented stack is thread-local, so synchronization is only needed
when a segment needs to be allocated or freed.
3. Fragmentation is not an issue when allocating memory on the
RegionAllocator stack, though it can be an issue when trying to allocate
a new segment.
-
1. The requirement that memory be freed in last in, first out order.
2. No automatic garbage collection.
David Simcha License:
Boost License 1.0
- This struct provides an interface to the RegionAllocator functionality
and enforces scoped deletion. A new instance is created using the
newRegionAllocator function or the RegionAllocator.nested function.
An instance of a RegionAllocator is defined as a RegionAllocator
created by calling one of these functions and all copies of this initial object.
Each instance has reference semantics in that any copy will allocate from the
same memory. When the last copy of an instance goes out of scope, all memory
allocated via that instance is freed. Only the most recently created
still-existing RegionAllocator instance using a given stack may be used to
allocate and free memory at any given time. Deviations from this model result
in a RegionAllocatorError being thrown.
An uninitialized RegionAllocator (for example RegionAllocator.init)
has semantics similar to a null pointer. It may be assigned to or passed to
a function. However, any attempt to call a method will result in a
RegionAllocatorError being thrown.
Examples:
void foo() { auto alloc = newRegionAllocator(); auto ptr1 = bar(alloc); auto ptr2 = alloc.allocate(42); // The last copy of the RegionAllocator object used to allocate ptr1 // and ptr2 is going out of scope here. The memory pointed to by // both ptr1 and ptr2 will be freed. } void* bar(RegionAllocator alloc) { auto ret = alloc.allocate(42); auto alloc2 = alloc.nested(); auto ptr3 = alloc2.allocate(42); // ptr3 was allocated using alloc2, which is going out of scope. // Its memory will therefore be freed. ret was allocated using alloc. // A copy of this RegionAllocator is still alive in foo() after // bar() executes. Therefore, ret will not be freed on returning and // is still valid after bar() returns. return ret; }
void* thisIsSafe() { // This is safe because the two RegionAllocator objects being used // are using two different stacks. auto alloc = newRegionAllocator(); auto ptr1 = alloc.allocate(42); auto alloc2 = newRegionAllocator(1_048_576, GCScan.no); auto ptr2 = alloc2.allocate(42); auto ptr3 = alloc.allocate(42); }
void* dontDoThis() { auto alloc = newRegionAllocator(); auto ptr1 = alloc.allocate(42); auto alloc2 = alloc.nested(); // Error: Allocating from a RegionAllocator instance other than the // most recently created one that's still alive from a given stack. auto ptr = alloc.allocate(42); }
void uninitialized() { RegionAllocator alloc; auto ptr = alloc.allocate(42); // Error: alloc is not initialized. auto alloc2 = alloc; // Ok. Both alloc, alloc2 are uninitialized. alloc2 = newRegionAllocator(); auto ptr2 = alloc2.allocate(42); // Ok. auto ptr3 = alloc.allocate(42); // Error: alloc is still uninitialized. alloc = alloc2; auto ptr4 = alloc.allocate(42); // Ok. }
Note:
Allocations larger than this.segmentSize are handled as a special case and fall back to allocating directly from the C heap. These large allocations are freed as if they were allocated normally when free or freeLast is called or the last copy of a RegionAllocator instance goes out of scope. However, due to the extra bookkeeping required, destroying a region (as happens when the last copy of a RegionAllocator instance goes out of scope) will require time linear instead of constant in the number of allocations for regions where these large allocations are present. BUGS:
Finalizers/destuctors are never called on freeing objects allocated with create, newArray, uninitializedArray or array, as this would create an unacceptable amount of bookkeeping overhead and prevent O(1) freeing of regions.- Creates a new RegionAllocator instance using the same stack as
this. Memory can be allocated and freed from the returned
instance, but cannot be allocated and freed from this until the
last copy of the returned instance is destroyed.
Examples:
void fun() { auto alloc = newRegionAllocator(1_024 * 1_024, GCScan.no); auto ptr1 = alloc.allocate(42); foreach(i; 0..10) { auto alloc2 = alloc.nested(); auto ptr2 = alloc.allocate(42); // Error. auto ptr3 = alloc2.allocate(42); // Works. // ptr3 is freed on exiting this scope, since the last copy // of alloc2 is destroyed. } auto ptr4 = alloc.allocate(42); // Works. alloc2 no longer exists. // ptr4 is freed on exiting this scope, since the last copy of // alloc is destroyed. }
- Allocates nBytes bytes. The last block allocated from this RegionAllocator instance can be freed by calling RegionAllocator.free or RegionAllocator.freeLast or will be automatically freed when the last copy of this RegionAllocator instance goes out of scope. Allocation requests larger than segmentSize are allocated directly on the C heap, are scanned by the GC iff gcScanned is true.
- Frees the last block of memory allocated by the current RegionAllocator. Throws a RegionAllocatorError if this RegionAllocator is not the most recently created still-existing RegionAllocator on the stack it uses.
- Checks that ptr is a pointer to the block that would be freed by freeLast then calls freeLast. Throws a RegionAllocatorError if the pointer does not point to the block that would be freed by freeLast.
- Attempts to resize a previously allocated block of memory in place.
This is possible only if ptr points to the beginning of the last
block allocated by this RegionAllocator instance and, in the
case where newSize is greater than the old size, there is
additional space in the segment that ptr was allocated from.
Additionally, blocks larger than this RegionAllocator's segment size
cannot be grown or shrunk.
Returns:
True if the block was successfully resized, false otherwise. - Returns whether the stack used by this RegionAllocator instance is scanned by the garbage collector.
- Returns the number of bytes to which an allocation of size nBytes is guaranteed to be aligned.
- Returns the number of bytes used to satisfy an allocation request of nBytes. Will return a value greater than or equal to nBytes to account for alignment overhead.
- False because memory allocated by this allocator is not automatically reclaimed by the garbage collector.
- True because, when the last last copy of a instance goes out of scope, the memory it references is automatically freed.
- True because if memory is freed via free() instead of freeLast() then the pointer is checked for validity.
- Returns the segment size of the stack used by this RegionAllocator.
- Returns the maximum number of bytes that may be allocated in the current segment.
- Copies range, which must be a finite input range, to an array. The
array will be located on the RegionAllocator stack if any of the
following conditions apply:
1. std.traits.hasIndirections!(ElementType!R) is false.
2. R is a builtin array. In this case range maintains pointers
to all elements at least until array returns, preventing the
elements from being freed by the garbage collector. A similar
assumption cannot be made for ranges other than builtin arrays.
3. The stack used by this RegionAllocator is scanned by the garbage
collector.
If none of these conditions is met, the array is returned on the C heap
and GC.addRange is called. In either case, RegionAllocator.free,
RegionAllocator.freeLast, or the last copy of this RegionAllocator
instance going out of scope will free the array as if it had been
allocated on the RegionAllocator stack.
Rationale:
The most common reason to call array on a builtin array is to modify its contents inside a function without affecting the caller's view. In this case range is not modified and prevents the elements from being freed by the garbage collector. Furthermore, if the copy returned does need to be scanned, the client can call GC.addRange before modifying the original array. Examples:auto alloc = newRegionAllocator(); auto arr = alloc.array(iota(5)); assert(arr == [0, 1, 2, 3, 4]);
- Returns an instance of the std.allocators.allocator.DynamicAllocator
interface encapsulating this RegionAllocator instance. The
DynamicAllocator instance returned does not prevent the freeing of
memory allocated on this RegionAllocator instance. The memory is
freed when the last copy of this RegionAllocator instance goes out of
scope, even if a DynamicAllocator instance referencing this
RegionAllocator instance is still alive. Use of the
DynamicAllocator after the last copy of this RegionAllocator
instance has gone out of scope will result in undefined behavior.
Examples:
// The following is an example of what **not** to do: import std.allocators.region; void main() { auto dyn = returnDynamic(); } DynamicAllocator returnDynamic() { auto alloc = newRegionAllocator(); auto dyn = alloc.dynamicAllocator; // The last copy of alloc is going out of scope. When dyn is // returned, it will no longer be valid. Calling methods on it // from main() will result in undefined behavior. }
- Returns a new RegionAllocator that uses the default thread-local
stack. If this is called when a RegionAllocator using this stack
is already in existence, the semantics will be identical to calling the
nested method on the most recently created RegionAllocator
instance using this stack.
Examples:
void fun() { auto alloc = newRegionAllocator(); auto ptr1 = alloc.allocate(42); fun2(); // ptr1 freed on exit because last copy of alloc is going out of scope. } void fun2() { auto alloc2 = newRegionAllocator(); // Same as calling alloc.nested(). auto ptr2 = alloc2.allocate(42); // Works. // ptr2 freed on exit because last copy of alloc2 is going out of scope. }
- This flag determines whether a given RegionAllocator created using a
new stack is scanned for pointers by the garbage collector (GC). If yes, the
entire stack is scanned, not just the part currently in use, since there is
currently no efficient way to modify the bounds of a GC region. The stack is
scanned conservatively, meaning that any bit pattern that would point to
GC-allocated memory if interpreted as a pointer is considered to be a pointer.
This can result in GC-allocated memory being retained when it should be freed.
Due to these caveats, it is recommended that any stack scanned by the GC be
small and/or short-lived.
- Returns a new RegionAllocator that uses a newly created stack. The
underlying stack space is freed when the last RegionAllocator using
this stack goes out of scope.
Parameters:
size_t segmentSize The size of each segment of the stack. GCScan shouldScan Whether the stack should be scanned by the garbage collector. - These properties get and set the segment size of the default thread-local stack. The default size is 4 megabytes. The setter is only effective before the zero-argument overload of newRegionAllocator has been called for the first time in the current thread. Attempts to set this property after the first call to this function from the current thread throw a RegionAllocatorError.
- These properties determine whether the default thread-local stack is scanned by the garbage collector. The default is no. In most cases, scanning a stack this long-lived is not recommended, as it will cause too many false pointers. (See GCScan for details.) The setter is only effective before the zero-argument overload of newRegionAllocator has been called for the first time in the current thread. Attempts to set this property after the first call to this function from the current thread throw a RegionAllocatorError.
- The exception that is thrown on invalid use of . This exception is not thrown on out of memory. An OutOfMemoryError is thrown instead.