SPIRV-Tools/source/util/small_vector.h
Steven Perron 1f7b1f1bf7 Small vector optimization for operands.
We replace the std::vector in the Operand class by a new class that does
a small size optimization.  This helps improve compile time on Windows.

Tested on three sets of shaders.  Trying various values for the small
vector.  The optimal value for the operand class was 2.  However, for
the Instruction class, using an std::vector was optimal.  Size of "0"
means that an std::vector was used.

                Instruction size
	        0      4      8
Operand Size

0               489    544    684
1               593    487
2               469    570
4               473
8               505

This is a single thread run of ~120 shaders.  For the multithreaded run
the results were the similar.  The basline time was ~62sec.  The
optimal configuration was an 2 for the OperandData and an
std::vector for the OperandList with a compile time of ~38sec.  Similar
expiriments were done with other sets of shaders.  The compile time still
improved, but not as much.

Contributes to https://github.com/KhronosGroup/SPIRV-Tools/issues/1609.
2018-06-12 13:41:08 -04:00

465 lines
12 KiB
C++

// Copyright (c) 2018 Google LLC
//
// 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 LIBSPIRV_UTILS_SMALL_VECTOR_H_
#define LIBSPIRV_UTILS_SMALL_VECTOR_H_
#include <cassert>
#include <iostream>
#include <vector>
#include "opt/make_unique.h"
namespace spvtools {
namespace utils {
// The |SmallVector| class is intended to be a drop-in replacement for
// |std::vector|. The difference is in the implementation. A |SmallVector| is
// optimized for when the number of elements in the vector are small. Small is
// defined by the template parameter |small_size|.
//
// Note that |SmallVector| is not always faster than an |std::vector|, so you
// should experiment with different values for |small_size| and compare to
// using and |std::vector|.
//
// TODO: I have implemented the public member functions from |std::vector| that
// I needed. If others are needed they should be implemented. Do not implement
// public member functions that are not defined by std::vector.
template <class T, size_t small_size>
class SmallVector {
public:
using iterator = T*;
using const_iterator = const T*;
SmallVector()
: size_(0),
small_data_(reinterpret_cast<T*>(buffer)),
large_data_(nullptr) {}
SmallVector(const SmallVector& that) : SmallVector() { *this = that; }
SmallVector(SmallVector&& that) : SmallVector() { *this = std::move(that); }
SmallVector(const std::vector<T>& vec) : SmallVector() {
if (vec.size() > small_size) {
large_data_.reset(new std::vector<T>(vec));
} else {
size_ = vec.size();
for (uint32_t i = 0; i < size_; i++) {
new (small_data_ + i) T(vec[i]);
}
}
}
SmallVector(std::vector<T>&& vec) : SmallVector() {
if (vec.size() > small_size) {
large_data_.reset(new std::vector<T>(std::move(vec)));
} else {
size_ = vec.size();
for (uint32_t i = 0; i < size_; i++) {
new (small_data_ + i) T(std::move(vec[i]));
}
}
vec.clear();
}
SmallVector(std::initializer_list<T> init_list) : SmallVector() {
if (init_list.size() < small_size) {
for (auto it = init_list.begin(); it != init_list.end(); ++it) {
new (small_data_ + (size_++)) T(std::move(*it));
}
} else {
large_data_.reset(new std::vector<T>(std::move(init_list)));
}
}
SmallVector(size_t s, const T& v) : SmallVector() { resize(s, v); }
virtual ~SmallVector() {
for (T* p = small_data_; p < small_data_ + size_; ++p) {
p->~T();
}
}
SmallVector& operator=(const SmallVector& that) {
assert(small_data_);
if (that.large_data_) {
if (large_data_) {
*large_data_ = *that.large_data_;
} else {
large_data_.reset(new std::vector<T>(*that.large_data_));
}
} else {
large_data_.reset(nullptr);
size_t i = 0;
// Do a copy for any element in |this| that is already constructed.
for (; i < size_ && i < that.size_; ++i) {
small_data_[i] = that.small_data_[i];
}
if (i >= that.size_) {
// If the size of |this| becomes smaller after the assignment, then
// destroy any extra elements.
for (; i < size_; ++i) {
small_data_[i].~T();
}
} else {
// If the size of |this| becomes larger after the assignement, copy
// construct the new elements that are needed.
for (; i < that.size_; ++i) {
new (small_data_ + i) T(that.small_data_[i]);
}
}
size_ = that.size_;
}
return *this;
}
SmallVector& operator=(SmallVector&& that) {
if (that.large_data_) {
large_data_.reset(that.large_data_.release());
} else {
large_data_.reset(nullptr);
size_t i = 0;
// Do a move for any element in |this| that is already constructed.
for (; i < size_ && i < that.size_; ++i) {
small_data_[i] = std::move(that.small_data_[i]);
}
if (i >= that.size_) {
// If the size of |this| becomes smaller after the assignment, then
// destroy any extra elements.
for (; i < size_; ++i) {
small_data_[i].~T();
}
} else {
// If the size of |this| becomes larger after the assignement, move
// construct the new elements that are needed.
for (; i < that.size_; ++i) {
new (small_data_ + i) T(std::move(that.small_data_[i]));
}
}
size_ = that.size_;
}
// Reset |that| because all of the data has been moved to |this|.
that.DestructSmallData();
return *this;
}
template <class OtherVector>
friend bool operator==(const SmallVector& lhs, const OtherVector& rhs) {
if (lhs.size() != rhs.size()) {
return false;
}
auto rit = rhs.begin();
for (auto lit = lhs.begin(); lit != lhs.end(); ++lit, ++rit) {
if (*lit != *rit) {
return false;
}
}
return true;
}
friend bool operator==(const std::vector<T>& lhs, const SmallVector& rhs) {
return rhs == lhs;
}
friend bool operator!=(const SmallVector& lhs, const std::vector<T>& rhs) {
return !(lhs == rhs);
}
friend bool operator!=(const std::vector<T>& lhs, const SmallVector& rhs) {
return rhs != lhs;
}
T& operator[](size_t i) {
if (!large_data_) {
return small_data_[i];
} else {
return (*large_data_)[i];
}
}
const T& operator[](size_t i) const {
if (!large_data_) {
return small_data_[i];
} else {
return (*large_data_)[i];
}
}
size_t size() const {
if (!large_data_) {
return size_;
} else {
return large_data_->size();
}
}
iterator begin() {
if (large_data_) {
return large_data_->data();
} else {
return small_data_;
}
}
const_iterator begin() const {
if (large_data_) {
return large_data_->data();
} else {
return small_data_;
}
}
const_iterator cbegin() const { return begin(); }
iterator end() {
if (large_data_) {
return large_data_->data() + large_data_->size();
} else {
return small_data_ + size_;
}
}
const_iterator end() const {
if (large_data_) {
return large_data_->data() + large_data_->size();
} else {
return small_data_ + size_;
}
}
const_iterator cend() const { return end(); }
T* data() { return begin(); }
const T* data() const { return cbegin(); }
T& front() { return (*this)[0]; }
const T& front() const { return (*this)[0]; }
iterator erase(const_iterator pos) { return erase(pos, pos + 1); }
iterator erase(const_iterator first, const_iterator last) {
if (large_data_) {
size_t start_index = first - large_data_->data();
size_t end_index = last - large_data_->data();
auto r = large_data_->erase(large_data_->begin() + start_index,
large_data_->begin() + end_index);
return large_data_->data() + (r - large_data_->begin());
}
// Since C++11, std::vector has |const_iterator| for the parameters, so I
// follow that. However, I need iterators to modify the current container,
// which is not const. This is why I cast away the const.
iterator f = const_cast<iterator>(first);
iterator l = const_cast<iterator>(last);
iterator e = end();
size_t num_of_del_elements = last - first;
iterator ret = f;
if (first == last) {
return ret;
}
// Move |last| and any elements after it their earlier position.
while (l != e) {
*f = std::move(*l);
++f;
++l;
}
// Destroy the elements that were supposed to be deleted.
while (f != l) {
f->~T();
++f;
}
// Update the size.
size_ -= num_of_del_elements;
return ret;
}
void push_back(const T& value) {
if (!large_data_ && size_ == small_size) {
MoveToLargeData();
}
if (large_data_) {
large_data_->push_back(value);
return;
}
new (small_data_ + size_) T(value);
++size_;
}
void push_back(T&& value) {
if (!large_data_ && size_ == small_size) {
MoveToLargeData();
}
if (large_data_) {
large_data_->push_back(std::move(value));
return;
}
new (small_data_ + size_) T(std::move(value));
++size_;
}
template <class InputIt>
iterator insert(iterator pos, InputIt first, InputIt last) {
size_t element_idx = (pos - begin());
size_t num_of_new_elements = std::distance(first, last);
size_t new_size = size_ + num_of_new_elements;
if (!large_data_ && new_size > small_size) {
MoveToLargeData();
}
if (large_data_) {
typename std::vector<T>::iterator new_pos =
large_data_->begin() + element_idx;
large_data_->insert(new_pos, first, last);
return begin() + element_idx;
}
// Move |pos| and all of the elements after it over |num_of_new_elements|
// places. We start at the end and work backwards, to make sure we do not
// overwrite data that we have not moved yet.
for (iterator i = begin() + new_size - 1, j = end() - 1; j >= pos;
--i, --j) {
if (i >= begin() + size_) {
new (i) T(std::move(*j));
} else {
*i = std::move(*j);
}
}
// Copy the new elements into position.
iterator p = pos;
for (; first != last; ++p, ++first) {
if (p >= small_data_ + size_) {
new (p) T(*first);
} else {
*p = *first;
}
}
// Upate the size.
size_ += num_of_new_elements;
return pos;
}
bool empty() const {
if (large_data_) {
return large_data_->empty();
}
return size_ == 0;
}
void clear() {
if (large_data_) {
large_data_->clear();
} else {
DestructSmallData();
}
}
template <class... Args>
void emplace_back(Args&&... args) {
if (!large_data_ && size_ == small_size) {
MoveToLargeData();
}
if (large_data_) {
large_data_->emplace_back(std::forward<Args>(args)...);
} else {
new (small_data_ + size_) T(std::forward<Args>(args)...);
++size_;
}
}
void resize(size_t new_size, const T& v) {
if (!large_data_ && new_size > small_size) {
MoveToLargeData();
}
if (large_data_) {
large_data_->resize(new_size, v);
return;
}
// If |new_size| < |size_|, then destroy the extra elements.
for (size_t i = new_size; i < size_; ++i) {
small_data_[i].~T();
}
// If |new_size| > |size_|, the copy construct the new elements.
for (size_t i = size_; i < new_size; ++i) {
new (small_data_ + i) T(v);
}
// Update the size.
size_ = new_size;
}
private:
// Moves all of the element from |small_data_| into a new std::vector that can
// be access through |large_data|.
void MoveToLargeData() {
assert(!large_data_);
large_data_.reset(new std::vector<T>());
for (size_t i = 0; i < size_; ++i) {
large_data_->emplace_back(std::move(small_data_[i]));
}
DestructSmallData();
}
// Destroys all of the elements in |small_data_| that have been constructed.
void DestructSmallData() {
for (size_t i = 0; i < size_; ++i) {
small_data_[i].~T();
}
size_ = 0;
}
// The number of elements in |small_data_| that have been constructed.
size_t size_;
// The pointed used to access the array of elements when the number of
// elements is small.
T* small_data_;
// The actual data used to store the array elements. It must never be used
// directly, but must only be accesed through |small_data_|.
typename std::aligned_storage<sizeof(T), std::alignment_of<T>::value>::type
buffer[small_size];
// A pointer to a vector that is used to store the elements of the vector when
// this size exceeds |small_size|. If |large_data_| is nullptr, then the data
// is stored in |small_data_|. Otherwise, the data is stored in
// |large_data_|.
std::unique_ptr<std::vector<T>> large_data_;
}; // namespace utils
} // namespace utils
} // namespace spvtools
#endif // LIBSPIRV_UTILS_SMALL_VECTOR_H_