mirror of
https://github.com/luau-lang/luau.git
synced 2024-11-15 14:25:44 +08:00
477 lines
15 KiB
C++
477 lines
15 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
#pragma once
|
|
|
|
#include "Luau/Common.h"
|
|
|
|
#include <algorithm>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <new>
|
|
#include <stdexcept>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
|
|
namespace Luau
|
|
{
|
|
// `VecDeque` is a general double-ended implementation designed as a drop-in replacement for the
|
|
// standard library `std::deque`. It's backed by a growable ring buffer, rather than using the
|
|
// segmented queue design of `std::deque` which can degrade into a linked list in the worst case.
|
|
// The motivation for `VecDeque` as a replacement is to maintain the asymptotic complexity of
|
|
// `std::deque` while reducing overall allocations and promoting better usage of the cache. Its API
|
|
// is intended to be compatible with `std::deque` and `std::vector` as appropriate, and as such
|
|
// provides corresponding method definitions and supports the use of custom allocators.
|
|
//
|
|
// `VecDeque` offers pushing and popping from both ends with an amortized O(1) complexity. It also
|
|
// supports `std::vector`-style random-access in O(1). The implementation of buffer resizing uses
|
|
// a growth factor of 1.5x to enable better memory reuse after resizing, and reduce overall memory
|
|
// fragmentation when using the queue.
|
|
//
|
|
// Since `VecDeque` is a ring buffer, its elements are not necessarily contiguous in memory. To
|
|
// describe this, we refer to the two portions of the buffer as the `head` and the `tail`. The
|
|
// `head` is the initial portion of the queue that is on the range `[head, capacity)` and the tail
|
|
// is the (optionally) remaining portion on the range `[0, head + size - capacity)` whenever the
|
|
// `head + size` exceeds the capacity of the buffer.
|
|
//
|
|
// `VecDeque` does not currently support iteration since its primary focus is on providing
|
|
// double-ended queue functionality specifically, but it can be reasonably expanded to provide
|
|
// an iterator if we have a use-case for one in the future.
|
|
template<typename T, class Allocator = std::allocator<T>>
|
|
class VecDeque : Allocator
|
|
{
|
|
private:
|
|
static_assert(std::is_nothrow_move_constructible_v<T>);
|
|
static_assert(std::is_nothrow_move_assignable_v<T>);
|
|
|
|
T* buffer = nullptr; // the existing allocation we have backing this queue
|
|
size_t buffer_capacity = 0; // the size of our allocation
|
|
|
|
size_t head = 0; // the index of the head of the queue
|
|
size_t queue_size = 0; // the size of the queue
|
|
|
|
void destroyElements() noexcept
|
|
{
|
|
size_t head_size =
|
|
std::min(queue_size, capacity() - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
|
|
size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
|
|
|
|
// we have to destroy every element in the head portion
|
|
for (size_t index = head; index < head + head_size; index++)
|
|
buffer[index].~T();
|
|
|
|
// and any in the tail portion, if one exists
|
|
for (size_t index = 0; index < tail_size; index++)
|
|
buffer[index].~T();
|
|
}
|
|
|
|
bool is_full()
|
|
{
|
|
return queue_size == capacity();
|
|
}
|
|
|
|
void grow()
|
|
{
|
|
size_t old_capacity = capacity();
|
|
|
|
// we use a growth factor of 1.5x (plus a constant) here in order to enable the
|
|
// previous memory to be reused after a certain number of calls to grow.
|
|
// see: https://github.com/facebook/folly/blob/main/folly/docs/FBVector.md#memory-handling
|
|
size_t new_capacity = (old_capacity > 0) ? old_capacity * 3 / 2 + 1 : 4;
|
|
|
|
// check that it's a legal allocation
|
|
if (new_capacity > max_size())
|
|
throw std::bad_array_new_length();
|
|
|
|
// allocate a new backing buffer
|
|
T* new_buffer = this->allocate(new_capacity);
|
|
|
|
// we should not be growing if the capacity is not the current size
|
|
LUAU_ASSERT(old_capacity == queue_size);
|
|
|
|
size_t head_size =
|
|
std::min(queue_size, old_capacity - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
|
|
size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
|
|
|
|
// move the head into the new buffer
|
|
if (head_size != 0)
|
|
std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer);
|
|
|
|
// move the tail into the new buffer immediately after
|
|
if (tail_size != 0)
|
|
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
|
|
|
|
// destroy the old elements
|
|
destroyElements();
|
|
// deallocate the old buffer
|
|
this->deallocate(buffer, old_capacity);
|
|
|
|
// set up the queue to be backed by the new buffer
|
|
buffer = new_buffer;
|
|
buffer_capacity = new_capacity;
|
|
head = 0;
|
|
}
|
|
|
|
size_t logicalToPhysical(size_t pos)
|
|
{
|
|
return (head + pos) % capacity();
|
|
}
|
|
|
|
public:
|
|
VecDeque() = default;
|
|
|
|
explicit VecDeque(const Allocator& alloc) noexcept
|
|
: Allocator{alloc}
|
|
{
|
|
}
|
|
|
|
VecDeque(const VecDeque& other)
|
|
: buffer(this->allocate(other.buffer_capacity))
|
|
, buffer_capacity(other.buffer_capacity)
|
|
, head(other.head)
|
|
, queue_size(other.queue_size)
|
|
{
|
|
// copy the initialized contents of the other buffer to this one
|
|
size_t head_size = std::min(
|
|
other.queue_size,
|
|
other.buffer_capacity - other.head
|
|
); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
|
|
size_t tail_size = other.queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
|
|
|
|
if (head_size != 0)
|
|
std::uninitialized_copy(other.buffer + other.head, other.buffer + other.head + head_size, buffer + head);
|
|
|
|
if (tail_size != 0)
|
|
std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer);
|
|
}
|
|
|
|
VecDeque(const VecDeque& other, const Allocator& alloc)
|
|
: Allocator{alloc}
|
|
, buffer(this->allocate(other.buffer_capacity))
|
|
, buffer_capacity(other.buffer_capacity)
|
|
, head(other.head)
|
|
, queue_size(other.queue_size)
|
|
{
|
|
// copy the initialized contents of the other buffer to this one
|
|
size_t head_size = std::min(
|
|
other.queue_size,
|
|
other.buffer_capacity - other.head
|
|
); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
|
|
size_t tail_size = other.queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
|
|
|
|
if (head_size != 0)
|
|
std::uninitialized_copy(other.buffer + other.head, other.buffer + other.head + head_size, buffer + head);
|
|
|
|
if (tail_size != 0)
|
|
std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer);
|
|
}
|
|
|
|
VecDeque(VecDeque&& other) noexcept
|
|
: buffer(std::exchange(other.buffer, nullptr))
|
|
, buffer_capacity(std::exchange(other.buffer_capacity, 0))
|
|
, head(std::exchange(other.head, 0))
|
|
, queue_size(std::exchange(other.queue_size, 0))
|
|
{
|
|
}
|
|
|
|
VecDeque(VecDeque&& other, const Allocator& alloc) noexcept
|
|
: Allocator{alloc}
|
|
, buffer(std::exchange(other.buffer, nullptr))
|
|
, buffer_capacity(std::exchange(other.buffer_capacity, 0))
|
|
, head(std::exchange(other.head, 0))
|
|
, queue_size(std::exchange(other.queue_size, 0))
|
|
{
|
|
}
|
|
|
|
VecDeque(std::initializer_list<T> init, const Allocator& alloc = Allocator())
|
|
: Allocator{alloc}
|
|
{
|
|
buffer = this->allocate(init.size());
|
|
buffer_capacity = init.size();
|
|
queue_size = init.size();
|
|
|
|
std::uninitialized_copy(init.begin(), init.end(), buffer);
|
|
}
|
|
|
|
~VecDeque() noexcept
|
|
{
|
|
// destroy any elements that exist
|
|
destroyElements();
|
|
// free the allocated buffer
|
|
this->deallocate(buffer, buffer_capacity);
|
|
}
|
|
|
|
VecDeque& operator=(const VecDeque& other)
|
|
{
|
|
if (this == &other)
|
|
return *this;
|
|
|
|
// destroy all of the existing elements
|
|
destroyElements();
|
|
|
|
if (buffer_capacity < other.size())
|
|
{
|
|
// free the current buffer
|
|
this->deallocate(buffer, buffer_capacity);
|
|
|
|
buffer = this->allocate(other.buffer_capacity);
|
|
buffer_capacity = other.buffer_capacity;
|
|
}
|
|
|
|
size_t head_size = std::min(
|
|
other.queue_size,
|
|
other.buffer_capacity - other.head
|
|
); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
|
|
size_t tail_size = other.queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
|
|
|
|
// Assignment doesn't try to match the capacity of 'other' and thus makes the buffer contiguous
|
|
head = 0;
|
|
queue_size = other.queue_size;
|
|
|
|
if (head_size != 0)
|
|
std::uninitialized_copy(other.buffer + other.head, other.buffer + other.head + head_size, buffer);
|
|
|
|
if (tail_size != 0)
|
|
std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer + head_size);
|
|
|
|
return *this;
|
|
}
|
|
|
|
VecDeque& operator=(VecDeque&& other)
|
|
{
|
|
if (this == &other)
|
|
return *this;
|
|
|
|
// destroy all of the existing elements
|
|
destroyElements();
|
|
// free the current buffer
|
|
this->deallocate(buffer, buffer_capacity);
|
|
|
|
buffer = std::exchange(other.buffer, nullptr);
|
|
buffer_capacity = std::exchange(other.buffer_capacity, 0);
|
|
head = std::exchange(other.head, 0);
|
|
queue_size = std::exchange(other.queue_size, 0);
|
|
|
|
return *this;
|
|
}
|
|
|
|
Allocator get_allocator() const noexcept
|
|
{
|
|
return this;
|
|
}
|
|
|
|
// element access
|
|
|
|
T& at(size_t pos)
|
|
{
|
|
if (pos >= queue_size)
|
|
throw std::out_of_range("VecDeque");
|
|
|
|
return buffer[logicalToPhysical(pos)];
|
|
}
|
|
|
|
const T& at(size_t pos) const
|
|
{
|
|
if (pos >= queue_size)
|
|
throw std::out_of_range("VecDeque");
|
|
|
|
return buffer[logicalToPhysical(pos)];
|
|
}
|
|
|
|
[[nodiscard]] T& operator[](size_t pos) noexcept
|
|
{
|
|
LUAU_ASSERT(pos < queue_size);
|
|
|
|
return buffer[logicalToPhysical(pos)];
|
|
}
|
|
|
|
[[nodiscard]] const T& operator[](size_t pos) const noexcept
|
|
{
|
|
LUAU_ASSERT(pos < queue_size);
|
|
|
|
return buffer[logicalToPhysical(pos)];
|
|
}
|
|
|
|
T& front()
|
|
{
|
|
LUAU_ASSERT(!empty());
|
|
|
|
return buffer[head];
|
|
}
|
|
|
|
const T& front() const
|
|
{
|
|
LUAU_ASSERT(!empty());
|
|
|
|
return buffer[head];
|
|
}
|
|
|
|
T& back()
|
|
{
|
|
LUAU_ASSERT(!empty());
|
|
|
|
size_t back = logicalToPhysical(queue_size - 1);
|
|
return buffer[back];
|
|
}
|
|
|
|
const T& back() const
|
|
{
|
|
LUAU_ASSERT(!empty());
|
|
|
|
size_t back = logicalToPhysical(queue_size - 1);
|
|
return buffer[back];
|
|
}
|
|
|
|
// capacity
|
|
|
|
bool empty() const noexcept
|
|
{
|
|
return queue_size == 0;
|
|
}
|
|
|
|
size_t size() const noexcept
|
|
{
|
|
return queue_size;
|
|
}
|
|
|
|
size_t max_size() const noexcept
|
|
{
|
|
return std::numeric_limits<size_t>::max() / sizeof(T);
|
|
}
|
|
|
|
void reserve(size_t new_capacity)
|
|
{
|
|
// error if this allocation would be illegal
|
|
if (new_capacity > max_size())
|
|
throw std::length_error("too large");
|
|
|
|
size_t old_capacity = capacity();
|
|
|
|
// do nothing if we're requesting a capacity that would not cause growth
|
|
if (new_capacity <= old_capacity)
|
|
return;
|
|
|
|
size_t head_size =
|
|
std::min(queue_size, old_capacity - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
|
|
size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
|
|
|
|
// allocate a new backing buffer
|
|
T* new_buffer = this->allocate(new_capacity);
|
|
|
|
// move the head into the new buffer
|
|
if (head_size != 0)
|
|
std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer);
|
|
|
|
// move the tail into the new buffer immediately after
|
|
if (tail_size != 0)
|
|
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
|
|
|
|
// destroy all the existing elements before freeing the old buffer
|
|
destroyElements();
|
|
|
|
// deallocate the old buffer
|
|
this->deallocate(buffer, old_capacity);
|
|
|
|
// set up the queue to be backed by the new buffer
|
|
buffer = new_buffer;
|
|
buffer_capacity = new_capacity;
|
|
head = 0;
|
|
}
|
|
|
|
size_t capacity() const noexcept
|
|
{
|
|
return buffer_capacity;
|
|
}
|
|
|
|
void shrink_to_fit()
|
|
{
|
|
size_t old_capacity = capacity();
|
|
size_t new_capacity = queue_size;
|
|
|
|
if (old_capacity == new_capacity)
|
|
return;
|
|
|
|
size_t head_size =
|
|
std::min(queue_size, old_capacity - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
|
|
size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
|
|
|
|
// allocate a new backing buffer
|
|
T* new_buffer = this->allocate(new_capacity);
|
|
|
|
// move the head into the new buffer
|
|
if (head_size != 0)
|
|
std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer);
|
|
|
|
// move the tail into the new buffer immediately after, if we have one
|
|
if (tail_size != 0)
|
|
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
|
|
|
|
// destroy all the existing elements before freeing the old buffer
|
|
destroyElements();
|
|
// deallocate the old buffer
|
|
this->deallocate(buffer, old_capacity);
|
|
|
|
// set up the queue to be backed by the new buffer
|
|
buffer = new_buffer;
|
|
buffer_capacity = new_capacity;
|
|
head = 0;
|
|
}
|
|
|
|
[[nodiscard]] bool is_contiguous() const noexcept
|
|
{
|
|
// this is an overflow-safe alternative to writing `head + size <= capacity`.
|
|
return head <= capacity() - queue_size;
|
|
}
|
|
|
|
// modifiers
|
|
|
|
void clear() noexcept
|
|
{
|
|
destroyElements();
|
|
|
|
head = 0;
|
|
queue_size = 0;
|
|
}
|
|
|
|
void push_back(const T& value)
|
|
{
|
|
if (is_full())
|
|
grow();
|
|
|
|
size_t next_back = logicalToPhysical(queue_size);
|
|
new (buffer + next_back) T(value);
|
|
queue_size++;
|
|
}
|
|
|
|
void pop_back()
|
|
{
|
|
LUAU_ASSERT(!empty());
|
|
|
|
queue_size--;
|
|
size_t next_back = logicalToPhysical(queue_size);
|
|
buffer[next_back].~T();
|
|
}
|
|
|
|
void push_front(const T& value)
|
|
{
|
|
if (is_full())
|
|
grow();
|
|
|
|
head = (head == 0) ? capacity() - 1 : head - 1;
|
|
new (buffer + head) T(value);
|
|
queue_size++;
|
|
}
|
|
|
|
void pop_front()
|
|
{
|
|
LUAU_ASSERT(!empty());
|
|
|
|
buffer[head].~T();
|
|
head++;
|
|
queue_size--;
|
|
|
|
if (head == capacity())
|
|
head = 0;
|
|
}
|
|
};
|
|
|
|
} // namespace Luau
|