Skip to content

Commit 954b793

Browse files
committed
avoid C++ -> Python -> C++ overheads when passing around function objects
1 parent 52269e9 commit 954b793

File tree

7 files changed

+145
-23
lines changed

7 files changed

+145
-23
lines changed

docs/advanced.rst

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,22 +185,33 @@ The following interactive session shows how to call them from Python.
185185
>>> plus_1(number=43)
186186
44L
187187
188+
.. warning::
189+
190+
Keep in mind that passing a function from C++ to Python (or vice versa)
191+
will instantiate a piece of wrapper code that translates function
192+
invocations between the two languages. Naturally, this translation
193+
increases the computational cost of each function call somewhat. A
194+
problematic situation can arise when a function is copied back and forth
195+
between Python and C++ many times in a row, in which case the underlying
196+
wrappers will accumulate correspondingly. The resulting long sequence of
197+
C++ -> Python -> C++ -> ... roundtrips can significantly decrease
198+
performance.
199+
200+
There is one exception: pybind11 detects case where a stateless function
201+
(i.e. a function pointer or a lambda function without captured variables)
202+
is passed as an argument to another C++ function exposed in Python. In this
203+
case, there is no overhead. Pybind11 will extract the underlying C++
204+
function pointer from the wrapped function to sidestep a potential C++ ->
205+
Python -> C++ roundtrip. This is demonstrated in Example 5.
206+
188207
.. note::
189208

190209
This functionality is very useful when generating bindings for callbacks in
191-
C++ libraries (e.g. a graphical user interface library).
210+
C++ libraries (e.g. GUI libraries, asynchronous networking libraries, etc.).
192211

193212
The file :file:`example/example5.cpp` contains a complete example that
194213
demonstrates how to work with callbacks and anonymous functions in more detail.
195214

196-
.. warning::
197-
198-
Keep in mind that passing a function from C++ to Python (or vice versa)
199-
will instantiate a piece of wrapper code that translates function
200-
invocations between the two languages. Copying the same function back and
201-
forth between Python and C++ many times in a row will cause these wrappers
202-
to accumulate, which can decrease performance.
203-
204215
Overriding virtual functions in Python
205216
======================================
206217

example/example5.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,29 @@ py::cpp_function test_callback5() {
6565
py::arg("number"));
6666
}
6767

68+
int dummy_function(int i) { return i + 1; }
69+
int dummy_function2(int i, int j) { return i + j; }
70+
std::function<int(int)> roundtrip(std::function<int(int)> f) {
71+
std::cout << "roundtrip.." << std::endl;
72+
return f;
73+
}
74+
75+
void test_dummy_function(const std::function<int(int)> &f) {
76+
using fn_type = int (*)(int);
77+
auto result = f.target<fn_type>();
78+
if (!result) {
79+
std::cout << "could not convert to a function pointer." << std::endl;
80+
auto r = f(1);
81+
std::cout << "eval(1) = " << r << std::endl;
82+
} else if (*result == dummy_function) {
83+
std::cout << "argument matches dummy_function" << std::endl;
84+
auto r = (*result)(1);
85+
std::cout << "eval(1) = " << r << std::endl;
86+
} else {
87+
std::cout << "argument does NOT match dummy_function. This should never happen!" << std::endl;
88+
}
89+
}
90+
6891
void init_ex5(py::module &m) {
6992
py::class_<Pet> pet_class(m, "Pet");
7093
pet_class
@@ -113,4 +136,10 @@ void init_ex5(py::module &m) {
113136
/* p should be cleaned up when the returned function is garbage collected */
114137
};
115138
});
139+
140+
/* Test if passing a function pointer from C++ -> Python -> C++ yields the original pointer */
141+
m.def("dummy_function", &dummy_function);
142+
m.def("dummy_function2", &dummy_function2);
143+
m.def("roundtrip", &roundtrip);
144+
m.def("test_dummy_function", &test_dummy_function);
116145
}

example/example5.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,30 @@ def func3(a):
5454
print("func(number=43) = %i" % f(number=43))
5555

5656
test_cleanup()
57+
58+
from example import dummy_function
59+
from example import dummy_function2
60+
from example import test_dummy_function
61+
from example import roundtrip
62+
63+
test_dummy_function(dummy_function)
64+
test_dummy_function(roundtrip(dummy_function))
65+
test_dummy_function(lambda x: x + 2)
66+
67+
try:
68+
test_dummy_function(dummy_function2)
69+
print("Problem!")
70+
except Exception as e:
71+
if 'Incompatible function arguments' in str(e):
72+
print("All OK!")
73+
else:
74+
print("Problem!")
75+
76+
try:
77+
test_dummy_function(lambda x, y: x + y)
78+
print("Problem!")
79+
except Exception as e:
80+
if 'missing 1 required positional argument' in str(e):
81+
print("All OK!")
82+
else:
83+
print("Problem!")

example/example5.ref

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
Rabbit is a parrot
2-
Polly is a parrot
3-
Molly is a dog
4-
Woof!
5-
func(43) = 44
6-
Payload constructor
7-
Payload copy constructor
8-
Payload move constructor
9-
Payload destructor
10-
Payload destructor
11-
Payload destructor
122
Rabbit is a parrot
133
Polly is a parrot
4+
Polly is a parrot
5+
Molly is a dog
146
Molly is a dog
7+
Woof!
158
The following error is expected: Incompatible function arguments. The following argument types are supported:
169
1. (example.Dog) -> NoneType
17-
Invoked with: <Pet object at 0>
10+
Invoked with: <example.Pet object at 0>
1811
Callback function 1 called!
1912
False
2013
Callback function 2 called : Hello, x, True, 5
@@ -24,4 +17,22 @@ False
2417
Callback function 3 called : Partial object with one argument
2518
False
2619
func(43) = 44
20+
func(43) = 44
2721
func(number=43) = 44
22+
Payload constructor
23+
Payload copy constructor
24+
Payload move constructor
25+
Payload destructor
26+
Payload destructor
27+
Payload destructor
28+
argument matches dummy_function
29+
eval(1) = 2
30+
roundtrip..
31+
argument matches dummy_function
32+
eval(1) = 2
33+
could not convert to a function pointer.
34+
eval(1) = 3
35+
could not convert to a function pointer.
36+
All OK!
37+
could not convert to a function pointer.
38+
All OK!

include/pybind11/attr.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ struct function_record {
113113
/// True if name == '__init__'
114114
bool is_constructor : 1;
115115

116+
/// True if this is a stateless function pointer
117+
bool is_stateless : 1;
118+
116119
/// True if the function has a '*args' argument
117120
bool has_args : 1;
118121

include/pybind11/functional.h

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,29 @@ template <typename Return, typename... Args> struct type_caster<std::function<Re
2323
src_ = detail::get_function(src_);
2424
if (!src_ || !PyCallable_Check(src_.ptr()))
2525
return false;
26+
27+
{
28+
/*
29+
When passing a C++ function as an argument to another C++
30+
function via Python, every function call would normally involve
31+
a full C++ -> Python -> C++ roundtrip, which can be prohibitive.
32+
Here, we try to at least detect the case where the function is
33+
stateless (i.e. function pointer or lambda function without
34+
captured variables), in which case the roundtrip can be avoided.
35+
*/
36+
if (PyCFunction_Check(src_.ptr())) {
37+
capsule c(PyCFunction_GetSelf(src_.ptr()), true);
38+
auto rec = (function_record *) c;
39+
using FunctionType = Return (*) (Args...);
40+
41+
if (rec && rec->is_stateless && rec->data[1] == &typeid(FunctionType)) {
42+
struct capture { FunctionType f; };
43+
value = ((capture *) &rec->data)->f;
44+
return true;
45+
}
46+
}
47+
}
48+
2649
object src(src_, true);
2750
value = [src](Args... args) -> Return {
2851
gil_scoped_acquire acq;
@@ -35,7 +58,11 @@ template <typename Return, typename... Args> struct type_caster<std::function<Re
3558

3659
template <typename Func>
3760
static handle cast(Func &&f_, return_value_policy policy, handle /* parent */) {
38-
return cpp_function(std::forward<Func>(f_), policy).release();
61+
auto result = f_.template target<Return (*)(Args...)>();
62+
if (result)
63+
return cpp_function(*result, policy).release();
64+
else
65+
return cpp_function(std::forward<Func>(f_), policy).release();
3966
}
4067

4168
PYBIND11_TYPE_CASTER(type, _("function<") +

include/pybind11/pybind11.h

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ class cpp_function : public function {
8282

8383
/* Store the capture object directly in the function record if there is enough space */
8484
if (sizeof(capture) <= sizeof(rec->data)) {
85+
/* Without these pragmas, GCC warns that there might not be
86+
enough space to use the placement new operator. However, the
87+
'if' statement above ensures that this is the case. */
8588
#if defined(__GNUG__) && !defined(__clang__) && __GNUC__ >= 6
8689
# pragma GCC diagnostic push
8790
# pragma GCC diagnostic ignored "-Wplacement-new"
@@ -118,7 +121,7 @@ class cpp_function : public function {
118121
capture *cap = (capture *) (sizeof(capture) <= sizeof(rec->data)
119122
? &rec->data : rec->data[0]);
120123

121-
/* Perform the functioncall */
124+
/* Perform the function call */
122125
handle result = cast_out::cast(args_converter.template call<Return>(cap->f),
123126
rec->policy, parent);
124127

@@ -140,6 +143,16 @@ class cpp_function : public function {
140143

141144
if (cast_in::has_args) rec->has_args = true;
142145
if (cast_in::has_kwargs) rec->has_kwargs = true;
146+
147+
/* Stash some additional information used by an important optimization in 'functional.h' */
148+
using FunctionType = Return (*)(Args...);
149+
constexpr bool is_function_ptr =
150+
std::is_convertible<Func, FunctionType>::value &&
151+
sizeof(capture) == sizeof(void *);
152+
if (is_function_ptr) {
153+
rec->is_stateless = true;
154+
rec->data[1] = (void *) &typeid(FunctionType);
155+
}
143156
}
144157

145158
/// Register a function call with Python (generic non-templated code goes here)
@@ -157,6 +170,7 @@ class cpp_function : public function {
157170
else if (a.value)
158171
a.descr = strdup(((std::string) ((object) handle(a.value).attr("__repr__"))().str()).c_str());
159172
}
173+
160174
auto const &registered_types = detail::get_internals().registered_types_cpp;
161175

162176
/* Generate a proper function signature */
@@ -215,10 +229,10 @@ class cpp_function : public function {
215229
rec->name = strdup("__nonzero__");
216230
}
217231
#endif
218-
219232
rec->signature = strdup(signature.c_str());
220233
rec->args.shrink_to_fit();
221234
rec->is_constructor = !strcmp(rec->name, "__init__") || !strcmp(rec->name, "__setstate__");
235+
rec->is_stateless = false;
222236
rec->has_args = false;
223237
rec->has_kwargs = false;
224238
rec->nargs = (uint16_t) args;

0 commit comments

Comments
 (0)