avoid C++ -> Python -> C++ overheads when passing around function objects

This commit is contained in:
Wenzel Jakob 2016-07-10 10:13:18 +02:00
parent 52269e91aa
commit 954b7932fe
7 changed files with 147 additions and 25 deletions

View File

@ -185,21 +185,32 @@ The following interactive session shows how to call them from Python.
>>> plus_1(number=43) >>> plus_1(number=43)
44L 44L
.. note::
This functionality is very useful when generating bindings for callbacks in
C++ libraries (e.g. a graphical user interface library).
The file :file:`example/example5.cpp` contains a complete example that
demonstrates how to work with callbacks and anonymous functions in more detail.
.. warning:: .. warning::
Keep in mind that passing a function from C++ to Python (or vice versa) Keep in mind that passing a function from C++ to Python (or vice versa)
will instantiate a piece of wrapper code that translates function will instantiate a piece of wrapper code that translates function
invocations between the two languages. Copying the same function back and invocations between the two languages. Naturally, this translation
forth between Python and C++ many times in a row will cause these wrappers increases the computational cost of each function call somewhat. A
to accumulate, which can decrease performance. problematic situation can arise when a function is copied back and forth
between Python and C++ many times in a row, in which case the underlying
wrappers will accumulate correspondingly. The resulting long sequence of
C++ -> Python -> C++ -> ... roundtrips can significantly decrease
performance.
There is one exception: pybind11 detects case where a stateless function
(i.e. a function pointer or a lambda function without captured variables)
is passed as an argument to another C++ function exposed in Python. In this
case, there is no overhead. Pybind11 will extract the underlying C++
function pointer from the wrapped function to sidestep a potential C++ ->
Python -> C++ roundtrip. This is demonstrated in Example 5.
.. note::
This functionality is very useful when generating bindings for callbacks in
C++ libraries (e.g. GUI libraries, asynchronous networking libraries, etc.).
The file :file:`example/example5.cpp` contains a complete example that
demonstrates how to work with callbacks and anonymous functions in more detail.
Overriding virtual functions in Python Overriding virtual functions in Python
====================================== ======================================

View File

@ -65,6 +65,29 @@ py::cpp_function test_callback5() {
py::arg("number")); py::arg("number"));
} }
int dummy_function(int i) { return i + 1; }
int dummy_function2(int i, int j) { return i + j; }
std::function<int(int)> roundtrip(std::function<int(int)> f) {
std::cout << "roundtrip.." << std::endl;
return f;
}
void test_dummy_function(const std::function<int(int)> &f) {
using fn_type = int (*)(int);
auto result = f.target<fn_type>();
if (!result) {
std::cout << "could not convert to a function pointer." << std::endl;
auto r = f(1);
std::cout << "eval(1) = " << r << std::endl;
} else if (*result == dummy_function) {
std::cout << "argument matches dummy_function" << std::endl;
auto r = (*result)(1);
std::cout << "eval(1) = " << r << std::endl;
} else {
std::cout << "argument does NOT match dummy_function. This should never happen!" << std::endl;
}
}
void init_ex5(py::module &m) { void init_ex5(py::module &m) {
py::class_<Pet> pet_class(m, "Pet"); py::class_<Pet> pet_class(m, "Pet");
pet_class pet_class
@ -113,4 +136,10 @@ void init_ex5(py::module &m) {
/* p should be cleaned up when the returned function is garbage collected */ /* p should be cleaned up when the returned function is garbage collected */
}; };
}); });
/* Test if passing a function pointer from C++ -> Python -> C++ yields the original pointer */
m.def("dummy_function", &dummy_function);
m.def("dummy_function2", &dummy_function2);
m.def("roundtrip", &roundtrip);
m.def("test_dummy_function", &test_dummy_function);
} }

View File

@ -54,3 +54,30 @@ f = test_callback5()
print("func(number=43) = %i" % f(number=43)) print("func(number=43) = %i" % f(number=43))
test_cleanup() test_cleanup()
from example import dummy_function
from example import dummy_function2
from example import test_dummy_function
from example import roundtrip
test_dummy_function(dummy_function)
test_dummy_function(roundtrip(dummy_function))
test_dummy_function(lambda x: x + 2)
try:
test_dummy_function(dummy_function2)
print("Problem!")
except Exception as e:
if 'Incompatible function arguments' in str(e):
print("All OK!")
else:
print("Problem!")
try:
test_dummy_function(lambda x, y: x + y)
print("Problem!")
except Exception as e:
if 'missing 1 required positional argument' in str(e):
print("All OK!")
else:
print("Problem!")

View File

@ -1,20 +1,13 @@
Rabbit is a parrot Rabbit is a parrot
Rabbit is a parrot
Polly is a parrot Polly is a parrot
Polly is a parrot
Molly is a dog
Molly is a dog Molly is a dog
Woof! Woof!
func(43) = 44
Payload constructor
Payload copy constructor
Payload move constructor
Payload destructor
Payload destructor
Payload destructor
Rabbit is a parrot
Polly is a parrot
Molly is a dog
The following error is expected: Incompatible function arguments. The following argument types are supported: The following error is expected: Incompatible function arguments. The following argument types are supported:
1. (example.Dog) -> NoneType 1. (example.Dog) -> NoneType
Invoked with: <Pet object at 0> Invoked with: <example.Pet object at 0>
Callback function 1 called! Callback function 1 called!
False False
Callback function 2 called : Hello, x, True, 5 Callback function 2 called : Hello, x, True, 5
@ -24,4 +17,22 @@ False
Callback function 3 called : Partial object with one argument Callback function 3 called : Partial object with one argument
False False
func(43) = 44 func(43) = 44
func(43) = 44
func(number=43) = 44 func(number=43) = 44
Payload constructor
Payload copy constructor
Payload move constructor
Payload destructor
Payload destructor
Payload destructor
argument matches dummy_function
eval(1) = 2
roundtrip..
argument matches dummy_function
eval(1) = 2
could not convert to a function pointer.
eval(1) = 3
could not convert to a function pointer.
All OK!
could not convert to a function pointer.
All OK!

View File

@ -113,6 +113,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 stateless function pointer
bool is_stateless : 1;
/// True if the function has a '*args' argument /// True if the function has a '*args' argument
bool has_args : 1; bool has_args : 1;

View File

@ -23,6 +23,29 @@ public:
src_ = detail::get_function(src_); src_ = detail::get_function(src_);
if (!src_ || !PyCallable_Check(src_.ptr())) if (!src_ || !PyCallable_Check(src_.ptr()))
return false; return false;
{
/*
When passing a C++ function as an argument to another C++
function via Python, every function call would normally involve
a full C++ -> Python -> C++ roundtrip, which can be prohibitive.
Here, we try to at least detect the case where the function is
stateless (i.e. function pointer or lambda function without
captured variables), in which case the roundtrip can be avoided.
*/
if (PyCFunction_Check(src_.ptr())) {
capsule c(PyCFunction_GetSelf(src_.ptr()), true);
auto rec = (function_record *) c;
using FunctionType = Return (*) (Args...);
if (rec && rec->is_stateless && rec->data[1] == &typeid(FunctionType)) {
struct capture { FunctionType f; };
value = ((capture *) &rec->data)->f;
return true;
}
}
}
object src(src_, true); object src(src_, true);
value = [src](Args... args) -> Return { value = [src](Args... args) -> Return {
gil_scoped_acquire acq; gil_scoped_acquire acq;
@ -35,7 +58,11 @@ public:
template <typename Func> template <typename Func>
static handle cast(Func &&f_, return_value_policy policy, handle /* parent */) { static handle cast(Func &&f_, return_value_policy policy, handle /* parent */) {
return cpp_function(std::forward<Func>(f_), policy).release(); auto result = f_.template target<Return (*)(Args...)>();
if (result)
return cpp_function(*result, policy).release();
else
return cpp_function(std::forward<Func>(f_), policy).release();
} }
PYBIND11_TYPE_CASTER(type, _("function<") + PYBIND11_TYPE_CASTER(type, _("function<") +

View File

@ -82,6 +82,9 @@ protected:
/* Store the capture object directly in the function record if there is enough space */ /* Store the capture object directly in the function record if there is enough space */
if (sizeof(capture) <= sizeof(rec->data)) { if (sizeof(capture) <= sizeof(rec->data)) {
/* Without these pragmas, GCC warns that there might not be
enough space to use the placement new operator. However, the
'if' statement above ensures that this is the case. */
#if defined(__GNUG__) && !defined(__clang__) && __GNUC__ >= 6 #if defined(__GNUG__) && !defined(__clang__) && __GNUC__ >= 6
# pragma GCC diagnostic push # pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wplacement-new" # pragma GCC diagnostic ignored "-Wplacement-new"
@ -118,7 +121,7 @@ protected:
capture *cap = (capture *) (sizeof(capture) <= sizeof(rec->data) capture *cap = (capture *) (sizeof(capture) <= sizeof(rec->data)
? &rec->data : rec->data[0]); ? &rec->data : rec->data[0]);
/* Perform the functioncall */ /* Perform the function call */
handle result = cast_out::cast(args_converter.template call<Return>(cap->f), handle result = cast_out::cast(args_converter.template call<Return>(cap->f),
rec->policy, parent); rec->policy, parent);
@ -140,6 +143,16 @@ protected:
if (cast_in::has_args) rec->has_args = true; if (cast_in::has_args) rec->has_args = true;
if (cast_in::has_kwargs) rec->has_kwargs = true; if (cast_in::has_kwargs) rec->has_kwargs = true;
/* Stash some additional information used by an important optimization in 'functional.h' */
using FunctionType = Return (*)(Args...);
constexpr bool is_function_ptr =
std::is_convertible<Func, FunctionType>::value &&
sizeof(capture) == sizeof(void *);
if (is_function_ptr) {
rec->is_stateless = true;
rec->data[1] = (void *) &typeid(FunctionType);
}
} }
/// Register a function call with Python (generic non-templated code goes here) /// Register a function call with Python (generic non-templated code goes here)
@ -157,6 +170,7 @@ protected:
else if (a.value) else if (a.value)
a.descr = strdup(((std::string) ((object) handle(a.value).attr("__repr__"))().str()).c_str()); a.descr = strdup(((std::string) ((object) handle(a.value).attr("__repr__"))().str()).c_str());
} }
auto const &registered_types = detail::get_internals().registered_types_cpp; auto const &registered_types = detail::get_internals().registered_types_cpp;
/* Generate a proper function signature */ /* Generate a proper function signature */
@ -215,10 +229,10 @@ protected:
rec->name = strdup("__nonzero__"); rec->name = strdup("__nonzero__");
} }
#endif #endif
rec->signature = strdup(signature.c_str()); rec->signature = strdup(signature.c_str());
rec->args.shrink_to_fit(); rec->args.shrink_to_fit();
rec->is_constructor = !strcmp(rec->name, "__init__") || !strcmp(rec->name, "__setstate__"); rec->is_constructor = !strcmp(rec->name, "__init__") || !strcmp(rec->name, "__setstate__");
rec->is_stateless = false;
rec->has_args = false; rec->has_args = false;
rec->has_kwargs = false; rec->has_kwargs = false;
rec->nargs = (uint16_t) args; rec->nargs = (uint16_t) args;