blob: 3a75095d1fcb637755606a9c647816e160677c1b [file] [edit]
#include "runtime/buffer.h"
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <functional>
#include <limits>
#include "base/util.h"
namespace slinky {
namespace {
dim broadcast_dim(std::numeric_limits<index_t>::min(), std::numeric_limits<index_t>::max(), 0);
} // namespace
const dim& dim::broadcast() { return broadcast_dim; }
namespace {
index_t alloc_extent(const dim& dim) {
if (dim.fold_factor() != dim::unfolded) {
// TODO: We can do better than this if the dim doesn't cross a fold boundary.
return dim.fold_factor();
} else {
return dim.extent();
}
}
std::size_t alloc_size(std::size_t rank, std::size_t elem_size, const dim* dims) {
index_t flat_min = 0;
index_t flat_max = 0;
for (std::size_t i = 0; i < rank; ++i) {
if (dims[i].stride() == 0) continue;
index_t extent = alloc_extent(dims[i]);
assert(extent >= 0);
if (extent == 0) return 0;
flat_min += (extent - 1) * std::min<index_t>(0, dims[i].stride());
flat_max += (extent - 1) * std::max<index_t>(0, dims[i].stride());
}
return flat_max - flat_min + elem_size;
}
} // namespace
std::size_t raw_buffer::size_bytes() const { return alloc_size(rank, elem_size, dims); }
std::size_t raw_buffer::elem_count() const {
std::size_t result = 1;
for (std::size_t i = 0; i < rank; ++i) {
result *= std::max<index_t>(0, dims[i].extent());
}
return result;
}
raw_buffer_ptr raw_buffer::make(std::size_t rank, std::size_t elem_size, const class dim* dims) {
const std::size_t data_size = rank == 0 || dims ? alloc_size(rank, elem_size, dims) : 0;
const std::size_t size = sizeof(raw_buffer) + sizeof(slinky::dim) * rank + data_size;
char* mem = reinterpret_cast<char*>(malloc(size));
raw_buffer* buf = new (mem) raw_buffer();
mem += sizeof(raw_buffer);
buf->elem_size = elem_size;
buf->rank = rank;
buf->dims = reinterpret_cast<slinky::dim*>(mem);
if (rank > 0 && dims) {
internal::copy_small_n(dims, rank, buf->dims);
mem += sizeof(slinky::dim) * rank;
} else {
new (buf->dims) slinky::dim[buf->rank];
}
buf->base = data_size > 0 ? mem : nullptr;
return raw_buffer_ptr(buf, free);
}
raw_buffer_ptr raw_buffer::make_copy(const raw_buffer& src) {
auto buf = make(src.rank, src.elem_size, src.dims);
copy(src, *buf);
return buf;
}
raw_buffer_ptr raw_buffer::make_scalar(std::size_t elem_size, const void* value) {
auto buf = make(0, elem_size);
memcpy(buf->base, value, elem_size);
return buf;
}
namespace {
// This algorithm is the same idea as [1], but here it does not need to attempt to support compile-time constant
// strides, which makes it a lot easier to implement.
//
// 1. https://github.com/dsharlet/array/blob/38f8ce332fc4e26af08325ad0654c8516a445e8c/include/array/array.h#L835-L907
// A proposed stride is "OK" w.r.t. `dim` if the proposed stride does not cause this dimension's memory to overlap with
// any other dimension's memory.
struct init_stride_dim {
index_t stride;
index_t dim_stride; // stride * extent
init_stride_dim(index_t stride, index_t extent) : stride(stride), dim_stride(stride * extent) {}
bool operator<(const init_stride_dim& r) const { return dim_stride < r.dim_stride; }
};
SLINKY_ALWAYS_INLINE inline bool is_stride_ok(index_t stride, index_t extent, span<const init_stride_dim> dims) {
const index_t dim_stride = stride * extent;
for (const init_stride_dim& d : dims) {
if (d.stride >= dim_stride) {
// The dim is completely outside the proposed stride.
} else if (d.dim_stride <= stride) {
// The dim is completely inside the proposed stride.
} else {
return false;
}
}
return true;
}
} // namespace
std::size_t raw_buffer::init_strides(index_t alignment) {
// We remember the strides of the dims we know about, in sorted order.
init_stride_dim* dims = SLINKY_ALLOCA(init_stride_dim, rank);
init_stride_dim* dims_end = dims;
// Insert d into dims, sorted by dim_stride. Also track the flat max index of the buffer, to compute the size.
std::size_t flat_max = 0;
auto learn_dim = [&](const init_stride_dim& d) {
init_stride_dim* at = std::lower_bound(dims, dims_end, d);
internal::copy_small_n_backward(dims_end, dims_end - at, dims_end + 1);
*at = d;
flat_max += d.dim_stride - d.stride;
++dims_end;
};
std::size_t unknown_begin = rank;
std::size_t unknown_end = 0;
for (std::size_t i = 0; i < rank; ++i) {
if (dim(i).stride() == 0) continue;
index_t alloc_extent_i = alloc_extent(dim(i));
if (alloc_extent_i <= 1) {
// The buffer is empty or has extent 1, we don't care about the stride.
if (dim(i).stride() == dim::auto_stride) dim(i).set_stride(elem_size);
} else if (dim(i).stride() != dim::auto_stride) {
learn_dim(init_stride_dim(std::abs(dim(i).stride()), alloc_extent_i));
} else {
// Track the range of dimensions we need to find the stride of.
unknown_begin = std::min(unknown_begin, i);
unknown_end = i + 1;
}
}
for (std::size_t i = unknown_begin; i < unknown_end; ++i) {
if (dim(i).stride() != dim::auto_stride) continue;
const index_t alloc_extent_i = alloc_extent(dim(i));
assert(alloc_extent_i > 1);
span<const init_stride_dim> known_dims{dims, dims_end};
if (is_stride_ok(elem_size, alloc_extent_i, known_dims)) {
// This dimension can have stride elem_size, no other stride could be better.
dim(i).set_stride(elem_size);
learn_dim(init_stride_dim(elem_size, alloc_extent_i));
continue;
}
// Loop through all the dimensions and see if a stride that is just outside any dimension is OK.
for (const init_stride_dim& dim_j : known_dims) {
const index_t candidate = (dim_j.dim_stride + (alignment - 1)) & ~(alignment - 1);
if (&dim_j == &known_dims.back() || is_stride_ok(candidate, alloc_extent_i, known_dims)) {
dim(i).set_stride(candidate);
learn_dim(init_stride_dim(candidate, alloc_extent_i));
// The dims are sorted, so no subsequent candidate will be better.
break;
}
}
assert(dim(i).stride() != dim::auto_stride);
}
return (flat_max + elem_size + (alignment - 1)) & ~(alignment - 1);
}
void* raw_buffer::allocate(index_t alignment) {
std::size_t size = init_strides(alignment);
void* allocation = malloc(size + alignment - 1);
base = align_up(allocation, alignment);
return allocation;
}
namespace {
// Returns true if `value` is `size` repeats of the same byte. This probably will only ever be used for fills of 0.
bool is_repeated_byte(const void* value, std::size_t size) {
const char* bytes = reinterpret_cast<const char*>(value);
for (std::size_t i = 1; i < size; ++i) {
if (bytes[0] != bytes[i]) {
// The value is not the same repeated byte.
return false;
}
}
return true;
}
void fill(void* dst, const void* value, index_t elem_size, index_t size) {
if (elem_size == 1) {
memset(dst, *reinterpret_cast<const uint8_t*>(value), size);
} else {
assert(elem_size > 0);
while (size >= elem_size) {
memcpy(dst, value, elem_size);
dst = offset_bytes_non_null(dst, elem_size);
size -= elem_size;
}
// The elem_size might not divide the size if optimize_fill_value replicated it.
memcpy(dst, value, size);
}
}
// When copying broadcasted buffers, it would be slow if we copied each element one at a time. We can avoid this by
// duplicating the value into a buffer and copying from that larger buffer instead.
using fill_value_buffer = std::array<uint8_t, 64>;
void optimize_fill_value(const void*& value, index_t& elem_size, index_t fill_size, fill_value_buffer& buffer) {
if (is_repeated_byte(value, elem_size)) {
// This value can be filled with memset.
elem_size = 1;
return;
}
assert(elem_size != 1);
const index_t target_size = std::min<index_t>(buffer.size(), fill_size);
if (elem_size * 2 <= target_size) {
// Repeatedly duplicate the constant as long as it fits in the buffer.
memcpy(buffer.data(), value, elem_size);
while (elem_size * 2 <= target_size) {
memcpy(&buffer[elem_size], buffer.data(), elem_size);
elem_size *= 2;
}
value = buffer.data();
}
}
const slinky::dim& slice_dim0(raw_buffer& buf) {
if (buf.rank > 0) {
buf.rank--;
return *buf.dims++;
} else {
return dim::broadcast();
}
}
void unslice_dim0(raw_buffer& buf, const slinky::dim& d) {
if (&d + 1 == buf.dims) {
buf.rank++;
buf.dims--;
} else {
// The dimension didn't come from this buffer (rank 0 broadcast).
}
}
// Perform an unpadded copy.
void copy_impl(raw_buffer& src, raw_buffer& dst) {
assert(src.elem_size == dst.elem_size);
assert(dst.base || dst.elem_count() == 0);
index_t elem_size = dst.elem_size;
if (dst.rank == 0) {
memcpy(dst.base, src.base, elem_size);
} else {
const slinky::dim& dst_dim0 = dst.dim(0);
const slinky::dim& src_dim0 = src.rank > 0 ? src.dim(0) : dim::broadcast();
if (dst_dim0.empty()) {
// Empty destination, nothing to do.
} else if (dst_dim0.fold_factor() != dim::unfolded || src_dim0.fold_factor() != dim::unfolded ||
dst_dim0.stride() != elem_size || (src_dim0.stride() != 0 && src_dim0.stride() != elem_size)) {
// There is some complication to the innermost dimension's copy.
for_each_contiguous_slice(
dst, [elem_size](index_t extent, void* dst, const void* src) { memcpy(dst, src, extent * elem_size); }, src);
} else {
slice_dim0(dst);
slice_dim0(src);
const index_t dst_size = dst_dim0.extent() * elem_size;
if (src_dim0.stride() == 0) {
// The inner dimension is a fill call.
fill_value_buffer buffer;
const void* buffer_value;
index_t buffer_elem_size;
const void* cached_src = nullptr;
for_each_element(
[&](void* dst, const void* src) {
if (cached_src != src) {
cached_src = src;
buffer_elem_size = elem_size;
buffer_value = src;
optimize_fill_value(buffer_value, buffer_elem_size, dst_size, buffer);
}
fill(dst, buffer_value, buffer_elem_size, dst_size);
},
dst, src);
} else {
// The inner dimension is a memcpy.
assert(src_dim0.stride() == elem_size);
assert(src_dim0.begin() <= dst_dim0.begin());
assert(src_dim0.end() >= dst_dim0.end());
void* src_base = src.base;
src.base = offset_bytes(src.base, src_dim0.flat_offset_bytes(dst_dim0.min()));
for_each_element([=](void* dst, const void* src) { memcpy(dst, src, dst_size); }, dst, src);
src.base = src_base;
}
unslice_dim0(dst, dst_dim0);
unslice_dim0(src, src_dim0);
}
}
}
// The strategy used here for padding is to start with the last dimension (which typically has the largest stride), and
// fill the padded area. Once this padded area is filled, it can be cropped from the buffer, which reduces the area
// the next dimension needs to pad. Proceeding in this order minimizes the area we need to fill with small stride, which
// is where this is slow (because we can only copy one or a few elements at a time).
// This function copies `pad` to `dst` where `dst` is out of bounds of `src` in dimension `d`, and then crops `dst` such
// that only the unpadded area remains in dimension `d`.
void pad_impl(raw_buffer& src, raw_buffer& dst, raw_buffer& pad) {
for (int d = static_cast<int>(std::min(src.rank, dst.rank)) - 1; d >= 0; --d) {
const slinky::dim& src_d = src.dim(d);
// TODO: Try to implement this without saving and restoring the dst buffer between each crop.
void* dst_base = dst.base;
slinky::dim dst_d = dst.dim(d);
if (dst_d.min() < src_d.min()) {
// There's padding before the min in this dimension.
dst.crop(d, dst_d.min(), src_d.min() - 1);
copy_impl(pad, dst);
dst.base = dst_base;
dst.dim(d) = dst_d;
}
if (dst_d.max() > src_d.max()) {
// There's padding after the max in this dimension.
dst.crop(d, src_d.max() + 1, dst_d.max());
copy_impl(pad, dst);
dst.base = dst_base;
dst.dim(d) = dst_d;
}
// Crop off the padded areas we've filled in this dimension.
dst.crop(d, src_d.min(), src_d.max());
// If the src was bigger, crop it, so the bounds should match.
src.crop(d, dst_d.min(), dst_d.max());
}
}
} // namespace
SLINKY_NO_STACK_PROTECTOR void copy(const raw_buffer& src, const raw_buffer& dst, const raw_buffer* pad) {
assert(dst.elem_size == src.elem_size);
assert(dst.base || dst.elem_count() == 0);
if (dst.rank == 0) {
assert(src.base || (pad && pad->base));
memcpy(dst.base, !src.base && pad ? pad->base : src.base, dst.elem_size);
return;
}
// Make (shallow) copies of the buffers, so we can optimize the dimensions.
raw_buffer dst_opt = dst;
dst_opt.dims = SLINKY_ALLOCA(dim, dst.rank);
internal::copy_small_n(dst.dims, dst.rank, dst_opt.dims);
raw_buffer src_opt = src;
src_opt.dims = SLINKY_ALLOCA(dim, src.rank);
internal::copy_small_n(src.dims, src.rank, src_opt.dims);
// If the src has rank 0, then the padding is irrelevant, nothing is out of bounds.
if (src_opt.rank > 0 && pad && pad->base) {
assert(dst_opt.elem_size == pad->elem_size);
raw_buffer pad_opt = *pad;
pad_opt.dims = SLINKY_ALLOCA(dim, pad->rank);
internal::copy_small_n(pad->dims, pad->rank, pad_opt.dims);
optimize_dims(dst_opt, src_opt, pad_opt);
// Implement the padding in all but the first dimension.
pad_impl(src_opt, dst_opt, pad_opt);
if (src_opt.base == dst_opt.base) {
// This is an in-place padded copy, we're done.
return;
}
} else {
optimize_dims(dst_opt, src_opt);
}
copy_impl(src_opt, dst_opt);
}
void pad(const dim* in_bounds, const raw_buffer& dst, const raw_buffer& pad) {
assert(dst.elem_size == pad.elem_size);
if (dst.rank == 0) {
return;
}
// To implement pad, we'll make a buffer that looks like dst, but cropped to the bounds, and copy it with pad.
raw_buffer dst_opt = dst;
dst_opt.dims = SLINKY_ALLOCA(dim, dst.rank);
internal::copy_small_n(dst.dims, dst.rank, dst_opt.dims);
raw_buffer src = {nullptr, dst.elem_size, dst.rank, SLINKY_ALLOCA(dim, dst.rank)};
for (std::size_t d = 0; d < dst.rank; ++d) {
src.dim(d) = {in_bounds[d].min(), in_bounds[d].max(), 0, dim::unfolded};
}
raw_buffer pad_opt = pad;
pad_opt.dims = SLINKY_ALLOCA(dim, pad.rank);
internal::copy_small_n(pad.dims, pad.rank, pad_opt.dims);
optimize_dims(dst_opt, src, pad_opt);
// Implement the padding in all but the first dimension.
pad_impl(src, dst_opt, pad_opt);
}
namespace internal {
namespace {
template <std::size_t BufsSize>
SLINKY_ALWAYS_INLINE inline bool is_contiguous_slice(span<const raw_buffer*, BufsSize> bufs, std::size_t d) {
const raw_buffer& buf = *bufs[0];
if (buf.dim(d).stride() != static_cast<index_t>(buf.elem_size)) {
// This dimension is not contiguous.
return false;
}
for (std::size_t n = 1; n < bufs.size(); n++) {
const raw_buffer& buf_n = *bufs[n];
if (&buf_n == &buf || !buf_n.base) {
// This is the same buffer as the base, or the base pointer is nullptr.
continue;
} else if (d >= buf_n.rank) {
// This dimension is broadcasted, it's not contiguous.
return false;
} else if (buf_n.dim(d).stride() != static_cast<index_t>(buf_n.elem_size)) {
// This dimension is not contiguous.
return false;
}
}
return true;
}
template <std::size_t BufsSize>
SLINKY_ALWAYS_INLINE inline bool can_fuse(span<const raw_buffer*, BufsSize> bufs, std::size_t d) {
assert(d > 0);
const raw_buffer& buf = *bufs[0];
const dim& base_inner = buf.dim(d - 1);
const dim& base_outer = buf.dim(d);
const index_t inner_extent = base_inner.extent();
if (base_inner.stride() * inner_extent != base_outer.stride()) {
// The dimensions are not contiguous in memory.
return false;
}
if (base_outer.stride() != 0 && base_inner.fold_factor() != dim::unfolded) {
// One of the dimensions is folded.
return false;
}
for (std::size_t n = 1; n < bufs.size(); n++) {
const raw_buffer& buf_n = *bufs[n];
if (&buf_n == &buf || !buf_n.base) {
// This is the same buffer as the base, or the base pointer is nullptr.
continue;
}
const std::size_t rank = buf_n.rank;
if (d > rank) {
// Both dimensions are broadcasts, they can be fused.
continue;
}
const dim& inner = buf_n.dim(d - 1);
if (inner.min() != base_inner.min() || inner.max() != base_inner.max()) {
// The bounds of the inner dimension are not equal.
return false;
}
const index_t outer_stride = d < rank ? buf_n.dim(d).stride() : 0;
if (inner.stride() * inner_extent != outer_stride) {
// The dimensions are not contiguous in memory.
return false;
}
if (outer_stride != 0 && inner.fold_factor() != dim::unfolded) {
// One of the dimensions is folded.
return false;
}
}
return true;
}
// We need a non-linear loop if we can't compute a pointer for any dimension via multiplying the stride and adding it to
// the beginning of the dimension:
// - A dimension is folded
// - A dimension is partially out of bounds
template <std::size_t BufsSize>
SLINKY_ALWAYS_INLINE inline bool use_nonlinear_loop(span<const raw_buffer*, BufsSize> bufs, std::size_t d) {
const raw_buffer& buf = *bufs[0];
const dim& buf_dim = buf.dim(d);
if (buf_dim.is_folded()) {
// The main buffer is folded.
return true;
}
for (std::size_t n = 1; n < bufs.size(); ++n) {
const raw_buffer& buf_n = *bufs[n];
if (&buf_n == &buf || !buf_n.base) {
// This is the same buffer as the base, or the base pointer is nullptr.
continue;
} else if (d >= buf_n.rank) {
// Broadcast dimension.
continue;
}
const dim& buf_n_dim = buf_n.dim(d);
if (buf_n_dim.is_folded(buf_dim)) {
// There's a folded buffer, we need a folded loop.
return true;
} else if (!buf_n_dim.contains(buf_dim)) {
// One of the extra buffers is out of bounds, use a folded loop.
return true;
}
}
return false;
}
template <std::size_t BufsSize = dynamic_extent>
struct for_each_loop;
// These functions may modify the first parameter in-place.
// TODO: The last parameter (slice_extent) is unused by `for_each_element_callback`. Find a way to eliminate the
// parameter in that case. Adding a template parameter to everything affected to control this is painful.
template <std::size_t BufsSize>
using for_each_loop_impl = void (*)(mutable_span<void*, BufsSize>, const for_each_loop<BufsSize>*, index_t);
template <std::size_t BufsSize>
struct for_each_loop {
union {
// For linear loops, the extent of the loop.
index_t extent;
// For nonlinear loops, the intersection of all the folds in this loop.
index_t fold_factor;
};
for_each_loop_impl<BufsSize> impl;
union {
index_t strides[1]; // [bufs_size]
const dim* dims[1]; // [bufs_size]
};
};
std::ptrdiff_t sizeof_for_each_loop(std::size_t bufs_size) {
return sizeof(for_each_loop<>) - sizeof(for_each_loop<>::dims) + sizeof(for_each_loop<>::dims) * bufs_size;
}
// We store a plan for a parallel for loop in a structure of the following layout, for N buffers and rank R loops:
// for_each_loop<N> loops[R];
// F f;
//
// We can't make a simple struct for this, because N and R are not necessarily compile-time constants.
template <typename F>
SLINKY_ALWAYS_INLINE inline std::size_t size_of_plan(std::size_t bufs_size, std::size_t rank) {
// We only need max(rank, 1) for each loops, but it only wastes a little stack and it's cheaper to compute the add.
return sizeof_for_each_loop(bufs_size) * (rank + 1) + sizeof(F);
}
// Compile-time dispatch to either for_each_contiguous_slice_callback or for_each_element_callback.
SLINKY_ALWAYS_INLINE inline void call_f(
for_each_element_callback f, void** bases, index_t extent, const index_t* strides, index_t slice_extent) {
assert(slice_extent == 1);
f(bases, extent, strides);
}
SLINKY_ALWAYS_INLINE inline void call_f(
for_each_contiguous_slice_callback f, void** bases, index_t extent, const index_t* strides, index_t slice_extent) {
f(slice_extent, bases, extent, strides);
}
template <typename F, std::size_t BufsSize>
void for_each_impl_call_f(
mutable_span<void*, BufsSize> bases, const for_each_loop<BufsSize>* loop, index_t slice_extent) {
const F& f = *reinterpret_cast<const F*>(offset_bytes_non_null(loop, sizeof_for_each_loop(bases.size())));
call_f(f, bases.data(), loop->extent, loop->strides, slice_extent);
}
template <std::size_t BufsSize>
SLINKY_NO_STACK_PROTECTOR void call_impl_linear(index_t extent, mutable_span<void*, BufsSize> bases,
const for_each_loop<BufsSize>* loop, const index_t* strides, index_t slice_extent) {
assert(extent >= 1);
for_each_loop_impl<BufsSize> impl = loop->impl;
void** bases_i = SLINKY_ALLOCA(void*, bases.size());
for (;;) {
copy_small_n(bases.data(), bases.size(), bases_i);
impl({bases_i, bases.size()}, loop, slice_extent);
if (SLINKY_UNLIKELY(--extent <= 0)) break;
increment_bases<BufsSize>(bases.size(), bases.data(), strides);
}
}
template <typename F, std::size_t BufsSize>
SLINKY_NO_STACK_PROTECTOR void for_each_impl_linear(
mutable_span<void*, BufsSize> bases, const for_each_loop<BufsSize>* loop, index_t slice_extent) {
const index_t* strides = loop->strides;
index_t extent = loop->extent;
assert(extent >= 1);
loop = offset_bytes_non_null(loop, sizeof_for_each_loop(bases.size()));
call_impl_linear(extent, bases, loop, strides, slice_extent);
}
template <typename F, std::size_t BufsSize, bool CallF, bool Contiguous>
SLINKY_NO_STACK_PROTECTOR void for_each_impl_nonlinear(
mutable_span<void*, BufsSize> bases, const for_each_loop<BufsSize>* loop, index_t slice_extent) {
const dim* const* dims = loop->dims;
const index_t fold_factor = loop->fold_factor;
loop = offset_bytes_non_null(loop, sizeof_for_each_loop(bases.size()));
const F& f = *reinterpret_cast<const F*>(loop);
index_t* strides = SLINKY_ALLOCA(index_t, bases.size());
for (std::size_t n = 0; n < bases.size(); ++n) {
strides[n] = dims[n]->stride();
}
const dim& dim_0 = *dims[0];
void** bases_i = SLINKY_ALLOCA(void*, bases.size());
// To handle non-linear loops, we process an interval [min, max] in blocks of the `fold_factor`, within which we can
// compute the base pointers linearly from the strides, if the buffers are fully in-bounds or out-of-bounds.
// We need to handle buffers going in and out of bounds too, so we break the blocks into smaller chunks at those
// boundaries.
auto run_one_fold = [&](index_t min, index_t max) {
for (index_t i = min; i <= max;) {
index_t max_i = max;
assert(!dim_0.is_folded(i, max_i));
bases_i[0] = offset_bytes_non_null(bases[0], dim_0.flat_offset_bytes(i));
for (std::size_t n = 1; n < bases.size(); n++) {
if (!bases[n]) {
bases_i[n] = nullptr;
continue;
}
const dim& dim_n = *dims[n];
assert(!dim_n.is_folded(i, max_i));
if (dim_n.contains(i)) {
// This interval starts out non-null, but could become null before the end of the interval.
bases_i[n] = offset_bytes_non_null(bases[n], dim_n.flat_offset_bytes(i));
max_i = std::min(max_i, dim_n.max());
} else {
bases_i[n] = nullptr;
if (dim_n.min() > i) {
// This interval starts out null, but could become non-null before the end of the interval.
max_i = std::min(max_i, dim_n.min() - 1);
}
}
}
index_t extent_i = Contiguous ? 1 : max_i - i + 1;
index_t slice_extent_i = (Contiguous ? (max_i - i + 1) : 1) * slice_extent;
if (CallF) {
// If the next step is to call f, do that eagerly here to avoid an extra call.
call_f(f, bases_i, extent_i, strides, slice_extent_i);
} else {
call_impl_linear<BufsSize>(extent_i, {bases_i, bases.size()}, loop, strides, slice_extent_i);
}
i = max_i + 1;
}
};
index_t min = dim_0.min();
index_t max = dim_0.max();
if (fold_factor == dim::unfolded) {
// Not folded, we can treat the whole range as one chunk.
run_one_fold(min, max);
} else {
index_t first_fold = align_up(min, fold_factor);
if (min != first_fold) {
// Handle the partial fold before the first fold boundary.
run_one_fold(min, std::min(first_fold - 1, max));
}
for (index_t i = first_fold; i <= max; i += fold_factor) {
// Process up to the end without crossing a fold boundary.
run_one_fold(i, std::min(i + fold_factor - 1, max));
}
}
}
index_t gcd_fold_factor(index_t a, index_t b) {
if (a == dim::unfolded) return b;
if (b == dim::unfolded) return a;
return gcd(a, b);
}
template <bool SkipContiguous, std::size_t BufsSize, typename F>
SLINKY_NO_STACK_PROTECTOR SLINKY_ALWAYS_INLINE inline void for_each_impl(span<const raw_buffer*, BufsSize> bufs, F f) {
const raw_buffer& buf = *bufs[0];
auto* loop = reinterpret_cast<for_each_loop<BufsSize>*>(SLINKY_ALLOCA(char, size_of_plan<F>(bufs.size(), buf.rank)));
void** bases = SLINKY_ALLOCA(void*, bufs.size());
bases[0] = buf.base;
for (std::size_t n = 1; n < bufs.size(); ++n) {
bases[n] = bufs[n]->base;
}
for_each_loop_impl<BufsSize> inner_impl;
for_each_loop<BufsSize>* outer_loop = loop;
index_t slice_extent = 1;
index_t extent = 1;
for (std::ptrdiff_t d = static_cast<std::ptrdiff_t>(buf.rank) - 1; d >= 0; --d) {
const dim& buf_dim = buf.dim(d);
if (buf_dim.min() == buf_dim.max()) {
// extent 1, we don't need any of the logic here, skip to below.
} else if (buf_dim.max() < buf_dim.min() || use_nonlinear_loop(bufs, d)) {
// extent > 1 and there is a folded dimension in one of the buffers, or we need to crop one of the buffers, or the
// loops are empty.
if (SkipContiguous && is_contiguous_slice(bufs, d)) {
loop->impl = for_each_impl_nonlinear<F, BufsSize, false, true>;
inner_impl = for_each_impl_nonlinear<F, BufsSize, true, true>;
} else {
loop->impl = for_each_impl_nonlinear<F, BufsSize, false, false>;
inner_impl = for_each_impl_nonlinear<F, BufsSize, true, false>;
}
extent = 1;
const dim** dims = loop->dims;
dims[0] = &buf.dim(d);
loop->fold_factor = buf_dim.fold_factor();
for (std::size_t n = 1; n < bufs.size(); n++) {
const raw_buffer& buf_n = *bufs[n];
if (d < static_cast<std::ptrdiff_t>(buf_n.rank)) {
const dim& buf_n_dim = buf_n.dim(d);
dims[n] = &buf_n_dim;
loop->fold_factor = gcd_fold_factor(loop->fold_factor, buf_n_dim.fold_factor());
} else {
dims[n] = &broadcast_dim;
}
}
loop = offset_bytes_non_null(loop, sizeof_for_each_loop(bufs.size()));
continue;
} else {
// Use a linear, possibly fused loop below.
extent *= buf_dim.max() - buf_dim.min() + 1;
}
// Align the bases for dimensions we will access via linear pointer arithmetic.
if (buf_dim.fold_factor() != dim::unfolded) {
// This function is expected to adjust all bases to point to the min of `buf_dim`. For non-folded dimensions, that
// is true by construction, but not for folded dimensions.
index_t offset = buf_dim.flat_offset_bytes(buf_dim.min());
bases[0] = offset_bytes_non_null(bases[0], offset);
}
for (std::size_t n = 1; n < bufs.size(); n++) {
const raw_buffer& buf_n = *bufs[n];
if (SLINKY_LIKELY(bases[n] && d < static_cast<std::ptrdiff_t>(buf_n.rank))) {
const dim& buf_n_dim = buf_n.dim(d);
if (SLINKY_LIKELY(buf_n_dim.contains(buf_dim))) {
index_t offset = buf_n_dim.flat_offset_bytes(buf_dim.min());
bases[n] = offset_bytes_non_null(bases[n], offset);
} else {
// If we got here, we need to say the buffer is always out of bounds. If it is partially out of bounds,
// use_nonlinear_loop should have returned true above.
assert(buf_n_dim.empty() || buf_n_dim.min() > buf_dim.max() || buf_n_dim.max() < buf_dim.min());
bases[n] = nullptr;
}
}
}
if (extent == 1 || (d > 0 && can_fuse(bufs, d))) {
// Let this fuse with the next dimension.
} else if (SkipContiguous && is_contiguous_slice(bufs, d)) {
// This is the slice dimension.
slice_extent *= extent;
extent = 1;
} else {
// For the "output" buf, we can't cross a fold boundary, which means we can treat it as linear.
assert(!buf_dim.is_folded());
loop->extent = extent;
extent = 1;
loop->impl = for_each_impl_linear<F, BufsSize>;
inner_impl = for_each_impl_call_f<F, BufsSize>;
index_t* strides = loop->strides;
strides[0] = buf_dim.stride();
for (std::size_t n = 1; n < bufs.size(); n++) {
const raw_buffer& buf_n = *bufs[n];
strides[n] = d < static_cast<std::ptrdiff_t>(buf_n.rank) ? buf_n.dim(d).stride() : 0;
}
loop = offset_bytes_non_null(loop, sizeof_for_each_loop(bufs.size()));
}
}
if (loop == outer_loop) {
// There are no loops, just call f. This is an edge case below branch which assumes there is at least one loop.
call_f(f, bases, 1, nullptr, slice_extent);
} else {
// Put the callback at the end of the plan, where the inner loop expects to find it.
*reinterpret_cast<F*>(loop) = f;
// We need to replace the implementation of the last loop.
for_each_loop<BufsSize>* inner_loop = offset_bytes_non_null(loop, -sizeof_for_each_loop(bufs.size()));
(void)inner_impl;
inner_loop->impl = inner_impl;
// Run the outer loop.
outer_loop->impl({bases, bufs.size()}, outer_loop, slice_extent);
}
}
} // namespace
template <std::size_t BufsSize>
SLINKY_NO_STACK_PROTECTOR void for_each_contiguous_slice_impl(
span<const raw_buffer*, BufsSize> bufs, for_each_contiguous_slice_callback f) {
for_each_impl<true, BufsSize>(bufs, f);
}
template <size_t BufsSize>
SLINKY_NO_STACK_PROTECTOR void for_each_element_impl(
span<const raw_buffer*, BufsSize> bufs, for_each_element_callback f) {
for_each_impl<false, BufsSize>(bufs, f);
}
// These are templates defined in an implementation file, explicitly instantiate the templates we want to exist.
template void for_each_contiguous_slice_impl<dynamic_extent>(
span<const raw_buffer*> bufs, for_each_contiguous_slice_callback f);
template void for_each_contiguous_slice_impl<1>(span<const raw_buffer*, 1> bufs, for_each_contiguous_slice_callback f);
template void for_each_contiguous_slice_impl<2>(span<const raw_buffer*, 2> bufs, for_each_contiguous_slice_callback f);
template void for_each_contiguous_slice_impl<3>(span<const raw_buffer*, 3> bufs, for_each_contiguous_slice_callback f);
template void for_each_contiguous_slice_impl<4>(span<const raw_buffer*, 4> bufs, for_each_contiguous_slice_callback f);
template void for_each_element_impl<dynamic_extent>(span<const raw_buffer*> bufs, for_each_element_callback f);
template void for_each_element_impl<1>(span<const raw_buffer*, 1> bufs, for_each_element_callback f);
template void for_each_element_impl<2>(span<const raw_buffer*, 2> bufs, for_each_element_callback f);
template void for_each_element_impl<3>(span<const raw_buffer*, 3> bufs, for_each_element_callback f);
template void for_each_element_impl<4>(span<const raw_buffer*, 4> bufs, for_each_element_callback f);
} // namespace internal
} // namespace slinky