Add Chromium's StackVector base class and ityp::stack_vec

In order to lift Dawn's restriction on the number of bindings
per bind group, static sized arays need to be converted to
dynamically sized vectors. This CL adds Chromium's StackVector
class which behaves like std::vector but provides a stack allocator
to allocate small vectors on the stack. Dawn can use this to avoid
making separate heap allocations for a smaller, realistic binding
counts.

The CL also adds an ityp::stack_vec class to support using
a StackVector with TypedInteger indices.

Bug: dawn:442, dawn:443
Change-Id: I7604c02b3ea52cd63990a2e8b45ed238a5d52232
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/23681
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Bryan Bernhart <bryan.bernhart@intel.com>
Reviewed-by: Austin Eng <enga@chromium.org>
Commit-Queue: Austin Eng <enga@chromium.org>
This commit is contained in:
Austin Eng 2020-07-08 20:27:30 +00:00 committed by Commit Bot service account
parent 8a9919980f
commit d761d5a575
7 changed files with 548 additions and 0 deletions

View File

@ -178,6 +178,7 @@ if (is_win || is_linux || is_mac || is_fuchsia || is_android) {
"ityp_array.h",
"ityp_bitset.h",
"ityp_span.h",
"ityp_stack_vec.h",
"vulkan_platform.h",
"windows_with_undefs.h",
"xlib_with_undefs.h",

View File

@ -49,6 +49,7 @@ target_sources(dawn_common PRIVATE
"ityp_array.h"
"ityp_bitset.h"
"ityp_span.h"
"ityp_stack_vec.h"
"vulkan_platform.h"
"windows_with_undefs.h"
"xlib_with_undefs.h"

View File

@ -122,6 +122,14 @@ class Ref {
mPointee = nullptr;
}
bool operator==(const T* other) const {
return mPointee == other;
}
bool operator!=(const T* other) const {
return mPointee != other;
}
operator bool() {
return mPointee != nullptr;
}

264
src/common/StackContainer.h Normal file
View File

@ -0,0 +1,264 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file is a modified copy of Chromium's /src/base/containers/stack_container.h
#ifndef COMMON_STACKCONTAINER_H_
#define COMMON_STACKCONTAINER_H_
#include "common/Compiler.h"
#include <cstddef>
#include <vector>
// This allocator can be used with STL containers to provide a stack buffer
// from which to allocate memory and overflows onto the heap. This stack buffer
// would be allocated on the stack and allows us to avoid heap operations in
// some situations.
//
// STL likes to make copies of allocators, so the allocator itself can't hold
// the data. Instead, we make the creator responsible for creating a
// StackAllocator::Source which contains the data. Copying the allocator
// merely copies the pointer to this shared source, so all allocators created
// based on our allocator will share the same stack buffer.
//
// This stack buffer implementation is very simple. The first allocation that
// fits in the stack buffer will use the stack buffer. Any subsequent
// allocations will not use the stack buffer, even if there is unused room.
// This makes it appropriate for array-like containers, but the caller should
// be sure to reserve() in the container up to the stack buffer size. Otherwise
// the container will allocate a small array which will "use up" the stack
// buffer.
template <typename T, size_t stack_capacity>
class StackAllocator : public std::allocator<T> {
public:
typedef typename std::allocator<T>::pointer pointer;
typedef typename std::allocator<T>::size_type size_type;
// Backing store for the allocator. The container owner is responsible for
// maintaining this for as long as any containers using this allocator are
// live.
struct Source {
Source() : used_stack_buffer_(false) {
}
// Casts the buffer in its right type.
T* stack_buffer() {
return reinterpret_cast<T*>(stack_buffer_);
}
const T* stack_buffer() const {
return reinterpret_cast<const T*>(&stack_buffer_);
}
// The buffer itself. It is not of type T because we don't want the
// constructors and destructors to be automatically called. Define a POD
// buffer of the right size instead.
alignas(T) char stack_buffer_[sizeof(T[stack_capacity])];
#if defined(DAWN_COMPILER_GCC) && !defined(__x86_64__) && !defined(__i386__)
static_assert(alignof(T) <= 16, "http://crbug.com/115612");
#endif
// Set when the stack buffer is used for an allocation. We do not track
// how much of the buffer is used, only that somebody is using it.
bool used_stack_buffer_;
};
// Used by containers when they want to refer to an allocator of type U.
template <typename U>
struct rebind {
typedef StackAllocator<U, stack_capacity> other;
};
// For the straight up copy c-tor, we can share storage.
StackAllocator(const StackAllocator<T, stack_capacity>& rhs)
: std::allocator<T>(), source_(rhs.source_) {
}
// ISO C++ requires the following constructor to be defined,
// and std::vector in VC++2008SP1 Release fails with an error
// in the class _Container_base_aux_alloc_real (from <xutility>)
// if the constructor does not exist.
// For this constructor, we cannot share storage; there's
// no guarantee that the Source buffer of Ts is large enough
// for Us.
// TODO: If we were fancy pants, perhaps we could share storage
// iff sizeof(T) == sizeof(U).
template <typename U, size_t other_capacity>
StackAllocator(const StackAllocator<U, other_capacity>& other) : source_(nullptr) {
}
// This constructor must exist. It creates a default allocator that doesn't
// actually have a stack buffer. glibc's std::string() will compare the
// current allocator against the default-constructed allocator, so this
// should be fast.
StackAllocator() : source_(nullptr) {
}
explicit StackAllocator(Source* source) : source_(source) {
}
// Actually do the allocation. Use the stack buffer if nobody has used it yet
// and the size requested fits. Otherwise, fall through to the standard
// allocator.
pointer allocate(size_type n) {
if (source_ && !source_->used_stack_buffer_ && n <= stack_capacity) {
source_->used_stack_buffer_ = true;
return source_->stack_buffer();
} else {
return std::allocator<T>::allocate(n);
}
}
// Free: when trying to free the stack buffer, just mark it as free. For
// non-stack-buffer pointers, just fall though to the standard allocator.
void deallocate(pointer p, size_type n) {
if (source_ && p == source_->stack_buffer())
source_->used_stack_buffer_ = false;
else
std::allocator<T>::deallocate(p, n);
}
private:
Source* source_;
};
// A wrapper around STL containers that maintains a stack-sized buffer that the
// initial capacity of the vector is based on. Growing the container beyond the
// stack capacity will transparently overflow onto the heap. The container must
// support reserve().
//
// This will not work with std::string since some implementations allocate
// more bytes than requested in calls to reserve(), forcing the allocation onto
// the heap. http://crbug.com/709273
//
// WATCH OUT: the ContainerType MUST use the proper StackAllocator for this
// type. This object is really intended to be used only internally. You'll want
// to use the wrappers below for different types.
template <typename TContainerType, size_t stack_capacity>
class StackContainer {
public:
typedef TContainerType ContainerType;
typedef typename ContainerType::value_type ContainedType;
typedef StackAllocator<ContainedType, stack_capacity> Allocator;
// Allocator must be constructed before the container!
StackContainer() : allocator_(&stack_data_), container_(allocator_) {
// Make the container use the stack allocation by reserving our buffer size
// before doing anything else.
container_.reserve(stack_capacity);
}
// Getters for the actual container.
//
// Danger: any copies of this made using the copy constructor must have
// shorter lifetimes than the source. The copy will share the same allocator
// and therefore the same stack buffer as the original. Use std::copy to
// copy into a "real" container for longer-lived objects.
ContainerType& container() {
return container_;
}
const ContainerType& container() const {
return container_;
}
// Support operator-> to get to the container. This allows nicer syntax like:
// StackContainer<...> foo;
// std::sort(foo->begin(), foo->end());
ContainerType* operator->() {
return &container_;
}
const ContainerType* operator->() const {
return &container_;
}
#ifdef UNIT_TEST
// Retrieves the stack source so that that unit tests can verify that the
// buffer is being used properly.
const typename Allocator::Source& stack_data() const {
return stack_data_;
}
#endif
protected:
typename Allocator::Source stack_data_;
Allocator allocator_;
ContainerType container_;
private:
StackContainer(const StackContainer& rhs) = delete;
StackContainer& operator=(const StackContainer& rhs) = delete;
StackContainer(StackContainer&& rhs) = delete;
StackContainer& operator=(StackContainer&& rhs) = delete;
};
// Range-based iteration support for StackContainer.
template <typename TContainerType, size_t stack_capacity>
auto begin(const StackContainer<TContainerType, stack_capacity>& stack_container)
-> decltype(begin(stack_container.container())) {
return begin(stack_container.container());
}
template <typename TContainerType, size_t stack_capacity>
auto begin(StackContainer<TContainerType, stack_capacity>& stack_container)
-> decltype(begin(stack_container.container())) {
return begin(stack_container.container());
}
template <typename TContainerType, size_t stack_capacity>
auto end(StackContainer<TContainerType, stack_capacity>& stack_container)
-> decltype(end(stack_container.container())) {
return end(stack_container.container());
}
template <typename TContainerType, size_t stack_capacity>
auto end(const StackContainer<TContainerType, stack_capacity>& stack_container)
-> decltype(end(stack_container.container())) {
return end(stack_container.container());
}
// StackVector -----------------------------------------------------------------
// Example:
// StackVector<int, 16> foo;
// foo->push_back(22); // we have overloaded operator->
// foo[0] = 10; // as well as operator[]
template <typename T, size_t stack_capacity>
class StackVector
: public StackContainer<std::vector<T, StackAllocator<T, stack_capacity>>, stack_capacity> {
public:
StackVector()
: StackContainer<std::vector<T, StackAllocator<T, stack_capacity>>, stack_capacity>() {
}
// We need to put this in STL containers sometimes, which requires a copy
// constructor. We can't call the regular copy constructor because that will
// take the stack buffer from the original. Here, we create an empty object
// and make a stack buffer of its own.
StackVector(const StackVector<T, stack_capacity>& other)
: StackContainer<std::vector<T, StackAllocator<T, stack_capacity>>, stack_capacity>() {
this->container().assign(other->begin(), other->end());
}
StackVector<T, stack_capacity>& operator=(const StackVector<T, stack_capacity>& other) {
this->container().assign(other->begin(), other->end());
return *this;
}
// Vectors are commonly indexed, which isn't very convenient even with
// operator-> (using "->at()" does exception stuff we don't want).
T& operator[](size_t i) {
return this->container().operator[](i);
}
const T& operator[](size_t i) const {
return this->container().operator[](i);
}
private:
// StackVector(const StackVector& rhs) = delete;
// StackVector& operator=(const StackVector& rhs) = delete;
StackVector(StackVector&& rhs) = delete;
StackVector& operator=(StackVector&& rhs) = delete;
};
#endif // COMMON_STACKCONTAINER_H_

102
src/common/ityp_stack_vec.h Normal file
View File

@ -0,0 +1,102 @@
// Copyright 2020 The Dawn Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef COMMON_ITYP_STACK_VEC_H_
#define COMMON_ITYP_STACK_VEC_H_
#include "common/Assert.h"
#include "common/StackContainer.h"
#include "common/UnderlyingType.h"
namespace ityp {
template <typename Index, typename Value, size_t StaticCapacity = 0>
class stack_vec : private StackVector<Value, StaticCapacity> {
using I = UnderlyingType<Index>;
using Base = StackVector<Value, StaticCapacity>;
static_assert(StaticCapacity <= std::numeric_limits<I>::max(), "");
public:
stack_vec() : Base() {
}
stack_vec(Index size) : Base() {
this->container().resize(static_cast<I>(size));
}
Value& operator[](Index i) {
ASSERT(i < size());
return Base::operator[](static_cast<I>(i));
}
constexpr const Value& operator[](Index i) const {
ASSERT(i < size());
return Base::operator[](static_cast<I>(i));
}
void resize(Index size) {
this->container().resize(static_cast<I>(size));
}
void reserve(Index size) {
this->container().reserve(static_cast<I>(size));
}
Value* data() {
return this->container().data();
}
const Value* data() const {
return this->container().data();
}
Value* begin() noexcept {
return this->container().begin();
}
const Value* begin() const noexcept {
return this->container().begin();
}
Value* end() noexcept {
return this->container().end();
}
const Value* end() const noexcept {
return this->container().end();
}
Value& front() {
return this->container().front();
}
const Value& front() const {
return this->container().front();
}
Value& back() {
return this->container().back();
}
const Value& back() const {
return this->container().back();
}
Index size() const {
return Index(static_cast<I>(this->container().size()));
}
};
} // namespace ityp
#endif // COMMON_ITYP_STACK_VEC_H_

View File

@ -173,6 +173,7 @@ test("dawn_unittests") {
"unittests/SerialMapTests.cpp",
"unittests/SerialQueueTests.cpp",
"unittests/SlabAllocatorTests.cpp",
"unittests/StackContainerTests.cpp",
"unittests/SystemUtilsTests.cpp",
"unittests/ToBackendTests.cpp",
"unittests/TypedIntegerTests.cpp",

View File

@ -0,0 +1,171 @@
// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file is a modified copy of Chromium's /src/base/containers/stack_container_unittest.cc
#include <gtest/gtest.h>
#define UNIT_TEST
#include "common/RefCounted.h"
#include "common/StackContainer.h"
#include <algorithm>
#include <cstddef>
namespace {
class Dummy : public RefCounted {
public:
explicit Dummy(int* alive) : mAlive(alive) {
++*mAlive;
}
private:
~Dummy() {
--*mAlive;
}
int* const mAlive;
};
} // namespace
TEST(StackContainer, Vector) {
const int stack_size = 3;
StackVector<int, stack_size> vect;
const int* stack_buffer = &vect.stack_data().stack_buffer()[0];
// The initial |stack_size| elements should appear in the stack buffer.
EXPECT_EQ(static_cast<size_t>(stack_size), vect.container().capacity());
for (int i = 0; i < stack_size; i++) {
vect.container().push_back(i);
EXPECT_EQ(stack_buffer, &vect.container()[0]);
EXPECT_TRUE(vect.stack_data().used_stack_buffer_);
}
// Adding more elements should push the array onto the heap.
for (int i = 0; i < stack_size; i++) {
vect.container().push_back(i + stack_size);
EXPECT_NE(stack_buffer, &vect.container()[0]);
EXPECT_FALSE(vect.stack_data().used_stack_buffer_);
}
// The array should still be in order.
for (int i = 0; i < stack_size * 2; i++)
EXPECT_EQ(i, vect.container()[i]);
// Resize to smaller. Our STL implementation won't reallocate in this case,
// otherwise it might use our stack buffer. We reserve right after the resize
// to guarantee it isn't using the stack buffer, even though it doesn't have
// much data.
vect.container().resize(stack_size);
vect.container().reserve(stack_size * 2);
EXPECT_FALSE(vect.stack_data().used_stack_buffer_);
// Copying the small vector to another should use the same allocator and use
// the now-unused stack buffer. GENERALLY CALLERS SHOULD NOT DO THIS since
// they have to get the template types just right and it can cause errors.
std::vector<int, StackAllocator<int, stack_size>> other(vect.container());
EXPECT_EQ(stack_buffer, &other.front());
EXPECT_TRUE(vect.stack_data().used_stack_buffer_);
for (int i = 0; i < stack_size; i++)
EXPECT_EQ(i, other[i]);
}
TEST(StackContainer, VectorDoubleDelete) {
// Regression testing for double-delete.
typedef StackVector<Ref<Dummy>, 2> Vector;
Vector vect;
int alive = 0;
Ref<Dummy> dummy = AcquireRef(new Dummy(&alive));
EXPECT_EQ(alive, 1);
vect->push_back(dummy);
EXPECT_EQ(alive, 1);
Dummy* dummy_unref = dummy.Get();
dummy = nullptr;
EXPECT_EQ(alive, 1);
auto itr = std::find(vect->begin(), vect->end(), dummy_unref);
EXPECT_EQ(itr->Get(), dummy_unref);
vect->erase(itr);
EXPECT_EQ(alive, 0);
// Shouldn't crash at exit.
}
namespace {
template <size_t alignment>
class AlignedData {
public:
AlignedData() {
memset(data_, 0, alignment);
}
~AlignedData() = default;
alignas(alignment) char data_[alignment];
};
} // anonymous namespace
#define EXPECT_ALIGNED(ptr, align) EXPECT_EQ(0u, reinterpret_cast<uintptr_t>(ptr) & (align - 1))
TEST(StackContainer, BufferAlignment) {
StackVector<wchar_t, 16> text;
text->push_back(L'A');
EXPECT_ALIGNED(&text[0], alignof(wchar_t));
StackVector<double, 1> doubles;
doubles->push_back(0.0);
EXPECT_ALIGNED(&doubles[0], alignof(double));
StackVector<AlignedData<16>, 1> aligned16;
aligned16->push_back(AlignedData<16>());
EXPECT_ALIGNED(&aligned16[0], 16);
#if !defined(DAWN_COMPILER_GCC) || defined(__x86_64__) || defined(__i386__)
// It seems that non-X86 gcc doesn't respect greater than 16 byte alignment.
// See http://gcc.gnu.org/bugzilla/show_bug.cgi?id=33721 for details.
// TODO(sbc):re-enable this if GCC starts respecting higher alignments.
StackVector<AlignedData<256>, 1> aligned256;
aligned256->push_back(AlignedData<256>());
EXPECT_ALIGNED(&aligned256[0], 256);
#endif
}
template class StackVector<int, 2>;
template class StackVector<Ref<Dummy>, 2>;
template <typename T, size_t size>
void CheckStackVectorElements(const StackVector<T, size>& vec, std::initializer_list<T> expected) {
auto expected_it = expected.begin();
EXPECT_EQ(vec->size(), expected.size());
for (T t : vec) {
EXPECT_NE(expected.end(), expected_it);
EXPECT_EQ(*expected_it, t);
++expected_it;
}
EXPECT_EQ(expected.end(), expected_it);
}
TEST(StackContainer, Iteration) {
StackVector<int, 3> vect;
vect->push_back(7);
vect->push_back(11);
CheckStackVectorElements(vect, {7, 11});
for (int& i : vect) {
++i;
}
CheckStackVectorElements(vect, {8, 12});
vect->push_back(13);
CheckStackVectorElements(vect, {8, 12, 13});
vect->resize(5);
CheckStackVectorElements(vect, {8, 12, 13, 0, 0});
vect->resize(1);
CheckStackVectorElements(vect, {8});
}