| // SPDX-License-Identifier: Apache-2.0 | |
| // Copyright Verizon Media 2020 | |
| /** @file | |
| MemArena memory allocator. Chunks of memory are allocated, frozen into generations and thawed | |
| away when unused. | |
| */ | |
| #include "swoc/MemArena.h" | |
| #include <algorithm> | |
| namespace swoc { inline namespace SWOC_VERSION_NS { | |
| void (*MemArena::destroyer)(MemArena *) = std::destroy_at<MemArena>; | |
| inline bool | |
| MemArena::Block::satisfies(size_t n, size_t align) const { | |
| auto r = this->remaining(); | |
| return r >= (n + align_padding(this->data() + allocated, align)); | |
| } | |
| MemArena::MemArena(MemSpan<void> static_block) { | |
| static constexpr Scalar<16, size_t> MIN_BLOCK_SIZE = round_up(sizeof(Block) + Block::MIN_FREE_SPACE); | |
| if (static_block.size() < MIN_BLOCK_SIZE) { | |
| throw std::domain_error("MemArena static block is too small."); | |
| } | |
| // Construct the block data in the block and put it on the list. Make a note this is the | |
| // static block that shouldn't be deleted. | |
| auto space = static_block.size() - sizeof(Block); | |
| _static_block = new (static_block.data()) Block(space); | |
| _active_reserved = space; | |
| _active.prepend(_static_block); | |
| } | |
| // Need to break these out because the default implementation doesn't clear the | |
| // integral values in @a that. | |
| MemArena::MemArena(swoc::MemArena::self_type &&that) noexcept | |
| : _active_allocated(that._active_allocated), | |
| _active_reserved(that._active_reserved), | |
| _frozen_allocated(that._frozen_allocated), | |
| _frozen_reserved(that._frozen_reserved), | |
| _reserve_hint(that._reserve_hint), | |
| _frozen(std::move(that._frozen)), | |
| _active(std::move(that._active)), | |
| _static_block(that._static_block) { | |
| // Clear data in @a that to indicate all of the memory has been moved. | |
| that._active_allocated = that._active_reserved = 0; | |
| that._frozen_allocated = that._frozen_reserved = 0; | |
| that._reserve_hint = 0; | |
| that._static_block = nullptr; | |
| } | |
| MemArena * | |
| MemArena::construct_self_contained(size_t n) { | |
| MemArena tmp{n + sizeof(MemArena)}; | |
| return tmp.make<MemArena>(std::move(tmp)); | |
| } | |
| MemArena & | |
| MemArena::operator=(swoc::MemArena::self_type &&that) noexcept { | |
| this->clear(); | |
| std::swap(_active_allocated, that._active_allocated); | |
| std::swap(_active_reserved, that._active_reserved); | |
| std::swap(_frozen_allocated, that._frozen_allocated); | |
| std::swap(_frozen_reserved, that._frozen_reserved); | |
| std::swap(_reserve_hint, that._reserve_hint); | |
| _active = std::move(that._active); | |
| _frozen = std::move(that._frozen); | |
| return *this; | |
| } | |
| MemArena::Block * | |
| MemArena::make_block(size_t n) { | |
| // If there's no reservation hint, use the extent. This is transient because the hint is cleared. | |
| if (_reserve_hint == 0) { | |
| if (_active_reserved) { | |
| _reserve_hint = _active_reserved; | |
| } else if (_frozen_allocated) { // immediately after freezing - use that extent. | |
| _reserve_hint = _frozen_allocated; | |
| } | |
| } | |
| n = std::max<size_t>(n, _reserve_hint); | |
| _reserve_hint = 0; // did this, clear for next time. | |
| // Add in overhead and round up to paragraph units. | |
| n = Paragraph{round_up(n + ALLOC_HEADER_SIZE + sizeof(Block))}; | |
| // If more than a page or withing a quarter page of a full page, | |
| // round up to page unit size and clip back to account for alloc header. | |
| if (n >= (Page::SCALE - QuarterPage::SCALE)) { | |
| n = Page{round_up(n)} - ALLOC_HEADER_SIZE; | |
| } else if (n >= QuarterPage::SCALE) { // if at least a quarter page, round up to quarter pages. | |
| n = QuarterPage{round_up(n)}; | |
| } | |
| // Allocate space for the Block instance and the requested memory and construct a Block at the front. | |
| auto free_space = n - sizeof(Block); | |
| _active_reserved += free_space; | |
| return new (n) Block(free_space); | |
| } | |
| MemSpan<void> | |
| MemArena::alloc(size_t n, size_t align) { | |
| MemSpan<void> zret; | |
| this->require(n, align); | |
| auto block = _active.head(); | |
| zret = block->alloc(n, align); | |
| _active_allocated += n; | |
| // If this block is now full, move it to the back. | |
| if (block->is_full() && block != _active.tail()) { | |
| _active.erase(block); | |
| _active.append(block); | |
| } | |
| return zret; | |
| } | |
| MemArena & | |
| MemArena::freeze(size_t n) { | |
| this->destroy_frozen(); | |
| _frozen = std::move(_active); | |
| // Update the meta data. | |
| _frozen_allocated = _active_allocated; | |
| _active_allocated = 0; | |
| _frozen_reserved = _active_reserved; | |
| _active_reserved = 0; | |
| _reserve_hint = n; | |
| return *this; | |
| } | |
| MemArena & | |
| MemArena::thaw() { | |
| this->destroy_frozen(); | |
| _frozen_reserved = _frozen_allocated = 0; | |
| if (_static_block) { | |
| _static_block->discard(); | |
| _active.prepend(_static_block); | |
| _active_reserved += _static_block->remaining(); | |
| } | |
| return *this; | |
| } | |
| bool | |
| MemArena::contains(const void *ptr) const { | |
| auto pred = [ptr](const Block &b) -> bool { return b.contains(ptr); }; | |
| return std::any_of(_active.begin(), _active.end(), pred) || std::any_of(_frozen.begin(), _frozen.end(), pred); | |
| } | |
| MemArena & | |
| MemArena::require(size_t n, size_t align) { | |
| auto spot = _active.begin(); | |
| Block *block{nullptr}; | |
| // Search back through the list until a full block is hit, which is a miss. | |
| while (spot != _active.end() && !spot->satisfies(n, align)) { | |
| if (spot->is_full()) { | |
| spot = _active.end(); | |
| } else { | |
| ++spot; | |
| } | |
| } | |
| if (spot == _active.end()) { // no block has enough free space | |
| block = this->make_block(n); // assuming a new block is sufficiently aligned. | |
| _active.prepend(block); | |
| } else if (spot != _active.begin()) { | |
| // big enough space, move to the head of the list. | |
| block = spot; | |
| _active.erase(block); | |
| _active.prepend(block); | |
| } | |
| // Invariant - the head active block has at least @a n bytes of free storage. | |
| return *this; | |
| } | |
| void | |
| MemArena::destroy_active() { | |
| auto sb = _static_block; // C++20 nonsense - capture of @a this is incompatible with C++17. | |
| _active | |
| .apply([=](Block *b) { | |
| if (b != sb) { | |
| delete b; | |
| } | |
| }) | |
| .clear(); | |
| } | |
| void | |
| MemArena::destroy_frozen() { | |
| auto sb = _static_block; // C++20 nonsense - capture of @a this is incompatible with C++17. | |
| _frozen | |
| .apply([=](Block *b) { | |
| if (b != sb) { | |
| delete b; | |
| } | |
| }) | |
| .clear(); | |
| } | |
| MemArena & | |
| MemArena::clear(size_t hint) { | |
| _reserve_hint = hint ? hint : _frozen_allocated + _active_allocated; | |
| _frozen_reserved = _frozen_allocated = 0; | |
| _active_reserved = _active_allocated = 0; | |
| this->destroy_frozen(); | |
| this->destroy_active(); | |
| return *this; | |
| } | |
| MemArena & | |
| MemArena::discard(MemSpan<const void> span) { | |
| // This is intended to iterate over empty blocks until @a span is found. | |
| for ( auto & block : _active) { | |
| if (block.contains(span.data())) { // it's in this block, final iteration. | |
| if (block.allocated_data_end() == span.data_end()) { | |
| block.allocated -= span.size(); | |
| _active_allocated -= span.size(); | |
| } | |
| break; | |
| } else if (block.allocated > 0) { | |
| // If the block wasn't empty the only other place | |
| // @a span could be is in the most recent filled block, which is last in the list. | |
| // Invariant - the first block does not contain @a span. | |
| // Therefore, if the last block contains @a span, it is not the first block. | |
| auto lfb = _active.tail(); // list is not empty, must exist. | |
| if (lfb->contains(span.data()) && lfb->allocated_data_end() == span.data_end()) { | |
| lfb->allocated -= span.size(); | |
| _active_allocated -= span.size(); | |
| if (!lfb->is_full()) { | |
| _active.erase(lfb); | |
| _active.prepend(lfb); | |
| } | |
| } | |
| break; // loop always ends after hitting a non-empty block. | |
| } | |
| } | |
| return *this; | |
| } | |
| MemArena & | |
| MemArena::discard(size_t hint) { | |
| // Because existing blocks remain, clear the reserve hint so then when a new block is allocated | |
| // it uses the allocation size then, not what it is now. Now is handled by the existing blocks, | |
| // unless the caller explicitly provides a hint, | |
| _reserve_hint = hint ? hint : 0; | |
| for (auto &block : _active) { | |
| block.discard(); | |
| } | |
| _active_allocated = 0; | |
| return *this; | |
| } | |
| MemArena::~MemArena() { | |
| // Destruct in a way that makes it safe for the instance to be in one of its own memory blocks. | |
| // This means copying members that will be used during the delete. | |
| Block *ba = _active.head(); | |
| Block *bf = _frozen.head(); | |
| Block *sb = _static_block; | |
| _active.clear(); | |
| _frozen.clear(); | |
| while (bf) { | |
| Block *b = bf; | |
| bf = bf->_link._next; | |
| if (b != sb) { | |
| delete b; | |
| } | |
| } | |
| while (ba) { | |
| Block *b = ba; | |
| ba = ba->_link._next; | |
| if (b != sb) { | |
| delete b; | |
| } | |
| } | |
| } | |
| #if __has_include(<memory_resource>) | |
| void * | |
| MemArena::do_allocate(std::size_t bytes, std::size_t align) { | |
| return this->alloc(bytes, align).data(); | |
| } | |
| void | |
| MemArena::do_deallocate(void *, size_t, size_t) {} | |
| bool | |
| MemArena::do_is_equal(std::pmr::memory_resource const &that) const noexcept { | |
| return this == &that; | |
| } | |
| #endif | |
| }} // namespace swoc::SWOC_VERSION_NS |