blob: 6e2c2ec481a4bd9376464ab698b00665d01dd4fd [file]
/*
pybind11/detail/argument_vector.h: small_vector-like containers to
avoid heap allocation of arguments during function call dispatch.
Copyright (c) Meta Platforms, Inc. and affiliates.
All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE file.
*/
#pragma once
#include <pybind11/pytypes.h>
#include "common.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iterator>
#include <type_traits>
#include <utility>
#include <vector>
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
PYBIND11_WARNING_DISABLE_MSVC(4127)
PYBIND11_NAMESPACE_BEGIN(detail)
// Shared implementation utility for our small_vector-like containers.
// We support C++11 and C++14, so we cannot use
// std::variant. Union with the tag packed next to the inline
// array's size is smaller anyway, allowing 1 extra handle of
// inline storage for free. Compare the layouts (1 line per
// size_t/void*, assuming a 64-bit machine):
// With variant, total is N + 2 for N >= 2:
// - variant tag (cannot be packed with the array size)
// - array size (or first pointer of 3 in std::vector)
// - N pointers of inline storage (or 2 remaining pointers of std::vector)
// Custom union, total is N + 1 for N >= 3:
// - variant tag & array size if applicable
// - N pointers of inline storage (or 3 pointers of std::vector)
//
// NOTE: this is a low-level representational convenience; the two
// use cases of this union are materially different and in particular
// have different semantics for inline_array::size. All that is being
// shared is the memory management behavior.
template <typename ArrayT, std::size_t InlineSize, typename VectorT = ArrayT>
union inline_array_or_vector {
struct inline_array {
bool is_inline = true;
std::uint32_t size = 0;
std::array<ArrayT, InlineSize> arr;
};
struct heap_vector {
bool is_inline = false;
std::vector<VectorT> vec;
heap_vector() = default;
heap_vector(std::size_t count, VectorT value) : vec(count, value) {}
};
inline_array iarray;
heap_vector hvector;
inline_array_or_vector() : iarray() {}
~inline_array_or_vector() {
if (is_inline()) {
iarray.~inline_array();
} else {
hvector.~heap_vector();
}
}
// Disable copy ctor and assignment.
inline_array_or_vector(const inline_array_or_vector &) = delete;
inline_array_or_vector &operator=(const inline_array_or_vector &) = delete;
inline_array_or_vector(inline_array_or_vector &&rhs) noexcept {
if (rhs.is_inline()) {
new (&iarray) inline_array(std::move(rhs.iarray));
} else {
new (&hvector) heap_vector(std::move(rhs.hvector));
}
assert(is_inline() == rhs.is_inline());
}
inline_array_or_vector &operator=(inline_array_or_vector &&rhs) noexcept {
if (this == &rhs) {
return *this;
}
if (is_inline()) {
iarray.~inline_array();
} else {
hvector.~heap_vector();
}
if (rhs.is_inline()) {
new (&iarray) inline_array(std::move(rhs.iarray));
} else {
new (&hvector) heap_vector(std::move(rhs.hvector));
}
return *this;
}
bool is_inline() const {
// It is undefined behavior to access the inactive member of a
// union directly. However, it is well-defined to reinterpret_cast any
// pointer into a pointer to char and examine it as an array
// of bytes. See
// https://dev-discuss.pytorch.org/t/unionizing-for-profit-how-to-exploit-the-power-of-unions-in-c/444#the-memcpy-loophole-4
bool result = false;
static_assert(offsetof(inline_array, is_inline) == 0,
"untagged union implementation relies on this");
static_assert(offsetof(heap_vector, is_inline) == 0,
"untagged union implementation relies on this");
std::memcpy(&result, reinterpret_cast<const char *>(this), sizeof(bool));
return result;
}
};
template <typename T, std::size_t InlineSize>
struct small_vector {
public:
small_vector() = default;
// Disable copy ctor and assignment.
small_vector(const small_vector &) = delete;
small_vector &operator=(const small_vector &) = delete;
small_vector(small_vector &&) noexcept = default;
small_vector &operator=(small_vector &&) noexcept = default;
std::size_t size() const {
if (is_inline()) {
return m_repr.iarray.size;
}
return m_repr.hvector.vec.size();
}
T const *data() const {
if (is_inline()) {
return m_repr.iarray.arr.data();
}
return m_repr.hvector.vec.data();
}
T &operator[](std::size_t idx) {
assert(idx < size());
if (is_inline()) {
return m_repr.iarray.arr[idx];
}
return m_repr.hvector.vec[idx];
}
T const &operator[](std::size_t idx) const {
assert(idx < size());
if (is_inline()) {
return m_repr.iarray.arr[idx];
}
return m_repr.hvector.vec[idx];
}
void push_back(const T &x) { emplace_back(x); }
void push_back(T &&x) { emplace_back(std::move(x)); }
template <typename... Args>
void emplace_back(Args &&...x) {
if (is_inline()) {
auto &ha = m_repr.iarray;
if (ha.size == InlineSize) {
move_to_heap_vector_with_reserved_size(InlineSize + 1);
m_repr.hvector.vec.emplace_back(std::forward<Args>(x)...);
} else {
ha.arr[ha.size++] = T(std::forward<Args>(x)...);
}
} else {
m_repr.hvector.vec.emplace_back(std::forward<Args>(x)...);
}
}
void reserve(std::size_t sz) {
if (is_inline()) {
if (sz > InlineSize) {
move_to_heap_vector_with_reserved_size(sz);
}
} else {
reserve_slow_path(sz);
}
}
private:
using repr_type = inline_array_or_vector<T, InlineSize>;
repr_type m_repr;
PYBIND11_NOINLINE void move_to_heap_vector_with_reserved_size(std::size_t reserved_size) {
assert(is_inline());
auto &ha = m_repr.iarray;
using heap_vector = typename repr_type::heap_vector;
heap_vector hv;
hv.vec.reserve(reserved_size);
static_assert(std::is_nothrow_move_constructible<T>::value,
"this conversion is not exception safe");
static_assert(std::is_nothrow_move_constructible<heap_vector>::value,
"this conversion is not exception safe");
std::move(ha.arr.begin(), ha.arr.begin() + ha.size, std::back_inserter(hv.vec));
new (&m_repr.hvector) heap_vector(std::move(hv));
}
PYBIND11_NOINLINE void reserve_slow_path(std::size_t sz) { m_repr.hvector.vec.reserve(sz); }
bool is_inline() const { return m_repr.is_inline(); }
};
// Container to avoid heap allocation for kRequestedInlineSize or fewer booleans.
template <std::size_t kRequestedInlineSize>
struct small_vector<bool, kRequestedInlineSize> {
private:
public:
small_vector() = default;
// Disable copy ctor and assignment.
small_vector(const small_vector &) = delete;
small_vector &operator=(const small_vector &) = delete;
small_vector(small_vector &&) noexcept = default;
small_vector &operator=(small_vector &&) noexcept = default;
small_vector(std::size_t count, bool value) {
if (count > kInlineSize) {
new (&m_repr.hvector) typename repr_type::heap_vector(count, value);
} else {
auto &inline_arr = m_repr.iarray;
inline_arr.arr.fill(value ? static_cast<std::size_t>(-1) : 0);
inline_arr.size = static_cast<decltype(inline_arr.size)>(count);
}
}
std::size_t size() const {
if (is_inline()) {
return m_repr.iarray.size;
}
return m_repr.hvector.vec.size();
}
void reserve(std::size_t sz) {
if (is_inline()) {
if (sz > kInlineSize) {
move_to_heap_vector_with_reserved_size(sz);
}
} else {
m_repr.hvector.vec.reserve(sz);
}
}
bool operator[](std::size_t idx) const {
if (is_inline()) {
return inline_index(idx);
}
assert(idx < m_repr.hvector.vec.size());
return m_repr.hvector.vec[idx];
}
void push_back(bool b) {
if (is_inline()) {
auto &ha = m_repr.iarray;
if (ha.size == kInlineSize) {
move_to_heap_vector_with_reserved_size(kInlineSize + 1);
push_back_slow_path(b);
} else {
assert(ha.size < kInlineSize);
const auto wbi = word_and_bit_index(ha.size++);
assert(wbi.word < kWords);
assert(wbi.bit < kBitsPerWord);
if (b) {
ha.arr[wbi.word] |= (static_cast<std::size_t>(1) << wbi.bit);
} else {
ha.arr[wbi.word] &= ~(static_cast<std::size_t>(1) << wbi.bit);
}
assert(operator[](ha.size - 1) == b);
}
} else {
push_back_slow_path(b);
}
}
void set(std::size_t idx, bool value = true) {
if (is_inline()) {
auto &ha = m_repr.iarray;
assert(ha.size < kInlineSize);
const auto wbi = word_and_bit_index(idx);
assert(wbi.word < kWords);
assert(wbi.bit < kBitsPerWord);
if (value) {
ha.arr[wbi.word] |= (static_cast<std::size_t>(1) << wbi.bit);
} else {
ha.arr[wbi.word] &= ~(static_cast<std::size_t>(1) << wbi.bit);
}
} else {
m_repr.hvector.vec[idx] = value;
}
}
void swap(small_vector &rhs) noexcept { std::swap(m_repr, rhs.m_repr); }
private:
struct WordAndBitIndex {
std::size_t word;
std::size_t bit;
};
static WordAndBitIndex word_and_bit_index(std::size_t idx) {
return WordAndBitIndex{idx / kBitsPerWord, idx % kBitsPerWord};
}
bool inline_index(std::size_t idx) const {
const auto wbi = word_and_bit_index(idx);
assert(wbi.word < kWords);
assert(wbi.bit < kBitsPerWord);
return m_repr.iarray.arr[wbi.word] & (static_cast<std::size_t>(1) << wbi.bit);
}
PYBIND11_NOINLINE void move_to_heap_vector_with_reserved_size(std::size_t reserved_size) {
auto &inline_arr = m_repr.iarray;
using heap_vector = typename repr_type::heap_vector;
heap_vector hv;
hv.vec.reserve(reserved_size);
for (std::size_t ii = 0; ii < inline_arr.size; ++ii) {
hv.vec.push_back(inline_index(ii));
}
new (&m_repr.hvector) heap_vector(std::move(hv));
}
PYBIND11_NOINLINE void push_back_slow_path(bool b) { m_repr.hvector.vec.push_back(b); }
static constexpr auto kBitsPerWord = 8 * sizeof(std::size_t);
static constexpr auto kWords = (kRequestedInlineSize + kBitsPerWord - 1) / kBitsPerWord;
static constexpr auto kInlineSize = kWords * kBitsPerWord;
using repr_type = inline_array_or_vector<std::size_t, kWords, bool>;
repr_type m_repr;
bool is_inline() const { return m_repr.is_inline(); }
};
// Container to avoid heap allocation for N or fewer arguments.
template <size_t N>
using argument_vector = small_vector<handle, N>;
// Container to avoid heap allocation for N or fewer booleans.
template <size_t N>
using args_convert_vector = small_vector<bool, N>;
/// A small_vector of PyObject* that holds references and releases them on destruction.
/// This provides explicit ownership semantics without relying on py::object's
/// destructor, and avoids the need for reinterpret_cast when passing to vectorcall.
template <std::size_t InlineSize>
class ref_small_vector {
public:
ref_small_vector() = default;
~ref_small_vector() {
for (std::size_t i = 0; i < m_ptrs.size(); ++i) {
Py_XDECREF(m_ptrs[i]);
}
}
// Disable copy (prevent accidental double-decref)
ref_small_vector(const ref_small_vector &) = delete;
ref_small_vector &operator=(const ref_small_vector &) = delete;
// Move is allowed
ref_small_vector(ref_small_vector &&other) noexcept : m_ptrs(std::move(other.m_ptrs)) {
// other.m_ptrs is now empty, so its destructor won't decref anything
}
ref_small_vector &operator=(ref_small_vector &&other) noexcept {
if (this != &other) {
// Decref our current contents
for (std::size_t i = 0; i < m_ptrs.size(); ++i) {
Py_XDECREF(m_ptrs[i]);
}
m_ptrs = std::move(other.m_ptrs);
}
return *this;
}
/// Add a pointer, taking ownership (no incref, will decref on destruction)
void push_back_steal(PyObject *p) { m_ptrs.push_back(p); }
/// Add a pointer, borrowing (increfs now, will decref on destruction)
void push_back_borrow(PyObject *p) {
Py_XINCREF(p);
m_ptrs.push_back(p);
}
/// Add a null pointer (for PY_VECTORCALL_ARGUMENTS_OFFSET slot)
void push_back_null() { m_ptrs.push_back(nullptr); }
void reserve(std::size_t sz) { m_ptrs.reserve(sz); }
std::size_t size() const { return m_ptrs.size(); }
PyObject *operator[](std::size_t idx) const { return m_ptrs[idx]; }
PyObject *const *data() const { return m_ptrs.data(); }
private:
small_vector<PyObject *, InlineSize> m_ptrs;
};
PYBIND11_NAMESPACE_END(detail)
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)