Commit 39fd6a94 by Dean Moldovan

Reduce binary size overhead of new-style constructors

The lookup of the `self` type and value pointer are moved out of
template code and into `dispatcher`. This brings down the binary
size of constructors back to the level of the old placement-new
approach. (It also avoids a second lookup for `init_instance`.)

With this implementation, mixing old- and new-style constructors
in the same overload set may result in some runtime overhead for
temporary allocations/deallocations, but this should be fine as
old style constructors are phased out.
parent 93528f57
...@@ -133,8 +133,8 @@ struct argument_record { ...@@ -133,8 +133,8 @@ struct argument_record {
/// Internal data structure which holds metadata about a bound function (signature, overloads, etc.) /// Internal data structure which holds metadata about a bound function (signature, overloads, etc.)
struct function_record { struct function_record {
function_record() function_record()
: is_constructor(false), is_stateless(false), is_operator(false), : is_constructor(false), is_new_style_constructor(false), is_stateless(false),
has_args(false), has_kwargs(false), is_method(false) { } is_operator(false), has_args(false), has_kwargs(false), is_method(false) { }
/// Function name /// Function name
char *name = nullptr; /* why no C++ strings? They generate heavier code.. */ char *name = nullptr; /* why no C++ strings? They generate heavier code.. */
...@@ -163,6 +163,9 @@ struct function_record { ...@@ -163,6 +163,9 @@ struct function_record {
/// True if name == '__init__' /// True if name == '__init__'
bool is_constructor : 1; bool is_constructor : 1;
/// True if this is a new-style `__init__` defined in `detail/init.h`
bool is_new_style_constructor : 1;
/// True if this is a stateless function pointer /// True if this is a stateless function pointer
bool is_stateless : 1; bool is_stateless : 1;
...@@ -281,6 +284,9 @@ inline function_call::function_call(function_record &f, handle p) : ...@@ -281,6 +284,9 @@ inline function_call::function_call(function_record &f, handle p) :
args_convert.reserve(f.nargs); args_convert.reserve(f.nargs);
} }
/// Tag for a new-style `__init__` defined in `detail/init.h`
struct is_new_style_constructor { };
/** /**
* Partial template specializations to process custom attributes provided to * Partial template specializations to process custom attributes provided to
* cpp_function_ and class_. These are either used to initialize the respective * cpp_function_ and class_. These are either used to initialize the respective
...@@ -339,6 +345,10 @@ template <> struct process_attribute<is_operator> : process_attribute_default<is ...@@ -339,6 +345,10 @@ template <> struct process_attribute<is_operator> : process_attribute_default<is
static void init(const is_operator &, function_record *r) { r->is_operator = true; } static void init(const is_operator &, function_record *r) { r->is_operator = true; }
}; };
template <> struct process_attribute<is_new_style_constructor> : process_attribute_default<is_new_style_constructor> {
static void init(const is_new_style_constructor &, function_record *r) { r->is_new_style_constructor = true; }
};
/// Process a keyword argument attribute (*without* a default value) /// Process a keyword argument attribute (*without* a default value)
template <> struct process_attribute<arg> : process_attribute_default<arg> { template <> struct process_attribute<arg> : process_attribute_default<arg> {
static void init(const arg &a, function_record *r) { static void init(const arg &a, function_record *r) {
......
...@@ -242,8 +242,9 @@ protected: ...@@ -242,8 +242,9 @@ protected:
.cast<std::string>() + "."; .cast<std::string>() + ".";
#endif #endif
signature += tinfo->type->tp_name; signature += tinfo->type->tp_name;
} else if (rec->is_constructor && arg_index == 0 && detail::same_type(typeid(handle), *t) && rec->scope) { } else if (rec->is_new_style_constructor && arg_index == 0) {
// A py::init(...) constructor takes `self` as a `handle`; rewrite it to the type // A new-style `__init__` takes `self` as `value_and_holder`.
// Rewrite it to the proper class type.
#if defined(PYPY_VERSION) #if defined(PYPY_VERSION)
signature += rec->scope.attr("__module__").cast<std::string>() + "."; signature += rec->scope.attr("__module__").cast<std::string>() + ".";
#endif #endif
...@@ -425,6 +426,23 @@ protected: ...@@ -425,6 +426,23 @@ protected:
handle parent = n_args_in > 0 ? PyTuple_GET_ITEM(args_in, 0) : nullptr, handle parent = n_args_in > 0 ? PyTuple_GET_ITEM(args_in, 0) : nullptr,
result = PYBIND11_TRY_NEXT_OVERLOAD; result = PYBIND11_TRY_NEXT_OVERLOAD;
auto self_value_and_holder = value_and_holder();
if (overloads->is_constructor) {
const auto tinfo = get_type_info((PyTypeObject *) overloads->scope.ptr());
const auto pi = reinterpret_cast<instance *>(parent.ptr());
self_value_and_holder = pi->get_value_and_holder(tinfo, false);
if (!self_value_and_holder.type || !self_value_and_holder.inst) {
PyErr_SetString(PyExc_TypeError, "__init__(self, ...) called with invalid `self` argument");
return nullptr;
}
// If this value is already registered it must mean __init__ is invoked multiple times;
// we really can't support that in C++, so just ignore the second __init__.
if (self_value_and_holder.instance_registered())
return none().release().ptr();
}
try { try {
// We do this in two passes: in the first pass, we load arguments with `convert=false`; // We do this in two passes: in the first pass, we load arguments with `convert=false`;
// in the second, we allow conversion (except for arguments with an explicit // in the second, we allow conversion (except for arguments with an explicit
...@@ -472,6 +490,18 @@ protected: ...@@ -472,6 +490,18 @@ protected:
size_t args_to_copy = std::min(pos_args, n_args_in); size_t args_to_copy = std::min(pos_args, n_args_in);
size_t args_copied = 0; size_t args_copied = 0;
// 0. Inject new-style `self` argument
if (func.is_new_style_constructor) {
// The `value` may have been preallocated by an old-style `__init__`
// if it was a preceding candidate for overload resolution.
if (self_value_and_holder)
self_value_and_holder.type->dealloc(self_value_and_holder);
call.args.push_back(reinterpret_cast<PyObject *>(&self_value_and_holder));
call.args_convert.push_back(false);
++args_copied;
}
// 1. Copy any position arguments given. // 1. Copy any position arguments given.
bool bad_arg = false; bool bad_arg = false;
for (; args_copied < args_to_copy; ++args_copied) { for (; args_copied < args_to_copy; ++args_copied) {
...@@ -715,13 +745,9 @@ protected: ...@@ -715,13 +745,9 @@ protected:
PyErr_SetString(PyExc_TypeError, msg.c_str()); PyErr_SetString(PyExc_TypeError, msg.c_str());
return nullptr; return nullptr;
} else { } else {
if (overloads->is_constructor) { if (overloads->is_constructor && !self_value_and_holder.holder_constructed()) {
auto tinfo = get_type_info((PyTypeObject *) overloads->scope.ptr());
auto *pi = reinterpret_cast<instance *>(parent.ptr()); auto *pi = reinterpret_cast<instance *>(parent.ptr());
auto v_h = pi->get_value_and_holder(tinfo); self_value_and_holder.type->init_instance(pi, nullptr);
if (!v_h.holder_constructed()) {
tinfo->init_instance(pi, nullptr);
}
} }
return result.ptr(); return result.ptr();
} }
......
...@@ -40,8 +40,7 @@ def test_init_factory_basic(): ...@@ -40,8 +40,7 @@ def test_init_factory_basic():
with pytest.raises(TypeError) as excinfo: with pytest.raises(TypeError) as excinfo:
m.TestFactory3(tag.null_ptr) m.TestFactory3(tag.null_ptr)
assert (str(excinfo.value) == assert str(excinfo.value) == "pybind11::init(): factory function returned nullptr"
"pybind11::init(): factory function returned nullptr")
assert [i.alive() for i in cstats] == [3, 3, 3] assert [i.alive() for i in cstats] == [3, 3, 3]
assert ConstructorStats.detail_reg_inst() == n_inst + 9 assert ConstructorStats.detail_reg_inst() == n_inst + 9
...@@ -352,9 +351,9 @@ def test_reallocations(capture, msg): ...@@ -352,9 +351,9 @@ def test_reallocations(capture, msg):
create_and_destroy(1.5) create_and_destroy(1.5)
assert msg(capture) == strip_comments(""" assert msg(capture) == strip_comments("""
noisy new # allocation required to attempt first overload noisy new # allocation required to attempt first overload
noisy delete # have to dealloc before considering factory init overload
noisy new # pointer factory calling "new", part 1: allocation noisy new # pointer factory calling "new", part 1: allocation
NoisyAlloc(double 1.5) # ... part two, invoking constructor NoisyAlloc(double 1.5) # ... part two, invoking constructor
noisy delete # have to dealloc before stashing factory-generated pointer
--- ---
~NoisyAlloc() # Destructor ~NoisyAlloc() # Destructor
noisy delete # operator delete noisy delete # operator delete
...@@ -396,9 +395,9 @@ def test_reallocations(capture, msg): ...@@ -396,9 +395,9 @@ def test_reallocations(capture, msg):
create_and_destroy(4, 0.5) create_and_destroy(4, 0.5)
assert msg(capture) == strip_comments(""" assert msg(capture) == strip_comments("""
noisy new # preallocation needed before invoking placement-new overload noisy new # preallocation needed before invoking placement-new overload
noisy delete # deallocation of preallocated storage
noisy new # Factory pointer allocation noisy new # Factory pointer allocation
NoisyAlloc(int 4) # factory pointer construction NoisyAlloc(int 4) # factory pointer construction
noisy delete # deallocation of preallocated storage
--- ---
~NoisyAlloc() # Destructor ~NoisyAlloc() # Destructor
noisy delete # operator delete noisy delete # operator delete
...@@ -408,6 +407,8 @@ def test_reallocations(capture, msg): ...@@ -408,6 +407,8 @@ def test_reallocations(capture, msg):
create_and_destroy(5, "hi") create_and_destroy(5, "hi")
assert msg(capture) == strip_comments(""" assert msg(capture) == strip_comments("""
noisy new # preallocation needed before invoking first placement new noisy new # preallocation needed before invoking first placement new
noisy delete # delete before considering new-style constructor
noisy new # preallocation for second placement new
noisy placement new # Placement new in the second placement new overload noisy placement new # Placement new in the second placement new overload
NoisyAlloc(int 5) # construction NoisyAlloc(int 5) # construction
--- ---
...@@ -450,11 +451,9 @@ def test_invalid_self(): ...@@ -450,11 +451,9 @@ def test_invalid_self():
for arg in (1, 2): for arg in (1, 2):
with pytest.raises(TypeError) as excinfo: with pytest.raises(TypeError) as excinfo:
BrokenTF1(arg) BrokenTF1(arg)
assert (str(excinfo.value) == assert str(excinfo.value) == "__init__(self, ...) called with invalid `self` argument"
"__init__(self, ...) called with invalid `self` argument")
for arg in (1, 2, 3, 4): for arg in (1, 2, 3, 4):
with pytest.raises(TypeError) as excinfo: with pytest.raises(TypeError) as excinfo:
BrokenTF6(arg) BrokenTF6(arg)
assert (str(excinfo.value) == assert str(excinfo.value) == "__init__(self, ...) called with invalid `self` argument"
"__init__(self, ...) called with invalid `self` argument")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment