Skip to content

Commit

Permalink
Optimize HandleAllocator for fast ID churning.
Browse files Browse the repository at this point in the history
Instead of calculating ranges of IDs and the overhead with updating
them every allocation/release, store a released ID list in a small
FastVector.

Optimize the allocate path for the "good case" of no reserved IDs so
that it either pops the last released ID or incriments a next value and
returns it. Release has a similar cost of just a push_back when there
are no reserved IDs.

This adds a small fixed memory cost due to the FastVector and a dynamic
memory cost of mReleasedList having up to N elements where N is the
maxmimum total handles allocated at one time.

Bug: angleproject:8434
Change-Id: I7c5aa126b5303c105cd2464d0d0933b922cc2b8f
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/5101509
Reviewed-by: Charlie Lao <[email protected]>
Commit-Queue: Shahbaz Youssefi <[email protected]>
  • Loading branch information
vonture authored and Angle LUCI CQ committed Dec 13, 2023
1 parent e172c10 commit b25ffe5
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 142 deletions.
33 changes: 31 additions & 2 deletions src/common/FastVector.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,15 @@ class FastVector final

void reserve(size_type count);

// Specialty function that removes a known element and might shuffle the list.
// Remove the first instance of "element", which is **required** to exist in the vector. May
// change the ordering of the data.
void remove_and_permute(const value_type &element);
void remove_and_permute(iterator pos);

// Remove all data that satisifies predicate P. May change the ordering of the data.
template <typename Pred>
void remove_all_and_permute(Pred p);

private:
void assign_from_initializer_list(std::initializer_list<value_type> init);
void ensure_capacity(size_t capacity);
Expand Down Expand Up @@ -533,15 +538,20 @@ void FastVector<T, N, Storage>::assign_from_initializer_list(std::initializer_li
template <class T, size_t N, class Storage>
ANGLE_INLINE void FastVector<T, N, Storage>::remove_and_permute(const value_type &element)
{
ASSERT(mSize > 0);
size_t len = mSize - 1;
for (size_t index = 0; index < len; ++index)
{
if (mData[index] == element)
{
mData[index] = std::move(mData[len]);
break;
pop_back();
return;
}
}
// Note: the element is required to exist in the vector. So if it's not found in the [0, N-1)
// elements, it must be the last element.
ASSERT(mData[mSize - 1] == element);
pop_back();
}

Expand All @@ -555,6 +565,25 @@ ANGLE_INLINE void FastVector<T, N, Storage>::remove_and_permute(iterator pos)
pop_back();
}

template <class T, size_t N, class Storage>
template <typename Pred>
ANGLE_INLINE void FastVector<T, N, Storage>::remove_all_and_permute(Pred p)
{
size_t i = 0;
while (i < mSize)
{
if (p(mData[i]))
{
mData[i] = std::move(mData[mSize - 1]);
pop_back();
}
else
{
i++;
}
}
}

template <class T, size_t N, class Storage>
void FastVector<T, N, Storage>::ensure_capacity(size_t capacity)
{
Expand Down
89 changes: 89 additions & 0 deletions src/common/FastVector_unittest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,94 @@ TEST(FastVector, NonCopyable)
EXPECT_EQ(3, copy[0].x);
}

// Tests of the remove_and_permute and remove_all_and_permute methods.
TEST(FastVector, RemoveAndPermute)
{
FastVector<int, 4> vec;

// remove_and_permute only removes one element
vec.push_back(0); // vec = { 0 }
vec.push_back(0); // vec = { 0, 0 }
EXPECT_EQ(2u, vec.size());
vec.remove_and_permute(0); // vec = { 0 }
EXPECT_EQ(1u, vec.size());
vec.remove_and_permute(0); // vec = { }
EXPECT_EQ(0u, vec.size());

// remove_and_permute removes the correct element
vec.push_back(10);
vec.push_back(15);
vec.push_back(7);
vec.push_back(999);
vec.push_back(-20);
vec.push_back(0);
vec.push_back(123);
EXPECT_EQ(7u, vec.size());
EXPECT_EQ(vec[0], 10);
EXPECT_EQ(vec[1], 15);
EXPECT_EQ(vec[2], 7);
EXPECT_EQ(vec[3], 999);
EXPECT_EQ(vec[4], -20);
EXPECT_EQ(vec[5], 0);
EXPECT_EQ(vec[6], 123);

vec.remove_and_permute(7);
EXPECT_EQ(6u, vec.size());
vec.remove_and_permute(-20);
EXPECT_EQ(5u, vec.size());
vec.remove_and_permute(10);
EXPECT_EQ(4u, vec.size());

EXPECT_NE(std::find(vec.begin(), vec.end(), 15), vec.end());
EXPECT_NE(std::find(vec.begin(), vec.end(), 999), vec.end());
EXPECT_NE(std::find(vec.begin(), vec.end(), 0), vec.end());
EXPECT_NE(std::find(vec.begin(), vec.end(), 123), vec.end());

EXPECT_EQ(std::find(vec.begin(), vec.end(), 7), vec.end());
EXPECT_EQ(std::find(vec.begin(), vec.end(), -20), vec.end());
EXPECT_EQ(std::find(vec.begin(), vec.end(), 10), vec.end());

// Remove an element with an iterator
vec.remove_and_permute(std::find(vec.begin(), vec.end(), 999));

EXPECT_NE(std::find(vec.begin(), vec.end(), 15), vec.end());
EXPECT_NE(std::find(vec.begin(), vec.end(), 0), vec.end());
EXPECT_NE(std::find(vec.begin(), vec.end(), 123), vec.end());

EXPECT_EQ(std::find(vec.begin(), vec.end(), 999), vec.end());

// Remove the last element with an iterator
vec.clear();
vec.push_back(100);
vec.push_back(-123);
vec.push_back(44);
EXPECT_EQ(3u, vec.size());
EXPECT_EQ(vec[2], 44);

vec.remove_and_permute(vec.begin() + 2);
EXPECT_EQ(2u, vec.size());

EXPECT_NE(std::find(vec.begin(), vec.end(), 100), vec.end());
EXPECT_NE(std::find(vec.begin(), vec.end(), -123), vec.end());

EXPECT_EQ(std::find(vec.begin(), vec.end(), 44), vec.end());

// remove_all_and_permute removes all elements matching
vec.clear();
vec.push_back(0);
vec.push_back(1); // vec = { 0, 1 }
vec.push_back(0); // vec = { 0, 1, 0 }
vec.remove_all_and_permute([](int x) { return x == 0; }); // vec = { 1 }
EXPECT_EQ(1u, vec.size());
EXPECT_EQ(1, vec[0]);

// remove_all_and_permute clears when everything matches
vec.push_back(1); // vec = { 1, 1 }
vec.push_back(0); // vec = { 1, 1, 0 }
vec.remove_all_and_permute([](int) { return true; }); // vec = { }
EXPECT_EQ(0u, vec.size());
}

// Basic functionality for FlatUnorderedMap
TEST(FlatUnorderedMap, BasicUsage)
{
Expand Down Expand Up @@ -388,4 +476,5 @@ TEST(FastMap, Basic)
EXPECT_TRUE(testMap[i] == i);
}
}

} // namespace angle
143 changes: 30 additions & 113 deletions src/libANGLE/HandleAllocator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,176 +18,93 @@
namespace gl
{

struct HandleAllocator::HandleRangeComparator
{
bool operator()(const HandleRange &range, GLuint handle) const { return (range.end < handle); }
};

HandleAllocator::HandleAllocator()
: mBaseValue(1),
mNextValue(1),
mMaxValue(std::numeric_limits<GLuint>::max()),
mLoggingEnabled(false)
{
mUnallocatedList.push_back(HandleRange(1, mMaxValue));
}
static constexpr bool kEnableHandleAllocatorLogging = false;

HandleAllocator::HandleAllocator() : HandleAllocator(std::numeric_limits<GLuint>::max()) {}

HandleAllocator::HandleAllocator(GLuint maximumHandleValue)
: mBaseValue(1), mNextValue(1), mMaxValue(maximumHandleValue), mLoggingEnabled(false)
{
mUnallocatedList.push_back(HandleRange(1, mMaxValue));
}
: mNextValue(1), mMaxValue(maximumHandleValue)
{}

HandleAllocator::~HandleAllocator() {}

void HandleAllocator::setBaseHandle(GLuint value)
{
ASSERT(mBaseValue == mNextValue);
mBaseValue = value;
mNextValue = value;
mNextValue = std::max(mNextValue, value);
mReleasedList.remove_all_and_permute([value](GLuint x) { return x < value; });
}

GLuint HandleAllocator::allocate()
{
ASSERT(!mUnallocatedList.empty() || !mReleasedList.empty());

// Allocate from released list, logarithmic time for pop_heap.
// Allocate from released list if possible
if (!mReleasedList.empty())
{
std::pop_heap(mReleasedList.begin(), mReleasedList.end(), std::greater<GLuint>());
GLuint reusedHandle = mReleasedList.back();
mReleasedList.pop_back();

if (mLoggingEnabled)
if (kEnableHandleAllocatorLogging)
{
WARN() << "HandleAllocator::allocate reusing " << reusedHandle << std::endl;
}

return reusedHandle;
}

// Allocate from unallocated list, constant time.
auto listIt = mUnallocatedList.begin();

GLuint freeListHandle = listIt->begin;
ASSERT(freeListHandle > 0);

if (listIt->begin == listIt->end)
{
mUnallocatedList.erase(listIt);
}
else
// The next value may be reserved. Iterate until an unreserved value is found
if (!mReservedList.empty())
{
listIt->begin++;
while (std::find(mReservedList.begin(), mReservedList.end(), mNextValue) !=
mReservedList.end())
{
mNextValue++;
}
}

if (mLoggingEnabled)
GLuint nextHandle = mNextValue++;

if (kEnableHandleAllocatorLogging)
{
WARN() << "HandleAllocator::allocate allocating " << freeListHandle << std::endl;
WARN() << "HandleAllocator::allocate allocating " << nextHandle << std::endl;
}

return freeListHandle;
return nextHandle;
}

void HandleAllocator::release(GLuint handle)
{
if (mLoggingEnabled)
if (kEnableHandleAllocatorLogging)
{
WARN() << "HandleAllocator::release releasing " << handle << std::endl;
}

// Try consolidating the ranges first.
for (HandleRange &handleRange : mUnallocatedList)
mReleasedList.remove_all_and_permute([handle](GLuint x) { return x == handle; });
if (handle <= mNextValue)
{
if (handleRange.begin - 1 == handle)
{
handleRange.begin--;
return;
}

if (handleRange.end == handle - 1)
{
handleRange.end++;
return;
}
mReleasedList.push_back(handle);
}

// Add to released list, logarithmic time for push_heap.
mReleasedList.push_back(handle);
std::push_heap(mReleasedList.begin(), mReleasedList.end(), std::greater<GLuint>());
}

void HandleAllocator::reserve(GLuint handle)
{
if (mLoggingEnabled)
if (kEnableHandleAllocatorLogging)
{
WARN() << "HandleAllocator::reserve reserving " << handle << std::endl;
}

// Clear from released list -- might be a slow operation.
if (!mReleasedList.empty())
{
auto releasedIt = std::find(mReleasedList.begin(), mReleasedList.end(), handle);
if (releasedIt != mReleasedList.end())
{
mReleasedList.erase(releasedIt);
std::make_heap(mReleasedList.begin(), mReleasedList.end(), std::greater<GLuint>());
return;
}
}

// Not in released list, reserve in the unallocated list.
auto boundIt = std::lower_bound(mUnallocatedList.begin(), mUnallocatedList.end(), handle,
HandleRangeComparator());

ASSERT(boundIt != mUnallocatedList.end());

GLuint begin = boundIt->begin;
GLuint end = boundIt->end;

if (handle == begin || handle == end)
{
if (begin == end)
{
mUnallocatedList.erase(boundIt);
}
else if (handle == begin)
{
boundIt->begin++;
}
else
{
ASSERT(handle == end);
boundIt->end--;
}
return;
}

ASSERT(begin < handle && handle < end);

// need to split the range
auto placementIt = mUnallocatedList.erase(boundIt);
placementIt = mUnallocatedList.insert(placementIt, HandleRange(handle + 1, end));
mUnallocatedList.insert(placementIt, HandleRange(begin, handle - 1));
mReservedList.push_back(handle);
mReleasedList.remove_all_and_permute([handle](GLuint x) { return x == handle; });
}

void HandleAllocator::reset()
{
mUnallocatedList.clear();
mUnallocatedList.push_back(HandleRange(1, mMaxValue));
mReservedList.clear();
mReleasedList.clear();
mBaseValue = 1;
mNextValue = 1;
}

bool HandleAllocator::anyHandleAvailableForAllocation() const
{
return !mUnallocatedList.empty() || !mReleasedList.empty();
}

void HandleAllocator::enableLogging(bool enabled)
{
mLoggingEnabled = enabled;
return !mReleasedList.empty() || mNextValue < mMaxValue;
}

} // namespace gl
Loading

0 comments on commit b25ffe5

Please sign in to comment.