// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "TraceServices/Containers/Allocators.h" #include "Containers/Array.h" namespace TraceServices { //////////////////////////////////////////////////////////////////////////////////////////////////// // // The TVariablePagedArrayPage allocates items in fixed size pages. Each page allocates space for // PageSize items, but a page may not use all its allocated items. // // To optimize lookup by index, as O(log(n)), a PageGroups array was added to identify the lists of // full pages. After initial lookup, an iterator is O(1). // // Example: // Page Size: 8 items // Pages: A B C D E F G H // Full Pages: ? * ? ? * * * ? // pages that are known to be full // Item Count: [8 8 7] [3 8 8 8 8] // used items in each page // First Index: 0 8 16 23 26 34 42 50 // first index in each page // Last Index: 7 15 22 30 33 41 49 57 // last index in each page // Page Groups: // group 0: 3 pages [A - C] 23 items [ 0 - 22] // group 1: 6 pages [D - H] 35 items [23 - 57] // // The first page (ex.: A or D) and the last page (ex.: C or H) in a group may or may not be full. // But all middle pages in a group (ex.: B, E, F, G) are always full. // // When adding an item (PushBack), it will just add the respective item in the last page (adding a // new page if necessary). // // When inserting items, if the page where insertion occurs has unused items, the item will be // inserted in the respective page (no additional pages or groups are created). If page does not have // unused items, it will be split in two pages (also a new page group will be created). // // Over TPagedArrayPage implementation, this has the advantage of being much faster when inserting // items. There will be insertions only in pages that have unused items. The downside is the extra // memory allocated and not used. // //////////////////////////////////////////////////////////////////////////////////////////////////// // For debugging the TVariablePagedArray implementation. #define DEBUG_VARIABLE_PAGED_ARRAY 0 #if DEBUG_VARIABLE_PAGED_ARRAY #define VARIABLE_PAGED_ARRAY_CHECK(x) check(x) #else #define VARIABLE_PAGED_ARRAY_CHECK(x) #endif //////////////////////////////////////////////////////////////////////////////////////////////////// // In a page, used items are [ItemOffset .. ItemOffset + ItemCount - 1]. // Unused items are [0 .. ItemOffset - 1] and [ItemOffset + ItemCount .. PageSize - 1]. template struct TVariablePagedArrayPage { ItemType* Items = nullptr; uint64 ItemOffset = 0; uint64 ItemCount = 0; }; //////////////////////////////////////////////////////////////////////////////////////////////////// template inline const ItemType* GetData(const TVariablePagedArrayPage& Page) { return Page.Items + Page.ItemOffset; } template inline SIZE_T GetNum(const TVariablePagedArrayPage& Page) { return Page.ItemCount; } template inline const ItemType* GetFirstItem(const TVariablePagedArrayPage& Page) { return Page.Items + Page.ItemOffset; } template inline const ItemType* GetLastItem(const TVariablePagedArrayPage& Page) { if (Page.ItemCount) { return Page.Items + Page.ItemOffset + Page.ItemCount - 1; } else { return nullptr; } } //////////////////////////////////////////////////////////////////////////////////////////////////// template struct TVariablePagedArrayPageGroup { uint64 FirstItemIndex = 0; uint64 ItemCount = 0; uint64 FirstPageIndex = 0; uint64 PageCount = 0; }; //////////////////////////////////////////////////////////////////////////////////////////////////// template class TVariablePagedArray; template class TVariablePagedArrayIterator { public: typedef TVariablePagedArrayPage PageType; typedef TVariablePagedArrayPageGroup PageGroupType; typedef TVariablePagedArray ArrayType; public: TVariablePagedArrayIterator(const ArrayType& InOuter) : Outer(&InOuter) { #if DEBUG_VARIABLE_PAGED_ARRAY Outer->CheckIntegrity(); #endif SetPositionAtFirstItem(); } TVariablePagedArrayIterator(const ArrayType& InOuter, const uint64 ItemIndex) : Outer(&InOuter) { #if DEBUG_VARIABLE_PAGED_ARRAY Outer->CheckIntegrity(); #endif // This does not validate implementation, but user input. check(ItemIndex < Outer->TotalItemCount); const PageType* Page; uint64 ItemIndexInPage; Outer->FindItemChecked(ItemIndex, Page, ItemIndexInPage); CurrentPage = Page; OnCurrentPageChanged(); CurrentItemIndex = ItemIndex; VARIABLE_PAGED_ARRAY_CHECK(ItemIndexInPage < CurrentPage->ItemCount); CurrentItem = CurrentPage->Items + CurrentPage->ItemOffset + ItemIndexInPage; } const PageType* GetCurrentPage() { return CurrentPage; } const ItemType* GetCurrentItem() { return CurrentItem; } const uint64 GetCurrentItemIndex() { return CurrentItemIndex; } const ItemType& operator*() const { return *CurrentItem; } const ItemType* operator->() const { return CurrentItem; } explicit operator bool() const { return CurrentItem != nullptr; } const PageType* PrevPage() { CurrentItemIndex -= static_cast(CurrentItem - CurrentPageFirstItem) + 1; if (CurrentPage != Outer->FirstPage) { --CurrentPage; OnCurrentPageChanged(); CurrentItem = CurrentPageLastItem; } else { CurrentPage = nullptr; CurrentItemIndex = 0; CurrentItem = nullptr; CurrentPageFirstItem = nullptr; CurrentPageLastItem = nullptr; } return CurrentPage; } const PageType* NextPage() { CurrentItemIndex += static_cast(CurrentPageLastItem - CurrentItem) + 1; if (CurrentPage != Outer->LastPage) { ++CurrentPage; OnCurrentPageChanged(); CurrentItem = CurrentPageFirstItem; } else { CurrentPage = nullptr; CurrentItemIndex = 0; CurrentItem = nullptr; CurrentPageFirstItem = nullptr; CurrentPageLastItem = nullptr; } return CurrentPage; } const ItemType* PrevItem() { if (CurrentItem == CurrentPageFirstItem) { PrevPage(); } else { --CurrentItemIndex; --CurrentItem; } return CurrentItem; } TVariablePagedArrayIterator& operator--() { PrevItem(); return *this; } TVariablePagedArrayIterator operator--(int) { TVariablePagedArrayIterator Tmp(*this); PrevItem(); return Tmp; } const ItemType* NextItem() { if (CurrentItem == CurrentPageLastItem) { NextPage(); } else { ++CurrentItemIndex; ++CurrentItem; } return CurrentItem; } TVariablePagedArrayIterator& operator++() { NextItem(); return *this; } TVariablePagedArrayIterator operator++(int) { TVariablePagedArrayIterator Tmp(*this); NextItem(); return Tmp; } const ItemType* SetPositionAtFirstItem() { // This does not validate implementation, but user input. check(Outer->TotalItemCount > 0); CurrentPage = Outer->FirstPage; OnCurrentPageChanged(); CurrentItemIndex = 0; CurrentItem = CurrentPageFirstItem; return CurrentItem; } const ItemType* SetPositionAtLastItem() { // This does not validate implementation, but user input. check(Outer->TotalItemCount > 0); CurrentPage = Outer->LastPage; OnCurrentPageChanged(); CurrentItemIndex = Outer->TotalItemCount - 1; CurrentItem = CurrentPageLastItem; return CurrentItem; } const ItemType* SetPosition(uint64 Index) { // This does not validate implementation, but user input. check(Index < Outer->TotalItemCount); PageType* Page; uint64 ItemIndexInPage; Outer->FindItemChecked(Index, Page, ItemIndexInPage); CurrentPage = Page; OnCurrentPageChanged(); CurrentItemIndex = Index; CurrentItem = CurrentPageFirstItem + ItemIndexInPage; return CurrentItem; } private: void OnCurrentPageChanged() { CurrentPageFirstItem = CurrentPage->Items + CurrentPage->ItemOffset; CurrentPageLastItem = CurrentPageFirstItem + CurrentPage->ItemCount - 1; } const ArrayType* Outer = nullptr; const PageType* CurrentPage = nullptr; uint64 CurrentItemIndex = 0; const ItemType* CurrentItem = nullptr; const ItemType* CurrentPageFirstItem = nullptr; const ItemType* CurrentPageLastItem = nullptr; }; //////////////////////////////////////////////////////////////////////////////////////////////////// template class TVariablePagedArray { public: typedef InItemType ItemType; typedef TVariablePagedArrayPage PageType; typedef TVariablePagedArrayPageGroup PageGroupType; typedef TVariablePagedArrayIterator TIterator; TVariablePagedArray(ILinearAllocator& InAllocator, uint64 InPageSize) : Allocator(InAllocator) , Pages() , FirstPage(nullptr) , LastPage(nullptr) , PageGroups() , FirstPageGroup(nullptr) , LastPageGroup(nullptr) , PageSize(InPageSize) , TotalItemCount(0) { } ~TVariablePagedArray() { for (PageType& Page : Pages) { const ItemType* const PageStart = Page.Items + Page.ItemOffset; const ItemType* const PageEnd = PageStart + Page.ItemCount; for (const ItemType* Item = PageStart; Item != PageEnd; ++Item) { Item->~ItemType(); } } } uint64 Num() const { return TotalItemCount; } uint64 GetPageSize() const { return PageSize; } uint64 NumPages() const { return Pages.Num(); } uint64 NumItemsWasted() const { const uint64 AllocatedItemCount = Pages.Num() * PageSize; return AllocatedItemCount - TotalItemCount; } double WastedPercent() const { const uint64 AllocatedItemCount = Pages.Num() * PageSize; return (AllocatedItemCount == 0) ? 0.0 : static_cast(AllocatedItemCount - TotalItemCount) / static_cast(AllocatedItemCount); } ItemType& PushBack() { if (!LastPage) { CreateInitialPage(); } if (LastPage->ItemOffset + LastPage->ItemCount == PageSize) { // Add new page. PageType& NewPage = Pages.AddDefaulted_GetRef(); NewPage.Items = reinterpret_cast(Allocator.Allocate(PageSize * sizeof(ItemType))); FirstPage = Pages.GetData(); LastPage = &NewPage; ++LastPageGroup->PageCount; } ItemType* ItemPtr = LastPage->Items + LastPage->ItemOffset + LastPage->ItemCount; new (ItemPtr) ItemType(); ++LastPage->ItemCount; ++LastPageGroup->ItemCount; ++TotalItemCount; return *ItemPtr; } ItemType& Insert(uint64 Index) { if (Index >= TotalItemCount) { return PushBack(); } uint64 PageGroupIndex = Algo::UpperBoundBy(PageGroups, Index, &PageGroupType::FirstItemIndex); VARIABLE_PAGED_ARRAY_CHECK(PageGroupIndex > 0 && PageGroupIndex <= PageGroups.Num()); --PageGroupIndex; PageGroupType* PageGroup = PageGroups.GetData() + PageGroupIndex; VARIABLE_PAGED_ARRAY_CHECK(Index >= PageGroup->FirstItemIndex); VARIABLE_PAGED_ARRAY_CHECK(Index < PageGroup->FirstItemIndex + PageGroup->ItemCount); if (Index == PageGroup->FirstItemIndex && PageGroupIndex > 0) { return PushBackItemInPageGroup(PageGroupIndex - 1); } // Find page containing the insertion index. uint64 ItemIndex = Index - PageGroup->FirstItemIndex; PageType* Page = Pages.GetData() + PageGroup->FirstPageIndex; // The first page (and the last page) in a group may have less items than PageSize. if (ItemIndex >= Page->ItemCount) { // Skip the first page in group. ItemIndex -= Page->ItemCount; ++Page; } Page += ItemIndex / PageSize; ItemIndex = ItemIndex % PageSize; VARIABLE_PAGED_ARRAY_CHECK(ItemIndex < Page->ItemCount); // Does the page have unused items? if (Page->ItemCount < PageSize) { // Are unused items at the end of the page? if (Page->ItemOffset + Page->ItemCount < PageSize) { // Yes. Insert new item. Move items to the right to make room for the new item. ItemType* ItemPtr = Page->Items + Page->ItemOffset + ItemIndex; memmove(ItemPtr + 1, ItemPtr, sizeof(ItemType) * (Page->ItemCount - ItemIndex)); ++Page->ItemCount; OnInsertedItem(PageGroupIndex); return *ItemPtr; } else { // No. We have unused items at the begining of the page. VARIABLE_PAGED_ARRAY_CHECK(Page->ItemOffset > 0); // Insert new item. Move items to the left to make room for the new item. if (ItemIndex > 0) { ItemType* const FirstItemPtr = Page->Items + Page->ItemOffset; memmove(FirstItemPtr - 1, FirstItemPtr, sizeof(ItemType) * ItemIndex); } --Page->ItemOffset; ++Page->ItemCount; OnInsertedItem(PageGroupIndex); ItemType* ItemPtr = Page->Items + Page->ItemOffset + ItemIndex; return *ItemPtr; } } // Page is full (no unused items). VARIABLE_PAGED_ARRAY_CHECK(Page->ItemCount == PageSize); ItemType* ItemPtr; // Split page. uint64 LeftPageIndex = Page - Pages.GetData(); PageType* LeftPage; PageType* RightPage; if (ItemIndex == 0) { // No need to split the page. Just add a new one in front of it. LeftPage = &Pages.InsertDefaulted_GetRef(static_cast(LeftPageIndex)); RightPage = LeftPage + 1; LeftPage->Items = reinterpret_cast(Allocator.Allocate(PageSize * sizeof(ItemType))); LeftPage->ItemCount = 1; // The new inserted item will be the first item in left page (but last item in the left page group). ItemPtr = LeftPage->Items; } else { // Add another page after current one. RightPage = &Pages.InsertDefaulted_GetRef(static_cast(LeftPageIndex) + 1); LeftPage = RightPage - 1; RightPage->Items = reinterpret_cast(Allocator.Allocate(PageSize * sizeof(ItemType))); RightPage->ItemCount = LeftPage->ItemCount - ItemIndex; RightPage->ItemOffset = PageSize - RightPage->ItemCount; LeftPage->ItemCount = ItemIndex + 1; // The inserted item will be the last item in left page (also last item in the left page group). ItemPtr = LeftPage->Items + LeftPage->ItemOffset + ItemIndex; // Move items from current (left) page to new (right) page. memcpy(RightPage->Items + RightPage->ItemOffset, ItemPtr, sizeof(ItemType) * RightPage->ItemCount); } OnInsertedPage(PageGroupIndex); OnInsertedItem(PageGroupIndex); // Split the page group. PageGroupType* RightPageGroup = &PageGroups.InsertDefaulted_GetRef(static_cast(PageGroupIndex) + 1); FirstPageGroup = PageGroups.GetData(); LastPageGroup = PageGroups.GetData() + PageGroups.Num() - 1; PageGroupType* LeftPageGroup = RightPageGroup - 1; const uint64 DuoItemCount = LeftPageGroup->ItemCount; const uint64 DuoPageCount = LeftPageGroup->PageCount; //LeftPageGroup->FirstItemIndex = unchanged LeftPageGroup->ItemCount = Index - LeftPageGroup->FirstItemIndex + 1; //LeftPageGroup->FirstPageIndex = unchanged LeftPageGroup->PageCount = LeftPageIndex - LeftPageGroup->FirstPageIndex + 1; RightPageGroup->FirstItemIndex = LeftPageGroup->FirstItemIndex + LeftPageGroup->ItemCount; RightPageGroup->ItemCount = DuoItemCount - LeftPageGroup->ItemCount; RightPageGroup->FirstPageIndex = LeftPageGroup->FirstPageIndex + LeftPageGroup->PageCount; RightPageGroup->PageCount = DuoPageCount - LeftPageGroup->PageCount; #if DEBUG_VARIABLE_PAGED_ARRAY CheckIntegrity(); #endif return *ItemPtr; } TIterator GetIterator() const { return TIterator(*this, 0); } TIterator GetIteratorFromItem(uint64 ItemIndex) const { return TIterator(*this, ItemIndex); } ItemType& operator[](uint64 Index) { PageType* Page; uint64 ItemIndexInPage; FindItemChecked(Index, Page, ItemIndexInPage); ItemType* Item = Page->Items + Page->ItemOffset + ItemIndexInPage; return *Item; } const ItemType& operator[](uint64 Index) const { return const_cast(*this)[Index]; } ItemType& First() { ItemType* Item = FirstPage->Items + FirstPage->ItemOffset; return *Item; } const ItemType& First() const { const ItemType* Item = FirstPage->Items + FirstPage->ItemOffset; return *Item; } ItemType& Last() { ItemType* Item = LastPage->Items + LastPage->ItemOffset + LastPage->ItemCount - 1; return *Item; } const ItemType& Last() const { const ItemType* Item = LastPage->Items + LastPage->ItemOffset + LastPage->ItemCount - 1; return *Item; } private: void FindItemChecked(const uint64 Index, const PageType*& OutPage, uint64& OutItemIndexInPage) const { VARIABLE_PAGED_ARRAY_CHECK(Index < TotalItemCount); // Find the page group containing the index. uint64 PageGroupIndex = Algo::UpperBoundBy(PageGroups, Index, &PageGroupType::FirstItemIndex); VARIABLE_PAGED_ARRAY_CHECK(PageGroupIndex > 0 && PageGroupIndex <= PageGroups.Num()); --PageGroupIndex; const PageGroupType* PageGroup = PageGroups.GetData() + PageGroupIndex; VARIABLE_PAGED_ARRAY_CHECK(Index >= PageGroup->FirstItemIndex); VARIABLE_PAGED_ARRAY_CHECK(Index < PageGroup->FirstItemIndex + PageGroup->ItemCount); // Find the page containing the index. uint64 ItemIndex = Index - PageGroup->FirstItemIndex; const PageType* Page = Pages.GetData() + PageGroup->FirstPageIndex; // The first page (and the last page) in a group may have less items than PageSize. if (ItemIndex >= Page->ItemCount) { // Skip the first page in group. ItemIndex -= Page->ItemCount; ++Page; } Page += ItemIndex / PageSize; ItemIndex = ItemIndex % PageSize; VARIABLE_PAGED_ARRAY_CHECK(ItemIndex < Page->ItemCount); OutPage = Page; OutItemIndexInPage = ItemIndex; } void CreateInitialPage() { // Create the initial page. VARIABLE_PAGED_ARRAY_CHECK(Pages.Num() == 0); PageType* Page = &Pages.AddDefaulted_GetRef(); Page->Items = reinterpret_cast(Allocator.Allocate(PageSize * sizeof(ItemType))); FirstPage = Page; LastPage = Page; // Create the initial page group. VARIABLE_PAGED_ARRAY_CHECK(PageGroups.Num() == 0); PageGroupType* PageGroup = &PageGroups.AddDefaulted_GetRef(); PageGroup->PageCount = 1; FirstPageGroup = PageGroup; LastPageGroup = PageGroup; } ItemType& PushBackItemInPageGroup(const uint64 PageGroupIndex) { PageGroupType& PageGroup = PageGroups[static_cast(PageGroupIndex)]; uint64 PageIndex = PageGroup.FirstPageIndex + PageGroup.PageCount - 1; PageType* Page = &Pages[static_cast(PageIndex)]; if (Page->ItemOffset + Page->ItemCount == PageSize) // if page is full { // Add a new page. ++PageIndex; Page = &Pages.InsertDefaulted_GetRef(static_cast(PageIndex)); Page->Items = reinterpret_cast(Allocator.Allocate(PageSize * sizeof(ItemType))); OnInsertedPage(PageGroupIndex); } // Push back a new item. ItemType* ItemPtr = Page->Items + Page->ItemOffset + Page->ItemCount; new (ItemPtr) ItemType(); ++Page->ItemCount; OnInsertedItem(PageGroupIndex); return *ItemPtr; } void OnInsertedPage(const uint64 PageGroupIndex) { FirstPage = Pages.GetData(); LastPage = Pages.GetData() + Pages.Num() - 1; PageGroupType* const PageGroup = PageGroups.GetData() + PageGroupIndex; ++PageGroup->PageCount; // Update the following page groups. PageGroupType* const StartPageGroup = PageGroup + 1; PageGroupType* const EndPageGroup = PageGroups.GetData() + PageGroups.Num(); for (PageGroupType* CurrentPageGroup = StartPageGroup; CurrentPageGroup != EndPageGroup; ++CurrentPageGroup) { ++CurrentPageGroup->FirstPageIndex; } } void OnInsertedItem(const uint64 PageGroupIndex) { PageGroupType* const PageGroup = PageGroups.GetData() + PageGroupIndex; ++PageGroup->ItemCount; ++TotalItemCount; // Update the following page groups. PageGroupType* const StartPageGroup = PageGroup + 1; PageGroupType* const EndPageGroup = PageGroups.GetData() + PageGroups.Num(); for (PageGroupType* CurrentPageGroup = StartPageGroup; CurrentPageGroup != EndPageGroup; ++CurrentPageGroup) { ++CurrentPageGroup->FirstItemIndex; } } void CheckIntegrity() const { check(PageSize > 0); if (TotalItemCount == 0) { check(Pages.Num() == 0); check(FirstPage == nullptr); check(LastPage == nullptr); check(PageGroups.Num() == 0); check(FirstPageGroup == nullptr); check(LastPageGroup == nullptr); } else { check(Pages.Num() > 0); check(FirstPage == Pages.GetData()); check(LastPage == Pages.GetData() + Pages.Num() - 1); check(PageGroups.Num() > 0); check(FirstPageGroup == PageGroups.GetData()); check(LastPageGroup == PageGroups.GetData() + PageGroups.Num() - 1); } // Verify pages. { uint64 ItemCount = 0; for (const PageType& Page : Pages) { check(Page.Items != nullptr); check(Page.ItemCount > 0); check(Page.ItemOffset + Page.ItemCount <= PageSize); ItemCount += Page.ItemCount; } check(ItemCount == TotalItemCount); } // Verify page groups. { uint64 ItemCount = 0; uint64 PageCount = 0; uint64 ItemIndex = 0; uint64 PageIndex = 0; for (const PageGroupType& PageGroup : PageGroups) { check(PageGroup.ItemCount > 0); ItemCount += PageGroup.ItemCount; check(PageGroup.FirstItemIndex == ItemIndex); ItemIndex += PageGroup.ItemCount; check(PageGroup.PageCount > 0); PageCount += PageGroup.PageCount; check(PageGroup.FirstPageIndex == PageIndex); PageIndex += PageGroup.PageCount; } check(ItemCount == TotalItemCount); check(PageCount == Pages.Num()); } } private: template friend class TVariablePagedArrayIterator; ILinearAllocator& Allocator; TArray Pages; PageType* FirstPage; PageType* LastPage; TArray PageGroups; PageGroupType* FirstPageGroup; PageGroupType* LastPageGroup; uint64 PageSize; uint64 TotalItemCount; }; //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// } // namespace TraceServices