blob: 683dfa6a34a023896f93a3d23e47b071891b25d9 [file] [edit]
#ifndef SLINKY_RUNTIME_BUFFER_H
#define SLINKY_RUNTIME_BUFFER_H
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <type_traits>
#include "base/arithmetic.h"
#include "base/function_ref.h"
#include "base/span.h"
#include "base/util.h"
namespace slinky {
// index_t needs to at least be as big as a pointer and must be signed.
// Using ptrdiff_t or intptr_t here seems tempting, but those can
// alias to `long` under some compilers which can cause some not so fun
// overloading issues with expr(), so let's use std::conditional
// instead to make it an exact alias of either int32_t or int64_t.
using index_t = std::conditional<sizeof(void*) == 4, std::int32_t, std::int64_t>::type;
// Helper to offset a pointer by a number of bytes.
template <typename T>
T* offset_bytes_non_null(T* x, std::ptrdiff_t bytes) {
assert(x != nullptr || bytes == 0);
return reinterpret_cast<T*>(reinterpret_cast<char*>(x) + bytes);
}
template <typename T>
const T* offset_bytes_non_null(const T* x, std::ptrdiff_t bytes) {
assert(x != nullptr || bytes == 0);
return reinterpret_cast<const T*>(reinterpret_cast<const char*>(x) + bytes);
}
template <typename T>
T* offset_bytes(T* x, std::ptrdiff_t bytes) {
return x ? reinterpret_cast<T*>(reinterpret_cast<char*>(x) + bytes) : x;
}
template <typename T>
const T* offset_bytes(const T* x, std::ptrdiff_t bytes) {
return x ? reinterpret_cast<const T*>(reinterpret_cast<const char*>(x) + bytes) : x;
}
template <typename T>
T* align_up(T* x, std::size_t align) {
return reinterpret_cast<T*>((reinterpret_cast<uintptr_t>(x) + align - 1) & ~(align - 1));
}
template <typename T>
const T* align_up(const T* x, std::size_t align) {
return reinterpret_cast<const T*>((reinterpret_cast<uintptr_t>(x) + align - 1) & ~(align - 1));
}
// TODO(https://github.com/dsharlet/slinky/issues/1): This and buffer_expr in pipeline.h should have the same API
// (except for expr instead of index_t).
class dim {
alignas(16) index_t min_;
index_t max_;
index_t stride_;
index_t fold_factor_;
public:
static constexpr index_t auto_stride = std::numeric_limits<index_t>::max();
static constexpr index_t unfolded = std::numeric_limits<index_t>::max();
dim() : min_(0), max_(-1), stride_(auto_stride), fold_factor_(unfolded) {}
dim(index_t min, index_t max, index_t stride = auto_stride, index_t fold_factor = unfolded)
: min_(min), max_(max), stride_(stride), fold_factor_(fold_factor) {}
static const dim& broadcast();
index_t min() const { return min_; }
index_t max() const { return max_; }
index_t begin() const { return min_; }
index_t end() const { return max_ + 1; }
index_t extent() const { return end() > begin() ? end() - begin() : 0; }
index_t stride() const { return stride_; }
index_t fold_factor() const { return fold_factor_; }
bool empty() const { return max_ < min_; }
bool unbounded() const {
return min_ == std::numeric_limits<index_t>::min() && max_ == std::numeric_limits<index_t>::max();
}
void set_extent(index_t extent) { max_ = min_ + extent - 1; }
void set_point(index_t x) {
min_ = x;
max_ = x;
}
void set_bounds(index_t min, index_t max) {
min_ = min;
max_ = max;
}
void set_range(index_t begin, index_t end) {
min_ = begin;
max_ = end - 1;
}
void set_unbounded() {
min_ = std::numeric_limits<index_t>::min();
max_ = std::numeric_limits<index_t>::max();
}
void set_min_extent(index_t min, index_t extent) {
min_ = min;
max_ = min + extent - 1;
}
void set_stride(index_t stride) { stride_ = stride; }
void set_fold_factor(index_t fold_factor) { fold_factor_ = fold_factor; }
void translate(index_t offset) {
min_ += offset;
max_ += offset;
}
// Returns true if the interval [a, b] is in bounds of this dimension.
bool contains(index_t a, index_t b) const { return min() <= a && b <= max(); }
bool contains(index_t x) const { return contains(x, x); }
bool contains(const dim& other) const { return contains(other.min(), other.max()); }
std::ptrdiff_t flat_offset_bytes(index_t i) const {
assert(contains(i));
#ifdef UNDEFINED_BEHAVIOR_SANITIZER
// Some integer overflow below is harmless when multiplied by zero, but flagged by ubsan.
if (stride() == 0) return 0;
#endif
if (fold_factor() == unfolded) {
return (i - min()) * stride();
} else {
return euclidean_mod_positive_modulus(i, fold_factor()) * stride();
}
}
// Check if the dimension crosses a fold between min and max.
bool is_folded(index_t min, index_t max) const {
if (stride() == 0 || fold_factor() == unfolded) return false;
return euclidean_div_positive_divisor(min, fold_factor()) != euclidean_div_positive_divisor(max, fold_factor());
}
bool is_folded(const dim& other) const { return is_folded(other.min(), other.max()); }
bool is_folded() const { return is_folded(min(), max()); }
};
template <typename T, std::size_t DimsSize = 0>
class buffer;
class raw_buffer;
using raw_buffer_ptr = std::shared_ptr<raw_buffer>;
using const_raw_buffer_ptr = std::shared_ptr<const raw_buffer>;
// This value allows expressing `at` and `address_at` arguments that slice that dimension.
static constexpr struct {
} slice;
// We have some difficult requirements for this buffer object:
// 1. We want type safety in user code, but we also want to be able to treat buffers as generic.
// 2. We want to store metadata (dimensions) efficiently.
// a. No extra storage for unused dimensions.
// b. In the same allocation as the buffer object itself if needed.
//
// To this end, we have a class (struct) raw_buffer:
// - Type erased.
// - Does not provide storage for dims (unless you use raw_buffer::make).
//
// And a class buffer<T, DimsSize>:
// - Has a type, can be accessed via operator() and at.
// - Provides storage for DimsSize dims (default is 0).
class raw_buffer {
protected:
static std::ptrdiff_t flat_offset_bytes_impl(const dim* dims, index_t i0) { return dims->flat_offset_bytes(i0); }
static std::ptrdiff_t flat_offset_bytes_impl(const dim* dims, decltype(slinky::slice)) { return 0; }
template <typename... Indices>
static std::ptrdiff_t flat_offset_bytes_impl(const dim* dims, index_t i0, Indices... indices) {
return dims->flat_offset_bytes(i0) + flat_offset_bytes_impl(dims + 1, indices...);
}
template <typename... Indices>
static std::ptrdiff_t flat_offset_bytes_impl(const dim* dims, decltype(slinky::slice), Indices... indices) {
return flat_offset_bytes_impl(dims + 1, indices...);
}
static bool contains_impl(const dim* dims, index_t i0) { return dims->contains(i0); }
template <typename... Indices>
static bool contains_impl(const dim* dims, index_t i0, Indices... indices) {
return dims->contains(i0) && contains_impl(dims + 1, indices...);
}
static void translate_impl(dim* dims, index_t o0) { dims->translate(o0); }
template <typename... Offsets>
static void translate_impl(dim* dims, index_t o0, Offsets... offsets) {
dims->translate(o0);
translate_impl(dims + 1, offsets...);
}
public:
using element = void;
using pointer = void*;
alignas(16) void* base;
std::size_t elem_size;
std::size_t rank;
slinky::dim* dims;
slinky::dim& dim(std::size_t i) {
assert(i < rank);
return dims[i];
}
const slinky::dim& dim(std::size_t i) const {
assert(i < rank);
return dims[i];
}
// `indices` may either be integral, or `slice`, indicating that the dimension should be sliced.
template <typename... Indices>
std::ptrdiff_t flat_offset_bytes(index_t i0, Indices... indices) const {
assert(sizeof...(indices) + 1 <= rank);
return flat_offset_bytes_impl(dims, i0, indices...);
}
template <typename... Indices>
std::ptrdiff_t flat_offset_bytes(decltype(slinky::slice) i0, Indices... indices) const {
assert(sizeof...(indices) + 1 <= rank);
return flat_offset_bytes_impl(dims, i0, indices...);
}
template <typename... Indices>
void* address_at(index_t i0, Indices... indices) const {
assert(sizeof...(indices) + 1 <= rank);
return offset_bytes(base, flat_offset_bytes(i0, indices...));
}
template <typename... Indices>
void* address_at(decltype(slinky::slice) i0, Indices... indices) const {
assert(sizeof...(indices) + 1 <= rank);
return offset_bytes(base, flat_offset_bytes(i0, indices...));
}
std::ptrdiff_t flat_offset_bytes() const { return 0; }
void* address_at() const { return base; }
template <typename... Indices>
bool contains(index_t i0, Indices... indices) const {
assert(sizeof...(indices) + 1 <= rank);
return contains_impl(dims, i0, indices...);
}
template <typename... Indices>
bool contains(decltype(slinky::slice) i0, Indices... indices) const {
assert(sizeof...(indices) + 1 <= rank);
return contains_impl(dims, i0, indices...);
}
bool contains() const { return true; }
std::ptrdiff_t flat_offset_bytes(span<const index_t> indices) const {
assert(indices.size() <= rank);
index_t offset = 0;
for (std::size_t i = 0; i < indices.size(); ++i) {
offset += dims[i].flat_offset_bytes(indices[i]);
}
return offset;
}
void* address_at(span<const index_t> indices) const { return offset_bytes(base, flat_offset_bytes(indices)); }
bool contains(span<const index_t> indices) const {
assert(indices.size() <= rank);
bool result = true;
for (std::size_t i = 0; i < indices.size(); ++i) {
result = result && dims[i].contains(indices[i]);
}
return result;
}
template <typename... Offsets>
raw_buffer& translate(index_t o0, Offsets... offsets) {
assert(sizeof...(offsets) + 1 <= rank);
translate_impl(dims, o0, offsets...);
return *this;
}
raw_buffer& translate(span<const index_t> offsets) {
assert(offsets.size() <= rank);
for (std::size_t i = 0; i < offsets.size(); ++i) {
dims[i].translate(offsets[i]);
}
return *this;
}
// Remove dimensions `ds`. The dimensions must be sorted in ascending order.
raw_buffer& slice(span<const std::size_t> ds) {
if (ds.size() == 1) return slice(ds[0]);
// Handle any slices of leading dimensions by just incrementing the dims pointer.
std::size_t slice_leading = 0;
for (std::size_t d : ds) {
if (d == slice_leading) {
++slice_leading;
} else {
break;
}
}
for (std::size_t i = slice_leading; i < ds.size(); ++i) {
std::size_t d = ds[i];
std::size_t next_d = i + 1 < ds.size() ? ds[i + 1] : rank;
assert(d < rank);
assert(next_d <= rank);
// Move the dimensions between this slice and the next slice down by the number of slices we've done so far.
// d and next_d are indices in the original dimensions.
for (std::size_t j = d; j + 1 < next_d; ++j) {
dims[j - (i - slice_leading)] = dims[j + 1];
}
}
dims += slice_leading;
rank -= ds.size();
return *this;
}
raw_buffer& slice(std::initializer_list<std::size_t> ds) { return slice({&*ds.begin(), ds.size()}); }
// Remove dimension `d` and move the base pointer to point to `at` in this dimension.
// `at` is dim(d).min() by default.
// If `d` is 0 or rank - 1, the slice does not mutate the dims array.
raw_buffer& slice(std::size_t d) {
assert(d < rank);
rank -= 1;
if (d == 0) {
// Slicing the first leading dimension, we can just increment the dims pointer.
dims += 1;
} else {
// We need to move all the dims above `d` down by one.
for (std::size_t i = d; i < rank; ++i) {
dims[i] = dims[i + 1];
}
}
return *this;
}
raw_buffer& slice(std::size_t d, index_t at) {
if (base != nullptr) {
if (dim(d).contains(at)) {
base = offset_bytes_non_null(base, dim(d).flat_offset_bytes(at));
} else {
base = nullptr;
}
}
return slice(d);
}
// Crop the buffer in dimension `d` to the bounds `[min, max]`. The bounds will be clamped to the existing bounds.
// Updates the base pointer to point to the new min.
raw_buffer& crop(std::size_t d, index_t min, index_t max) {
min = std::max(min, dim(d).min());
max = std::min(max, dim(d).max());
if (base != nullptr) {
if (max >= min) {
if (dim(d).fold_factor() == dim::unfolded) {
index_t offset = dim(d).flat_offset_bytes(min);
base = offset_bytes_non_null(base, offset);
}
} else {
base = nullptr;
}
}
dim(d).set_bounds(min, max);
return *this;
}
std::size_t size_bytes() const;
std::size_t elem_count() const;
// If any strides are `auto_stride`, replace them with automatically determined strides.
// `alignment` must be a power of 2.
std::size_t init_strides(index_t alignment = 1);
// Allocate and set the base pointer using `malloc`. Returns a pointer to the allocated memory, which should
// be deallocated with `aligned_free`. `alignment` must be a power of 2.
void* allocate(index_t alignment = 1);
template <typename NewT>
const buffer<NewT>& cast() const;
// Make a pointer to a buffer with an allocation for the dims and (optionally) elements in the same allocation.
static raw_buffer_ptr make(std::size_t rank, std::size_t elem_size, const class dim* dims = nullptr);
// Make a deep copy of another buffer, including allocating and copying the data.
static raw_buffer_ptr make_copy(const raw_buffer& src);
// Make a buffer around a scalar value. The resulting buffer will have rank 0. The result is a heap allocated
// buffer that contains a copy of the scalar value.
static raw_buffer_ptr make_scalar(std::size_t elem_size, const void* value);
template <typename T, typename = typename std::enable_if_t<std::is_trivial_v<T>>>
static raw_buffer_ptr make_scalar(const T& value) {
return make_scalar(sizeof(T), &value);
}
// Make a buffer around a scalar value. The resulting buffer will have rank 0. The result is a buffer that contains a
// pointer to the value.
static raw_buffer make_scalar_ref(std::size_t elem_size, void* value) {
return raw_buffer{value, elem_size, 0, nullptr};
}
template <typename T>
static raw_buffer make_scalar_ref(const T& value) {
return make_scalar_ref(sizeof(T), &value);
}
};
// This is a wrapper for a raw_buffer that represents a scalar value, with storage for the scalar value.
template <typename T>
class scalar : public raw_buffer {
public:
T value;
scalar(const T& value = T()) : value(value) {
base = &this->value;
elem_size = sizeof(T);
rank = 0;
dims = nullptr;
}
};
namespace internal {
template <typename T>
struct default_elem_size {
static constexpr std::size_t value = sizeof(T);
};
template <>
struct default_elem_size<void> {
static constexpr std::size_t value = 0;
};
template <>
struct default_elem_size<const void> {
static constexpr std::size_t value = 0;
};
template <typename T>
void copy_small_n(const T* src, std::size_t n, T* dst) {
switch (n) {
case 4: *dst++ = *src++;
case 3: *dst++ = *src++;
case 2: *dst++ = *src++;
case 1: *dst++ = *src++;
case 0: return;
default: std::copy_n(src, n, dst); return;
}
}
template <typename T>
void copy_small_n_backward(const T* src, std::size_t n, T* dst) {
switch (n) {
case 4: *(--dst) = *(--src);
case 3: *(--dst) = *(--src);
case 2: *(--dst) = *(--src);
case 1: *(--dst) = *(--src);
case 0: return;
default: std::copy_backward(src, src + n, dst + n); return;
}
}
} // namespace internal
template <typename T, std::size_t DimsSize>
class buffer : public raw_buffer {
private:
void* to_free;
// Avoid default constructor of slinky::dim, we might not use this.
alignas(slinky::dim) char dims_storage[sizeof(slinky::dim) * DimsSize];
void assign_dims(std::size_t rank, const slinky::dim* src = nullptr) {
assert(rank <= DimsSize);
this->rank = rank;
if (DimsSize > 0) {
dims = reinterpret_cast<slinky::dim*>(dims_storage);
if (src) {
internal::copy_small_n(src, rank, dims);
} else {
new (dims) slinky::dim[rank];
}
} else {
dims = nullptr;
}
}
void copy_construct(const raw_buffer& c) {
raw_buffer::base = c.base;
elem_size = c.elem_size;
assign_dims(c.rank, c.dims);
to_free = nullptr;
}
buffer& assign(const raw_buffer& c) {
if (static_cast<void*>(this) == static_cast<const void*>(&c)) return *this;
free();
copy_construct(c);
return *this;
}
template <std::size_t OtherDimsSize>
void move_construct(buffer<T, OtherDimsSize>&& m) {
copy_construct(m);
// Take ownership of the data.
to_free = m.to_free;
m.to_free = nullptr;
}
template <std::size_t OtherDimsSize>
buffer& move(buffer<T, OtherDimsSize>&& m) {
assign(m);
// Take ownership of the data.
std::swap(to_free, m.to_free);
return *this;
}
public:
using element = T;
using pointer = T*;
using raw_buffer::cast;
using raw_buffer::dim;
using raw_buffer::elem_size;
using raw_buffer::flat_offset_bytes;
using raw_buffer::rank;
buffer() {
raw_buffer::base = nullptr;
to_free = nullptr;
assign_dims(DimsSize);
elem_size = internal::default_elem_size<T>::value;
}
explicit buffer(std::size_t rank, std::size_t elem_size = internal::default_elem_size<T>::value) {
raw_buffer::base = nullptr;
to_free = nullptr;
assign_dims(rank);
this->elem_size = elem_size;
}
// Construct a buffer with extents, and strides computed such that the stride of dimension
// n is the product of all the extents of dimensions [0, n) and elem_size, i.e. the first
// dimension is "innermost".
buffer(span<const index_t> extents, std::size_t elem_size = internal::default_elem_size<T>::value)
: buffer(extents.size(), elem_size) {
slinky::dim* d = dims;
for (index_t extent : extents) {
d->set_min_extent(0, extent);
++d;
}
init_strides();
}
buffer(std::initializer_list<index_t> extents, std::size_t elem_size = internal::default_elem_size<T>::value)
: buffer({extents.begin(), extents.end()}, elem_size) {}
// TODO: A more general version of this constructor would probably be useful.
buffer(T* base, index_t size, std::size_t elem_size = internal::default_elem_size<T>::value) : buffer({size}) {
raw_buffer::base = base;
}
~buffer() { free(); }
// All buffer copy/assignment operators are shallow copies.
buffer(const raw_buffer& c) { copy_construct(c); }
buffer(const buffer& c) { copy_construct(c); }
buffer(buffer&& m) { move_construct(std::move(m)); }
template <std::size_t OtherDimsSize>
buffer(const buffer<T, OtherDimsSize>& c) {
copy_construct(c);
}
template <std::size_t OtherDimsSize>
buffer(buffer<T, OtherDimsSize>&& m) {
move_construct(std::move(m));
}
buffer& operator=(const raw_buffer& c) { return assign(c); }
buffer& operator=(const buffer& c) { return assign(c); }
buffer& operator=(buffer&& m) { return move(std::move(m)); }
template <std::size_t OtherDimsSize>
buffer& operator=(const buffer<T, OtherDimsSize>& c) {
return assign(c);
}
template <std::size_t OtherDimsSize>
buffer& operator=(buffer<T, OtherDimsSize>&& m) {
return move(std::move(m));
}
T* base() const { return reinterpret_cast<T*>(raw_buffer::base); }
// These accessors are not designed to be fast. They exist to facilitate testing,
// and maybe they are useful to compute addresses.
// `indices` may either be integral, or `slice`, indicating that the dimension should be sliced.
template <typename... Indices>
auto& at(index_t i0, Indices... indices) const {
return *offset_bytes_non_null(base(), flat_offset_bytes(i0, indices...));
}
template <typename... Indices>
auto& at(decltype(slinky::slice) i0, Indices... indices) const {
return *offset_bytes_non_null(base(), flat_offset_bytes(i0, indices...));
}
template <typename... Indices>
auto& operator()(index_t i0, Indices... indices) const {
return at(i0, indices...);
}
template <typename... Indices>
auto& operator()(decltype(slinky::slice) i0, Indices... indices) const {
return at(i0, indices...);
}
auto& at() const { return *base(); }
auto& operator()() const { return *base(); }
auto& at(span<const index_t> indices) const { return *offset_bytes_non_null(base(), flat_offset_bytes(indices)); }
auto& operator()(span<const index_t> indices) const { return at(indices); }
// Insert a new dimension `dim` at index d, increasing the rank by 1.
buffer<T, DimsSize>& unslice(std::size_t d, const slinky::dim& dim) {
assert(d <= rank);
slinky::dim* dims_storage = reinterpret_cast<slinky::dim*>(this->dims_storage);
if (d == 0 && &dims_storage[0] < dims) {
assert(dims < &dims_storage[DimsSize]);
dims -= 1;
} else {
assert(&dims_storage[0] <= dims && dims + 1 < &dims_storage[DimsSize]);
std::copy_backward(dims + d, dims + rank, dims + rank + 1);
}
rank += 1;
dims[d] = dim;
return *this;
}
void allocate(index_t alignment = 1) {
assert(!to_free);
to_free = raw_buffer::allocate(alignment);
}
void free() {
if (to_free) {
::free(to_free);
to_free = nullptr;
}
}
};
template <typename NewT>
const buffer<NewT>& raw_buffer::cast() const {
return *reinterpret_cast<const buffer<NewT>*>(this);
}
// Copy the contents of `src` to `dst`.
// If `padding` is nullptr, every index of `dst` must be in bounds of `src`.
// If `padding` is not nullptr, `dst` will be copied from `src` if it is in bounds, otherwise it will be copied from
// `padding`, which must be in bounds.
void copy(const raw_buffer& src, const raw_buffer& dst, const raw_buffer* pad = nullptr);
inline void copy(const raw_buffer& src, const raw_buffer& dst, const raw_buffer& pad) { copy(src, dst, &pad); }
// Performs only the padding operation of a copy. The region that would have been copied is unmodified.
void pad(const dim* src_bounds, const raw_buffer& dst, const raw_buffer& pad);
// Returns true if the two dimensions can be fused.
inline bool can_fuse(const dim& inner, const dim& outer) {
if (outer.min() == outer.max()) return true;
#ifdef UNDEFINED_BEHAVIOR_SANITIZER
// Some integer overflow below is harmless when multiplied by zero, but flagged by ubsan.
index_t next_stride = inner.stride() == 0 ? 0 : inner.stride() * inner.extent();
#else
index_t next_stride = inner.stride() * (inner.max() - inner.min() + 1);
#endif
if (next_stride != outer.stride()) return false;
return next_stride == 0 || inner.fold_factor() == dim::unfolded;
}
enum class fuse_type {
// Leave the outer dimension in its place in an undefined state.
undef,
// Replace the outer dimension with a dimension of extent 1 and min 0, preserving the rank.
keep,
// Remove the outer dimension, reducing the rank by 1.
remove,
};
// Fuse two dimensions of a buffer.
template <fuse_type type>
inline void fuse(index_t inner, index_t outer, raw_buffer& buf) {
if (outer >= static_cast<index_t>(buf.rank)) {
// Fusing an implicit broadcast, nothing to do.
assert(inner >= static_cast<index_t>(buf.rank) || can_fuse(buf.dim(inner), dim::broadcast()));
} else if (inner >= static_cast<index_t>(buf.rank)) {
// The inner dimension is an implicit broadcast.
dim& od = buf.dim(outer);
assert(can_fuse(dim::broadcast(), od));
if (type == fuse_type::keep) {
od.set_point(0);
} else if (type == fuse_type::remove) {
buf.slice(outer);
} else {
assert(type == fuse_type::undef);
}
} else {
dim& id = buf.dim(inner);
dim& od = buf.dim(outer);
assert(can_fuse(id, od));
if (id.min() == id.max()) {
if (type == fuse_type::remove) {
buf.slice(inner);
return;
} else {
id = od;
}
} else if (id.unbounded()) {
// Already fused
} else {
const index_t id_extent = id.extent();
if (od.min() != od.max() && od.fold_factor() != dim::unfolded) {
assert(id.fold_factor() == dim::unfolded);
id.set_fold_factor(od.fold_factor() * id_extent);
}
if (od.unbounded()) {
id.set_unbounded();
} else {
id.set_range(od.begin() * id_extent, od.end() * id_extent);
}
}
if (type == fuse_type::keep) {
od.set_point(0);
} else if (type == fuse_type::remove) {
buf.slice(outer);
} else {
assert(type == fuse_type::undef);
}
}
}
namespace internal {
// Returns true if all buffers have the same rank.
inline bool same_rank(const raw_buffer&) { return true; }
inline bool same_rank(const raw_buffer& buf0, const raw_buffer& buf1) { return buf0.rank == buf1.rank; }
template <typename... Bufs>
bool same_rank(const raw_buffer& buf0, const raw_buffer& buf1, const Bufs&... bufs) {
return buf0.rank == buf1.rank && same_rank(buf1, bufs...);
}
inline bool same_bounds(const dim& a, const dim& b) {
return a.min() == b.min() && a.max() == b.max();
}
inline const dim& dim_or_broadcast(const raw_buffer& buf, std::ptrdiff_t d) {
return d < static_cast<std::ptrdiff_t>(buf.rank) ? buf.dim(d) : dim::broadcast();
}
// Returns true if all buffers have the same bounds in dimension d.
inline bool same_bounds(std::ptrdiff_t, const raw_buffer&) { return true; }
template <typename... Bufs>
bool same_bounds(std::size_t d, const raw_buffer& buf0, const raw_buffer& buf1, const Bufs&... bufs) {
return (buf0.rank <= d || buf1.rank <= d || same_bounds(buf0.dim(d), buf1.dim(d))) && same_bounds(d, buf1, bufs...);
}
// Returns true if two dimensions of all buffers can be fused.
inline bool can_fuse(std::ptrdiff_t, std::ptrdiff_t) { return true; }
template <typename... Bufs>
bool can_fuse(std::ptrdiff_t inner, std::ptrdiff_t outer, const raw_buffer& buf, const Bufs&... bufs) {
return can_fuse(dim_or_broadcast(buf, inner), dim_or_broadcast(buf, outer)) && can_fuse(inner, outer, bufs...);
}
// Fuse two dimensions of all buffers.
template <fuse_type type>
void fuse(std::ptrdiff_t inner, std::ptrdiff_t outer) {}
template <fuse_type type, typename... Bufs>
void fuse(std::ptrdiff_t inner, std::ptrdiff_t outer, raw_buffer& buf, Bufs&... bufs) {
fuse<type>(inner, outer, buf);
fuse<type>(inner, outer, bufs...);
}
template <typename... Bufs>
SLINKY_ALWAYS_INLINE inline void attempt_fuse(
std::ptrdiff_t inner, std::ptrdiff_t outer, raw_buffer& buf, Bufs&... bufs) {
if (same_bounds(inner, buf, bufs...) && can_fuse(inner, outer, buf, bufs...)) {
fuse<fuse_type::remove>(inner, outer, buf, bufs...);
}
}
template <typename... Bufs>
SLINKY_ALWAYS_INLINE inline void attempt_fuse(
std::ptrdiff_t inner, std::ptrdiff_t outer, span<const int> dim_sets, raw_buffer& buf, Bufs&... bufs) {
if (static_cast<int>(dim_sets.size()) > outer && dim_sets[outer] != dim_sets[inner]) {
// These two dims are not part of the same set. Don't fuse them.
return;
}
attempt_fuse(inner, outer, buf, bufs...);
}
inline void swap_dims(std::size_t i, std::size_t j, raw_buffer& buf) { std::swap(buf.dim(i), buf.dim(j)); }
template <typename... Bufs>
void swap_dims(std::size_t i, std::size_t j, raw_buffer& buf, Bufs&... bufs) {
swap_dims(i, j, buf);
swap_dims(i, j, bufs...);
}
} // namespace internal
// Jointly sort the dimensions of all buffers such that the strides of the first buffer are in ascending order.
// `dim_sets` is an optional set of integers that indicates the sets that dimensions belong to. Dimensions are only
// sorted within the same set, where dimension `d` is identified by the value of `dim_sets[d]`. By default, all
// dimensions are considered to be in the same set.
// Returns true if the sort changed the ordering of the dimensions.
template <typename... Bufs>
bool sort_dims(span<const int> dim_sets, raw_buffer& buf, Bufs&... bufs) {
// We only attempt to sort the dimensions that exist in all buffers. We could do better here, sometimes we can
// swap implicit broadcast dimensions with another broadcast dimension.
const size_t rank = std::min({buf.rank, bufs.rank...});
bool modified = false;
// A bubble sort is appropriate here, because:
// - Typically, buffer ranks are very small.
// - To use std::sort or similar, we need to copy the buffer dimensions into a temporary, sort, and copy them back.
// - This template will be instantiated very frequently, it's worth attempting to minimize code size.
for (std::size_t i = 0; i < rank; ++i) {
for (std::size_t j = i + 1; j < rank; ++j) {
if (j < dim_sets.size() && dim_sets[i] != dim_sets[j]) continue;
if (buf.dim(i).stride() > buf.dim(j).stride()) {
internal::swap_dims(i, j, buf, bufs...);
modified = true;
}
}
}
return modified;
}
template <typename... Bufs>
bool sort_dims(raw_buffer& buf, Bufs&... bufs) {
return sort_dims({}, buf, bufs...);
}
// Fuse the dimensions of a set of buffers where the dimensions are contiguous in memory. It only fuses a dimension if
// the same dimension can be fused in all buffers. After this function runs, the buffers may have lower rank, but still
// address the same memory as the original buffers. For "shift invariant" operations, operating on such fused buffers is
// likely to be more efficient. `dim_sets` is an optional span of integers that indicates sets of dimensions that are
// eligible for fusion. By default, all dimensions are considered to be part of the same set.
template <typename... Bufs>
void fuse_contiguous_dims(span<const int> dim_sets, raw_buffer& buf, Bufs&... bufs) {
for (std::ptrdiff_t d = static_cast<std::ptrdiff_t>(buf.rank) - 1; d > 0; --d) {
internal::attempt_fuse(d - 1, d, dim_sets, buf, bufs...);
}
}
template <typename... Bufs>
void fuse_contiguous_dims(raw_buffer& buf, Bufs&... bufs) {
for (std::ptrdiff_t d = static_cast<std::ptrdiff_t>(buf.rank) - 1; d > 0; --d) {
internal::attempt_fuse(d - 1, d, buf, bufs...);
}
}
// Call both `sort_dims` and `fuse_contiguous_dims` on the buffers.
template <typename... Bufs>
void optimize_dims(span<const int> dim_sets, raw_buffer& buf, Bufs&... bufs) {
// The order of operations here is for performance: It's a lot faster to fuse dimensions than sort them. So we fuse
// what we can before sorting, then if the sorting changed the order of the dimensions, attempt to fuse again.
fuse_contiguous_dims(dim_sets, buf, bufs...);
if (sort_dims(dim_sets, buf, bufs...)) {
fuse_contiguous_dims(dim_sets, buf, bufs...);
}
}
template <typename... Bufs>
void optimize_dims(raw_buffer& buf, Bufs&... bufs) {
fuse_contiguous_dims(buf, bufs...);
if (sort_dims(buf, bufs...)) {
fuse_contiguous_dims(buf, bufs...);
}
}
namespace internal {
template <std::size_t BufsSize>
SLINKY_ALWAYS_INLINE inline void increment_bases(std::size_t n, void** bases, const index_t* strides) {
n = BufsSize != dynamic_extent ? BufsSize : n;
bases[0] = offset_bytes(bases[0], strides[0]);
if (1 < n) bases[1] = offset_bytes(bases[1], strides[1]);
if (2 < n) bases[2] = offset_bytes(bases[2], strides[2]);
for (std::size_t i = 3; i < n; ++i) {
bases[i] = offset_bytes(bases[i], strides[i]);
}
}
template <typename... Ts, std::size_t... Is>
auto array_to_tuple(void** x, std::index_sequence<Is...>) {
return std::make_tuple(static_cast<Ts>(x[Is])...);
}
// The implementation of for_each_element involves quite a bit of code. To avoid code size problems, we implement it
// with type erased callbacks. To mitigate the overhead impact of this, the last linear loop is implemented in the
// callbacks without type erasure below.
using for_each_contiguous_slice_callback = function_ref<void(index_t, void**, index_t, const index_t*)>;
using for_each_element_callback = function_ref<void(void**, index_t, const index_t*)>;
template <std::size_t BufsSize>
void for_each_contiguous_slice_impl(span<const raw_buffer*, BufsSize> bufs, for_each_contiguous_slice_callback fn);
template <std::size_t BufsSize>
void for_each_element_impl(span<const raw_buffer*, BufsSize> bufs, for_each_element_callback fn);
// The above templates are only instantiated for a small number of sizes, up to this number. Larger values should use
// 0, which is handled by a runtime parameter instead of a compile-time constant.
static constexpr std::size_t max_bufs_size = 4;
} // namespace internal
// Call `f(index_t extent, T* base[, Ts* bases, ...])` for each contiguous slice in the domain of `buf[,
// bufs...]`. This function attempts to be efficient to support production quality implementations of callbacks.
//
// When additional buffers are passed, they will be sliced in tandem with the 'main' buffer. Additional buffers can be
// lower rank than the main buffer, these "missing" dimensions are not sliced (i.e. broadcasting in this dimension).
// If the other buffers are out of bounds for a slice, the corresponding argument to the callback will be `nullptr`.
template <typename Buf, typename F, typename... Bufs>
SLINKY_NO_STACK_PROTECTOR void for_each_contiguous_slice(const Buf& buf, const F& f, const Bufs&... bufs) {
constexpr std::size_t BufsSize = sizeof...(Bufs) + 1;
std::array<const raw_buffer*, BufsSize> buf_ptrs = {&buf, &bufs...};
constexpr std::size_t ConstBufsSize = BufsSize <= internal::max_bufs_size ? BufsSize : dynamic_extent;
internal::for_each_contiguous_slice_impl<ConstBufsSize>(
buf_ptrs, [&f](index_t slice_extent, void** bases, index_t extent, const index_t* strides) {
for (;;) {
std::apply(f, std::tuple_cat(std::make_tuple(slice_extent),
internal::array_to_tuple<typename Buf::pointer, typename Bufs::pointer...>(
bases, std::make_index_sequence<BufsSize>())));
if (SLINKY_UNLIKELY(--extent <= 0)) break;
internal::increment_bases<BufsSize>(0, bases, strides);
}
});
}
// Call `f` with a pointer to each element of `buf`, and pointers to the same corresponding elements of `bufs`, or
// `nullptr` if `buf` is out of bounds of `bufs`.
template <typename F, typename Buf, typename... Bufs>
SLINKY_NO_STACK_PROTECTOR void for_each_element(const F& f, const Buf& buf, const Bufs&... bufs) {
constexpr std::size_t BufsSize = sizeof...(Bufs) + 1;
std::array<const raw_buffer*, BufsSize> buf_ptrs = {&buf, &bufs...};
constexpr std::size_t ConstBufsSize = BufsSize <= internal::max_bufs_size ? BufsSize : dynamic_extent;
internal::for_each_element_impl<ConstBufsSize>(buf_ptrs, [&f](void** bases, index_t extent, const index_t* strides) {
for (;;) {
std::apply(f, internal::array_to_tuple<typename Buf::pointer, typename Bufs::pointer...>(
bases, std::make_index_sequence<BufsSize>()));
if (SLINKY_UNLIKELY(--extent <= 0)) break;
internal::increment_bases<BufsSize>(0, bases, strides);
}
});
}
} // namespace slinky
#endif // SLINKY_RUNTIME_BUFFER_H