// Copyright 2020 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef V8_HEAP_FREE_LIST_H_ #define V8_HEAP_FREE_LIST_H_ #include "src/base/macros.h" #include "src/common/globals.h" #include "src/heap/memory-chunk.h" #include "src/objects/free-space.h" #include "src/objects/map.h" #include "src/utils/utils.h" #include "testing/gtest/include/gtest/gtest_prod.h" // nogncheck namespace v8 { namespace internal { namespace heap { class HeapTester; class TestCodePageAllocatorScope; } // namespace heap class AllocationObserver; class FreeList; class Isolate; class LargeObjectSpace; class LargePage; class LinearAllocationArea; class Page; class PagedSpace; class SemiSpace; using FreeListCategoryType = int32_t; static const FreeListCategoryType kFirstCategory = 0; static const FreeListCategoryType kInvalidCategory = -1; enum FreeMode { kLinkCategory, kDoNotLinkCategory }; enum class SpaceAccountingMode { kSpaceAccounted, kSpaceUnaccounted }; // A free list category maintains a linked list of free memory blocks. class FreeListCategory { public: void Initialize(FreeListCategoryType type) { type_ = type; available_ = 0; prev_ = nullptr; next_ = nullptr; } void Reset(FreeList* owner); void RepairFreeList(Heap* heap); // Relinks the category into the currently owning free list. Requires that the // category is currently unlinked. void Relink(FreeList* owner); void Free(Address address, size_t size_in_bytes, FreeMode mode, FreeList* owner); // Performs a single try to pick a node of at least |minimum_size| from the // category. Stores the actual size in |node_size|. Returns nullptr if no // node is found. FreeSpace PickNodeFromList(size_t minimum_size, size_t* node_size); // Picks a node of at least |minimum_size| from the category. Stores the // actual size in |node_size|. Returns nullptr if no node is found. FreeSpace SearchForNodeInList(size_t minimum_size, size_t* node_size); inline bool is_linked(FreeList* owner) const; bool is_empty() { return top().is_null(); } uint32_t available() const { return available_; } size_t SumFreeList(); int FreeListLength(); private: // For debug builds we accurately compute free lists lengths up until // {kVeryLongFreeList} by manually walking the list. static const int kVeryLongFreeList = 500; // Updates |available_|, |length_| and free_list_->Available() after an // allocation of size |allocation_size|. inline void UpdateCountersAfterAllocation(size_t allocation_size); FreeSpace top() { return top_; } void set_top(FreeSpace top) { top_ = top; } FreeListCategory* prev() { return prev_; } void set_prev(FreeListCategory* prev) { prev_ = prev; } FreeListCategory* next() { return next_; } void set_next(FreeListCategory* next) { next_ = next; } // |type_|: The type of this free list category. FreeListCategoryType type_ = kInvalidCategory; // |available_|: Total available bytes in all blocks of this free list // category. uint32_t available_ = 0; // |top_|: Points to the top FreeSpace in the free list category. FreeSpace top_; FreeListCategory* prev_ = nullptr; FreeListCategory* next_ = nullptr; friend class FreeList; friend class FreeListManyCached; friend class PagedSpace; friend class MapSpace; }; // A free list maintains free blocks of memory. The free list is organized in // a way to encourage objects allocated around the same time to be near each // other. The normal way to allocate is intended to be by bumping a 'top' // pointer until it hits a 'limit' pointer. When the limit is hit we need to // find a new space to allocate from. This is done with the free list, which is // divided up into rough categories to cut down on waste. Having finer // categories would scatter allocation more. class FreeList { public: // Creates a Freelist of the default class (FreeListLegacy for now). V8_EXPORT_PRIVATE static FreeList* CreateFreeList(); virtual ~FreeList() = default; // Returns how much memory can be allocated after freeing maximum_freed // memory. virtual size_t GuaranteedAllocatable(size_t maximum_freed) = 0; // Adds a node on the free list. The block of size {size_in_bytes} starting // at {start} is placed on the free list. The return value is the number of // bytes that were not added to the free list, because the freed memory block // was too small. Bookkeeping information will be written to the block, i.e., // its contents will be destroyed. The start address should be word aligned, // and the size should be a non-zero multiple of the word size. virtual size_t Free(Address start, size_t size_in_bytes, FreeMode mode); // Allocates a free space node frome the free list of at least size_in_bytes // bytes. Returns the actual node size in node_size which can be bigger than // size_in_bytes. This method returns null if the allocation request cannot be // handled by the free list. virtual V8_WARN_UNUSED_RESULT FreeSpace Allocate(size_t size_in_bytes, size_t* node_size, AllocationOrigin origin) = 0; // Returns a page containing an entry for a given type, or nullptr otherwise. V8_EXPORT_PRIVATE virtual Page* GetPageForSize(size_t size_in_bytes) = 0; virtual void Reset(); // Return the number of bytes available on the free list. size_t Available() { DCHECK(available_ == SumFreeLists()); return available_; } // Update number of available bytes on the Freelists. void IncreaseAvailableBytes(size_t bytes) { available_ += bytes; } void DecreaseAvailableBytes(size_t bytes) { available_ -= bytes; } bool IsEmpty() { bool empty = true; ForAllFreeListCategories([&empty](FreeListCategory* category) { if (!category->is_empty()) empty = false; }); return empty; } // Used after booting the VM. void RepairLists(Heap* heap); V8_EXPORT_PRIVATE size_t EvictFreeListItems(Page* page); int number_of_categories() { return number_of_categories_; } FreeListCategoryType last_category() { return last_category_; } size_t wasted_bytes() { return wasted_bytes_; } template void ForAllFreeListCategories(FreeListCategoryType type, Callback callback) { FreeListCategory* current = categories_[type]; while (current != nullptr) { FreeListCategory* next = current->next(); callback(current); current = next; } } template void ForAllFreeListCategories(Callback callback) { for (int i = kFirstCategory; i < number_of_categories(); i++) { ForAllFreeListCategories(static_cast(i), callback); } } virtual bool AddCategory(FreeListCategory* category); virtual V8_EXPORT_PRIVATE void RemoveCategory(FreeListCategory* category); void PrintCategories(FreeListCategoryType type); protected: class FreeListCategoryIterator final { public: FreeListCategoryIterator(FreeList* free_list, FreeListCategoryType type) : current_(free_list->categories_[type]) {} bool HasNext() const { return current_ != nullptr; } FreeListCategory* Next() { DCHECK(HasNext()); FreeListCategory* tmp = current_; current_ = current_->next(); return tmp; } private: FreeListCategory* current_; }; #ifdef DEBUG V8_EXPORT_PRIVATE size_t SumFreeLists(); bool IsVeryLong(); #endif // Tries to retrieve a node from the first category in a given |type|. // Returns nullptr if the category is empty or the top entry is smaller // than minimum_size. FreeSpace TryFindNodeIn(FreeListCategoryType type, size_t minimum_size, size_t* node_size); // Searches a given |type| for a node of at least |minimum_size|. FreeSpace SearchForNodeInList(FreeListCategoryType type, size_t minimum_size, size_t* node_size); // Returns the smallest category in which an object of |size_in_bytes| could // fit. virtual FreeListCategoryType SelectFreeListCategoryType( size_t size_in_bytes) = 0; FreeListCategory* top(FreeListCategoryType type) const { return categories_[type]; } inline Page* GetPageForCategoryType(FreeListCategoryType type); int number_of_categories_ = 0; FreeListCategoryType last_category_ = 0; size_t min_block_size_ = 0; std::atomic wasted_bytes_{0}; FreeListCategory** categories_ = nullptr; // |available_|: The number of bytes in this freelist. size_t available_ = 0; friend class FreeListCategory; friend class Page; friend class MemoryChunk; friend class ReadOnlyPage; friend class MapSpace; }; // FreeList used for spaces that don't have freelists // (only the LargeObject space for now). class NoFreeList final : public FreeList { public: size_t GuaranteedAllocatable(size_t maximum_freed) final { FATAL("NoFreeList can't be used as a standard FreeList. "); } size_t Free(Address start, size_t size_in_bytes, FreeMode mode) final { FATAL("NoFreeList can't be used as a standard FreeList."); } V8_WARN_UNUSED_RESULT FreeSpace Allocate(size_t size_in_bytes, size_t* node_size, AllocationOrigin origin) final { FATAL("NoFreeList can't be used as a standard FreeList."); } Page* GetPageForSize(size_t size_in_bytes) final { FATAL("NoFreeList can't be used as a standard FreeList."); } private: FreeListCategoryType SelectFreeListCategoryType(size_t size_in_bytes) final { FATAL("NoFreeList can't be used as a standard FreeList."); } }; // Use 24 Freelists: on per 16 bytes between 24 and 256, and then a few ones for // larger sizes. See the variable |categories_min| for the size of each // Freelist. Allocation is done using a best-fit strategy (considering only the // first element of each category though). // Performances are expected to be worst than FreeListLegacy, but memory // consumption should be lower (since fragmentation should be lower). class V8_EXPORT_PRIVATE FreeListMany : public FreeList { public: size_t GuaranteedAllocatable(size_t maximum_freed) override; Page* GetPageForSize(size_t size_in_bytes) override; FreeListMany(); ~FreeListMany() override; V8_WARN_UNUSED_RESULT FreeSpace Allocate(size_t size_in_bytes, size_t* node_size, AllocationOrigin origin) override; protected: static const size_t kMinBlockSize = 3 * kTaggedSize; // This is a conservative upper bound. The actual maximum block size takes // padding and alignment of data and code pages into account. static const size_t kMaxBlockSize = MemoryChunk::kPageSize; // Largest size for which categories are still precise, and for which we can // therefore compute the category in constant time. static const size_t kPreciseCategoryMaxSize = 256; // Categories boundaries generated with: // perl -E ' // @cat = (24, map {$_*16} 2..16, 48, 64); // while ($cat[-1] <= 32768) { // push @cat, $cat[-1]*2 // } // say join ", ", @cat; // say "\n", scalar @cat' static const int kNumberOfCategories = 24; static constexpr unsigned int categories_min[kNumberOfCategories] = { 24, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536}; // Return the smallest category that could hold |size_in_bytes| bytes. FreeListCategoryType SelectFreeListCategoryType( size_t size_in_bytes) override { if (size_in_bytes <= kPreciseCategoryMaxSize) { if (size_in_bytes < categories_min[1]) return 0; return static_cast(size_in_bytes >> 4) - 1; } for (int cat = (kPreciseCategoryMaxSize >> 4) - 1; cat < last_category_; cat++) { if (size_in_bytes < categories_min[cat + 1]) { return cat; } } return last_category_; } FRIEND_TEST(SpacesTest, FreeListManySelectFreeListCategoryType); FRIEND_TEST(SpacesTest, FreeListManyGuaranteedAllocatable); }; // Same as FreeListMany but uses a cache to know which categories are empty. // The cache (|next_nonempty_category|) is maintained in a way such that for // each category c, next_nonempty_category[c] contains the first non-empty // category greater or equal to c, that may hold an object of size c. // Allocation is done using the same strategy as FreeListMany (ie, best fit). class V8_EXPORT_PRIVATE FreeListManyCached : public FreeListMany { public: FreeListManyCached(); V8_WARN_UNUSED_RESULT FreeSpace Allocate(size_t size_in_bytes, size_t* node_size, AllocationOrigin origin) override; size_t Free(Address start, size_t size_in_bytes, FreeMode mode) override; void Reset() override; bool AddCategory(FreeListCategory* category) override; void RemoveCategory(FreeListCategory* category) override; protected: // Updates the cache after adding something in the category |cat|. void UpdateCacheAfterAddition(FreeListCategoryType cat) { for (int i = cat; i >= kFirstCategory && next_nonempty_category[i] > cat; i--) { next_nonempty_category[i] = cat; } } // Updates the cache after emptying category |cat|. void UpdateCacheAfterRemoval(FreeListCategoryType cat) { for (int i = cat; i >= kFirstCategory && next_nonempty_category[i] == cat; i--) { next_nonempty_category[i] = next_nonempty_category[cat + 1]; } } #ifdef DEBUG void CheckCacheIntegrity() { for (int i = 0; i <= last_category_; i++) { DCHECK(next_nonempty_category[i] == last_category_ + 1 || categories_[next_nonempty_category[i]] != nullptr); for (int j = i; j < next_nonempty_category[i]; j++) { DCHECK(categories_[j] == nullptr); } } } #endif // The cache is overallocated by one so that the last element is always // defined, and when updating the cache, we can always use cache[i+1] as long // as i is < kNumberOfCategories. int next_nonempty_category[kNumberOfCategories + 1]; private: void ResetCache() { for (int i = 0; i < kNumberOfCategories; i++) { next_nonempty_category[i] = kNumberOfCategories; } // Setting the after-last element as well, as explained in the cache's // declaration. next_nonempty_category[kNumberOfCategories] = kNumberOfCategories; } }; // Same as FreeListManyCached but uses a fast path. // The fast path overallocates by at least 1.85k bytes. The idea of this 1.85k // is: we want the fast path to always overallocate, even for larger // categories. Therefore, we have two choices: either overallocate by // "size_in_bytes * something" or overallocate by "size_in_bytes + // something". We choose the later, as the former will tend to overallocate too // much for larger objects. The 1.85k (= 2048 - 128) has been chosen such that // for tiny objects (size <= 128 bytes), the first category considered is the // 36th (which holds objects of 2k to 3k), while for larger objects, the first // category considered will be one that guarantees a 1.85k+ bytes // overallocation. Using 2k rather than 1.85k would have resulted in either a // more complex logic for SelectFastAllocationFreeListCategoryType, or the 36th // category (2k to 3k) not being used; both of which are undesirable. // A secondary fast path is used for tiny objects (size <= 128), in order to // consider categories from 256 to 2048 bytes for them. // Note that this class uses a precise GetPageForSize (inherited from // FreeListMany), which makes its fast path less fast in the Scavenger. This is // done on purpose, since this class's only purpose is to be used by // FreeListManyCachedOrigin, which is precise for the scavenger. class V8_EXPORT_PRIVATE FreeListManyCachedFastPath : public FreeListManyCached { public: V8_WARN_UNUSED_RESULT FreeSpace Allocate(size_t size_in_bytes, size_t* node_size, AllocationOrigin origin) override; protected: // Objects in the 18th category are at least 2048 bytes static const FreeListCategoryType kFastPathFirstCategory = 18; static const size_t kFastPathStart = 2048; static const size_t kTinyObjectMaxSize = 128; static const size_t kFastPathOffset = kFastPathStart - kTinyObjectMaxSize; // Objects in the 15th category are at least 256 bytes static const FreeListCategoryType kFastPathFallBackTiny = 15; STATIC_ASSERT(categories_min[kFastPathFirstCategory] == kFastPathStart); STATIC_ASSERT(categories_min[kFastPathFallBackTiny] == kTinyObjectMaxSize * 2); FreeListCategoryType SelectFastAllocationFreeListCategoryType( size_t size_in_bytes) { DCHECK(size_in_bytes < kMaxBlockSize); if (size_in_bytes >= categories_min[last_category_]) return last_category_; size_in_bytes += kFastPathOffset; for (int cat = kFastPathFirstCategory; cat < last_category_; cat++) { if (size_in_bytes <= categories_min[cat]) { return cat; } } return last_category_; } FRIEND_TEST( SpacesTest, FreeListManyCachedFastPathSelectFastAllocationFreeListCategoryType); }; // Uses FreeListManyCached if in the GC; FreeListManyCachedFastPath otherwise. // The reasonning behind this FreeList is the following: the GC runs in // parallel, and therefore, more expensive allocations there are less // noticeable. On the other hand, the generated code and runtime need to be very // fast. Therefore, the strategy for the former is one that is not very // efficient, but reduces fragmentation (FreeListManyCached), while the strategy // for the later is one that is very efficient, but introduces some // fragmentation (FreeListManyCachedFastPath). class V8_EXPORT_PRIVATE FreeListManyCachedOrigin : public FreeListManyCachedFastPath { public: V8_WARN_UNUSED_RESULT FreeSpace Allocate(size_t size_in_bytes, size_t* node_size, AllocationOrigin origin) override; }; } // namespace internal } // namespace v8 #endif // V8_HEAP_FREE_LIST_H_