pybind / pybind/pybind11

[FEAT] [RFC] Class builder API for high performance bindings

Open
#2,810 15 comments 1 reaction 0 assignees View on GitHub

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 PyCFunction wrapper capturing a capsule holding a detail::function_record
  • ... wrapped in an instancemethod
  • ... wrapped in a property stored 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_function into 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_builder API, or should we just rework the internals of pybind11::class_? Personally I think it depends on whether we are allowed to add new PyMethodDef and PyGetSetDef after calling PyType_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 with PyMethodDef, because PyMethodDef has no field to store the closure pointer (PyGetSetDef has a closure pointer and does not have this problem). Perhaps we will need the closure functionality from libffi to solve this problem?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.