[FEAT] [RFC] Class builder API for high performance bindings
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 18k
- Forks
- 2.3k
- Avg merge
- 5d 17h
- Merged PRs (30d)
- 10
Description
Motivation
Pybind11 provides easy-to-use APIs for writing Python bindings. However, these bindings are often implemented with multiple level of indirections, which can have negative impact on the performance. For example, class_::def_property and class_::def_property_readonly are currently implemented as:
- A
PyCFunctionwrapper capturing a capsule holding adetail::function_record - ... wrapped in an
instancemethod - ... wrapped in a
propertystored in the class dictionary.
Compared to a getset_descriptor directly implemented with PyGetSetDef, this can be 5x-20x slower both in terms of execution time and instruction counts. Similarly, constructors implemented in pybind11 can be slower than a low-level constructor function directly bound to type with the tp_init slot. Both problems show the necessity of having a set of high performance binding APIs for pybind11.
Design
The new API is called "class builder" because it employs the builder pattern:
pybind11::class_builder<Example>(m, "Example")
.def("method", [](Example& e) {})
.def_class("class_method", [](Example& e) {})
.def_static("static_method", []() { return 1; })
.def_attr_readonly("readonly_attr", []() { return 1; })
.build();
To create high-performance bindings for a class, we would call pybind11::class_builder instead of pybind11::class_ with almost exactly the same parameters. This gives us a class builder, which we can use to define a series of methods and attributes (note these are getset_descriptors not propertys). After we define all the members of the class, we call the build method, which will create the Python class and return a pybind11::class_<...> instance.
Internally, def_attr and def_attr_readonly is implemented as adding a new entry to the tp_getset slot, and def, def_class and def_static is implemented as adding a new entry to the tp_methods slot. The build method will call PyType_Ready method on the type being built, and wraps the ready PyTypeObject in a pybind11::class_<...> instance.
Misc
- With the class builder API, we are going to directly set the slots of the type object. Hence, we need to split
cpp_functioninto two parts: object wrapper and dispatching logic. For the class builder API, the previous part isn't needed. We'll however keep it for backward compatibility. - Should we have a separate
class_builderAPI, or should we just rework the internals ofpybind11::class_? Personally I think it depends on whether we are allowed to add newPyMethodDefandPyGetSetDefafter callingPyType_Ready. I guess the answer is probably no so a separate set of APIs is likely needed. - Pybind11 functions are, in fact, closures capturing a
detail::function_record, and it is challenging to make them work withPyMethodDef, becausePyMethodDefhas no field to store the closure pointer (PyGetSetDefhas a closure pointer and does not have this problem). Perhaps we will need the closure functionality fromlibffito solve this problem?
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the existing class_ and cpp_function implementations, then trace how PyType_Ready, PyMethodDef, and PyGetSetDef are used; the RFC also identifies libffi as an open question. Done means a settled class_builder API can define methods and attributes and return a class_ while providing the proposed direct-slot performance path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, python
- Domain
- api, backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100