Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor type_data to save space #194

Merged
merged 3 commits into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/benchmark.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ The following experiments analyze the performance of a large function-heavy
<https://github.com/pybind/pybind11/tree/smart_holder>`__ that addresses
long-standing issues related to holder types in pybind11.

Each experiment is shown twice: light gray `[debug]` columns provide data for
a debug build, and `[opt]` shows a size-optimized build that is representative
Each experiment is shown twice: light gray ``[debug]`` columns provide data for
a debug build, and ``[opt]`` shows a size-optimized build that is representative
of a deployment scenario. The former is included to show that nanobind
performance is also good during a typical development workflow.

Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ below inherit that of the preceding release.
Version 1.3.0 (TBD)
-------------------


* Reduced the size of nanobind type objects by 4 pointers.
(PR `#194 <https://github.com/wjakob/nanobind/pull/194>`__).
* Added a type caster between Python datetime/timedelta objects and
C++ ``std::chrono::duration``/``std::chrono::time_point``, ported
from pybind11. (PR `#175 <https://github.com/wjakob/nanobind/pull/175>`__).
* Added :cpp:func:`nb::python_error::discard_as_unraisable()
<python_error::discard_as_unraisable>` as a wrapper around
``PyErr_WriteUnraisable()``.
(PR `#175 <https://github.com/wjakob/nanobind/pull/175>`__).
* ABI version 9.

Version 1.2.0 (April 24, 2023)
------------------------------
Expand Down
141 changes: 80 additions & 61 deletions include/nanobind/nb_class.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
NAMESPACE_BEGIN(NB_NAMESPACE)
NAMESPACE_BEGIN(detail)

/// Flags about a type that persist throughout its lifetime
enum class type_flags : uint32_t {
/// Does the type provide a C++ destructor?
is_destructible = (1 << 0),
Expand All @@ -20,133 +21,151 @@ enum class type_flags : uint32_t {
/// Does the type provide a C++ move constructor?
is_move_constructible = (1 << 2),

/// Is this a python type that extends a bound C++ type?
is_python_type = (1 << 4),
/// Is the 'destruct' field of the type_data structure set?
has_destruct = (1 << 4),

/// Is the 'scope' field of the type_data structure set?
has_scope = (1 << 5),
/// Is the 'copy' field of the type_data structure set?
has_copy = (1 << 5),

/// Is the 'doc' field of the type_data structure set?
has_doc = (1 << 6),
/// Is the 'move' field of the type_data structure set?
has_move = (1 << 6),

/// Is the 'base' field of the type_data structure set?
has_base = (1 << 7),
/// Internal: does the type maintain a list of implicit conversions?
has_implicit_conversions = (1 << 7),

/// Is the 'base_py' field of the type_data structure set?
has_base_py = (1 << 8),
/// Is this a python type that extends a bound C++ type?
is_python_type = (1 << 8),

/// Is the 'destruct' field of the type_data structure set?
has_destruct = (1 << 9),
/// This type does not permit subclassing from Python
is_final = (1 << 9),

/// Is the 'copy' field of the type_data structure set?
has_copy = (1 << 10),
/// Is the 'supplement' field of the type_data structure set?
has_supplement = (1 << 10),

/// Is the 'move' field of the type_data structure set?
has_move = (1 << 11),
/// Instances of this type support dynamic attribute assignment
has_dynamic_attr = (1 << 11),

/// Internal: does the type maintain a list of implicit conversions?
has_implicit_conversions = (1 << 12),
/// The class uses an intrusive reference counting approach
intrusive_ptr = (1 << 12),

/// Is this a trampoline class meant to be overloaded in Python?
is_trampoline = (1 << 13),

/// Is the 'scope' field of the type_data structure set?
has_scope = (1 << 14),

/// This type is a signed enumeration
is_signed_enum = (1 << 13),
is_signed_enum = (1 << 15),

/// This type is an unsigned enumeration
is_unsigned_enum = (1 << 14),
is_unsigned_enum = (1 << 16),

/// This type is an arithmetic enumeration
is_arithmetic = (1 << 15),
is_arithmetic = (1 << 17),

/// This type provides extra PyType_Slot fields
has_type_slots = (1 << 16),
// Two more flag bits available (18 and 19) without needing
// a larger reorganization
};

/// This type does not permit subclassing from Python
is_final = (1 << 17),
/// Flags about a type that are only relevant when it is being created.
/// These are currently stored in type_data::flags alongside the type_flags
/// for more efficient memory layout, but could move elsewhere if we run
/// out of flags.
enum class type_init_flags : uint32_t {
/// Is the 'doc' field of the type_data_prelim structure set?
has_doc = (1 << 20),

/// This type does not permit subclassing from Python
has_supplement = (1 << 18),
/// Is the 'base' field of the type_data_prelim structure set?
has_base = (1 << 21),

/// Instances of this type support dynamic attribute assignment
has_dynamic_attr = (1 << 19),
/// Is the 'base_py' field of the type_data_prelim structure set?
has_base_py = (1 << 22),

/// The class uses an intrusive reference counting approach
intrusive_ptr = (1 << 20),
/// This type provides extra PyType_Slot fields
has_type_slots = (1 << 23),

/// Is this a trampoline class meant to be overloaded in Python?
is_trampoline = (1 << 21)
all_init_flags = (0xf << 20)
};

/// Information about a type that persists throughout its lifetime
struct type_data {
uint32_t size;
uint32_t align : 8;
uint32_t flags : 24;
const char *name;
const char *doc;
PyObject *scope;
const std::type_info *type;
const std::type_info *base;
PyTypeObject *type_py;
PyTypeObject *base_py;
void (*destruct)(void *);
void (*copy)(void *, const void *);
void (*move)(void *, void *) noexcept;
const std::type_info **implicit;
bool (**implicit_py)(PyTypeObject *, PyObject *, cleanup_list *) noexcept;
PyType_Slot *type_slots;
void *supplement;
void (*set_self_py)(void *, PyObject *) noexcept;
#if defined(Py_LIMITED_API)
size_t dictoffset;
#endif
};

NB_INLINE void type_extra_apply(type_data &t, const handle &h) {
t.flags |= (uint32_t) type_flags::has_base_py;
/// Information about a type that is only relevant when it is being created
struct type_init_data : type_data {
const std::type_info *base;
PyTypeObject *base_py;
const char *doc;
const PyType_Slot *type_slots;
};

NB_INLINE void type_extra_apply(type_init_data &t, const handle &h) {
t.flags |= (uint32_t) type_init_flags::has_base_py;
t.base_py = (PyTypeObject *) h.ptr();
}

NB_INLINE void type_extra_apply(type_data &t, const char *doc) {
t.flags |= (uint32_t) type_flags::has_doc;
NB_INLINE void type_extra_apply(type_init_data &t, const char *doc) {
t.flags |= (uint32_t) type_init_flags::has_doc;
t.doc = doc;
}

NB_INLINE void type_extra_apply(type_data &t, type_slots c) {
t.flags |= (uint32_t) type_flags::has_type_slots;
NB_INLINE void type_extra_apply(type_init_data &t, type_slots c) {
t.flags |= (uint32_t) type_init_flags::has_type_slots;
t.type_slots = c.value;
}

template <typename T>
NB_INLINE void type_extra_apply(type_data &t, intrusive_ptr<T> ip) {
NB_INLINE void type_extra_apply(type_init_data &t, intrusive_ptr<T> ip) {
t.flags |= (uint32_t) type_flags::intrusive_ptr;
t.set_self_py = (void (*)(void *, PyObject *) noexcept) ip.set_self_py;
}

NB_INLINE void type_extra_apply(type_data &t, is_enum e) {
if (e.is_signed)
t.flags |= (uint32_t) type_flags::is_signed_enum;
else
t.flags |= (uint32_t) type_flags::is_unsigned_enum;
}

NB_INLINE void type_extra_apply(type_data &t, is_final) {
NB_INLINE void type_extra_apply(type_init_data &t, is_final) {
t.flags |= (uint32_t) type_flags::is_final;
}

NB_INLINE void type_extra_apply(type_data &t, is_arithmetic) {
t.flags |= (uint32_t) type_flags::is_arithmetic;
}

NB_INLINE void type_extra_apply(type_data &t, dynamic_attr) {
NB_INLINE void type_extra_apply(type_init_data &t, dynamic_attr) {
t.flags |= (uint32_t) type_flags::has_dynamic_attr;
}

template <typename T>
NB_INLINE void type_extra_apply(type_data &t, supplement<T>) {
NB_INLINE void type_extra_apply(type_init_data &t, supplement<T>) {
static_assert(std::is_trivially_default_constructible_v<T>,
"The supplement type must be a POD (plain old data) type");
t.flags |= (uint32_t) type_flags::has_supplement | (uint32_t) type_flags::is_final;
t.supplement = (void *) malloc(sizeof(T));
}

// Enum-specific annotations:

NB_INLINE void type_extra_apply(type_init_data &t, is_enum e) {
if (e.is_signed)
t.flags |= (uint32_t) type_flags::is_signed_enum;
else
t.flags |= (uint32_t) type_flags::is_unsigned_enum;
}

NB_INLINE void type_extra_apply(type_init_data &t, is_arithmetic) {
t.flags |= (uint32_t) type_flags::is_arithmetic;
}

template <typename T> void wrap_copy(void *dst, const void *src) {
new ((T *) dst) T(*(const T *) src);
}
Expand Down Expand Up @@ -282,19 +301,19 @@ class class_ : public object {

template <typename... Extra>
NB_INLINE class_(handle scope, const char *name, const Extra &... extra) {
detail::type_data d;
detail::type_init_data d;

d.flags = (uint32_t) detail::type_flags::has_scope;
d.align = (uint8_t) alignof(Alias);
d.size = (uint32_t) sizeof(Alias);
d.supplement = 0;
d.supplement = nullptr;
d.name = name;
d.scope = scope.ptr();
d.type = &typeid(T);

if constexpr (!std::is_same_v<Base, T>) {
d.base = &typeid(Base);
d.flags |= (uint32_t) detail::type_flags::has_base;
d.flags |= (uint32_t) detail::type_init_flags::has_base;
}

if constexpr (!std::is_same_v<Alias, T>)
Expand Down
4 changes: 2 additions & 2 deletions include/nanobind/nb_lib.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,8 @@ NB_CORE PyObject *nb_func_new(const void *data) noexcept;
// ========================================================================

/// Create a Python type object for the given type record
struct type_data;
NB_CORE PyObject *nb_type_new(const type_data *c) noexcept;
struct type_init_data;
NB_CORE PyObject *nb_type_new(const type_init_data *c) noexcept;

/// Extract a pointer to a C++ type underlying a Python object, if possible
NB_CORE bool nb_type_get(const std::type_info *t, PyObject *o, uint8_t flags,
Expand Down
2 changes: 1 addition & 1 deletion src/nb_internals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

/// Tracks the ABI of nanobind
#ifndef NB_INTERNALS_VERSION
# define NB_INTERNALS_VERSION 7
# define NB_INTERNALS_VERSION 9
#endif

/// On MSVC, debug and release builds are not ABI-compatible!
Expand Down
22 changes: 12 additions & 10 deletions src/nb_type.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,6 @@ int nb_type_init(PyObject *self, PyObject *args, PyObject *kwds) {
t->name = NB_STRDUP(PyUnicode_AsUTF8AndSize(name, nullptr));
Py_DECREF(name);
t->type_py = (PyTypeObject *) self;
t->base = t_b->type;
t->base_py = t_b->type_py;
t->implicit = nullptr;
t->implicit_py = nullptr;
t->supplement = nullptr;
Expand All @@ -335,16 +333,16 @@ int nb_type_init(PyObject *self, PyObject *args, PyObject *kwds) {
}

/// Called when a C++ type is bound via nb::class_<>
PyObject *nb_type_new(const type_data *t) noexcept {
PyObject *nb_type_new(const type_init_data *t) noexcept {
bool is_signed_enum = t->flags & (uint32_t) type_flags::is_signed_enum,
is_unsigned_enum = t->flags & (uint32_t) type_flags::is_unsigned_enum,
is_arithmetic = t->flags & (uint32_t) type_flags::is_arithmetic,
is_enum = is_signed_enum || is_unsigned_enum,
has_scope = t->flags & (uint32_t) type_flags::has_scope,
has_doc = t->flags & (uint32_t) type_flags::has_doc,
has_base = t->flags & (uint32_t) type_flags::has_base,
has_base_py = t->flags & (uint32_t) type_flags::has_base_py,
has_type_slots = t->flags & (uint32_t) type_flags::has_type_slots,
has_doc = t->flags & (uint32_t) type_init_flags::has_doc,
has_base = t->flags & (uint32_t) type_init_flags::has_base,
has_base_py = t->flags & (uint32_t) type_init_flags::has_base_py,
has_type_slots = t->flags & (uint32_t) type_init_flags::has_type_slots,
has_supplement = t->flags & (uint32_t) type_flags::has_supplement,
has_dynamic_attr = t->flags & (uint32_t) type_flags::has_dynamic_attr,
intrusive_ptr = t->flags & (uint32_t) type_flags::intrusive_ptr;
Expand Down Expand Up @@ -387,6 +385,9 @@ PyObject *nb_type_new(const type_data *t) noexcept {
fail("nanobind::detail::nb_type_new(\"%s\"): multiple base types "
"specified!", t->name);
base = (PyObject *) t->base_py;
if (Py_TYPE(base) != internals.nb_type)
fail("nanobind::detail::nb_type_new(\"%s\"): base type is "
"not a nanobind type!", t->name);
} else if (has_base) {
auto it = internals.type_c2p.find(std::type_index(*t->base));
if (it == internals.type_c2p.end())
Expand Down Expand Up @@ -415,8 +416,8 @@ PyObject *nb_type_new(const type_data *t) noexcept {
constexpr size_t nb_enum_max_slots = 22,
nb_type_max_slots = 10,
nb_extra_slots = 80,
nb_total_slots = nb_enum_max_slots +
nb_type_max_slots +
nb_total_slots = nb_type_max_slots +
nb_enum_max_slots +
nb_extra_slots + 1;

PyMemberDef members[2] { };
Expand Down Expand Up @@ -676,7 +677,8 @@ PyObject *nb_type_new(const type_data *t) noexcept {
#endif

type_data *to = nb_type_data((PyTypeObject *) result);
*to = *t;
*to = *t; // note: slices off _init parts
to->flags &= ~(uint32_t) type_init_flags::all_init_flags;

if (!has_scope)
to->flags &= ~(uint32_t) type_flags::has_scope;
Expand Down