Skip to content

Commit 3965f5f

Browse files
authored
reintroduce vectorcall optimization with new PyCallArgs trait (#4768)
1 parent 9e63b34 commit 3965f5f

File tree

10 files changed

+484
-29
lines changed

10 files changed

+484
-29
lines changed

guide/src/performance.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ impl PartialEq<Foo> for FooBound<'_> {
9797
}
9898
```
9999

100+
## Calling Python callables (`__call__`)
101+
CPython support multiple calling protocols: [`tp_call`] and [`vectorcall`]. [`vectorcall`] is a more efficient protocol unlocking faster calls.
102+
PyO3 will try to dispatch Python `call`s using the [`vectorcall`] calling convention to archive maximum performance if possible and falling back to [`tp_call`] otherwise.
103+
This is implemented using the (internal) `PyCallArgs` trait. It defines how Rust types can be used as Python `call` arguments. This trait is currently implemented for
104+
- Rust tuples, where each member implements `IntoPyObject`,
105+
- `Bound<'_, PyTuple>`
106+
- `Py<PyTuple>`
107+
Rust tuples may make use of [`vectorcall`] where as `Bound<'_, PyTuple>` and `Py<PyTuple>` can only use [`tp_call`]. For maximum performance prefer using Rust tuples as arguments.
108+
109+
110+
[`tp_call`]: https://docs.python.org/3/c-api/call.html#the-tp-call-protocol
111+
[`vectorcall`]: https://docs.python.org/3/c-api/call.html#the-vectorcall-protocol
112+
100113
## Disable the global reference pool
101114

102115
PyO3 uses global mutable state to keep track of deferred reference count updates implied by `impl<T> Drop for Py<T>` being called without the GIL being held. The necessary synchronization to obtain and apply these reference count updates when PyO3-based code next acquires the GIL is somewhat expensive and can become a significant part of the cost of crossing the Python-Rust boundary.

newsfragments/4768.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added `PyCallArgs` trait for arguments into the Python calling protocol. This enabled using a faster calling convention for certain types, improving performance.

newsfragments/4768.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`PyAnyMethods::call` an friends now require `PyCallArgs` for their positional arguments.

src/call.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
//! Defines how Python calls are dispatched, see [`PyCallArgs`].for more information.
2+
3+
use crate::ffi_ptr_ext::FfiPtrExt as _;
4+
use crate::types::{PyAnyMethods as _, PyDict, PyString, PyTuple};
5+
use crate::{ffi, Borrowed, Bound, IntoPyObjectExt as _, Py, PyAny, PyResult};
6+
7+
pub(crate) mod private {
8+
use super::*;
9+
10+
pub trait Sealed {}
11+
12+
impl Sealed for () {}
13+
impl Sealed for Bound<'_, PyTuple> {}
14+
impl Sealed for Py<PyTuple> {}
15+
16+
pub struct Token;
17+
}
18+
19+
/// This trait marks types that can be used as arguments to Python function
20+
/// calls.
21+
///
22+
/// This trait is currently implemented for Rust tuple (up to a size of 12),
23+
/// [`Bound<'py, PyTuple>`] and [`Py<PyTuple>`]. Custom types that are
24+
/// convertable to `PyTuple` via `IntoPyObject` need to do so before passing it
25+
/// to `call`.
26+
///
27+
/// This trait is not intended to used by downstream crates directly. As such it
28+
/// has no publicly available methods and cannot be implemented ouside of
29+
/// `pyo3`. The corresponding public API is available through [`call`]
30+
/// ([`call0`], [`call1`] and friends) on [`PyAnyMethods`].
31+
///
32+
/// # What is `PyCallArgs` used for?
33+
/// `PyCallArgs` is used internally in `pyo3` to dispatch the Python calls in
34+
/// the most optimal way for the current build configuration. Certain types,
35+
/// such as Rust tuples, do allow the usage of a faster calling convention of
36+
/// the Python interpreter (if available). More types that may take advantage
37+
/// from this may be added in the future.
38+
///
39+
/// [`call0`]: crate::types::PyAnyMethods::call0
40+
/// [`call1`]: crate::types::PyAnyMethods::call1
41+
/// [`call`]: crate::types::PyAnyMethods::call
42+
/// [`PyAnyMethods`]: crate::types::PyAnyMethods
43+
#[cfg_attr(
44+
diagnostic_namespace,
45+
diagnostic::on_unimplemented(
46+
message = "`{Self}` cannot used as a Python `call` argument",
47+
note = "`PyCallArgs` is implemented for Rust tuples, `Bound<'py, PyTuple>` and `Py<PyTuple>`",
48+
note = "if your type is convertable to `PyTuple` via `IntoPyObject`, call `<arg>.into_pyobject(py)` manually",
49+
note = "if you meant to pass the type as a single argument, wrap it in a 1-tuple, `(<arg>,)`"
50+
)
51+
)]
52+
pub trait PyCallArgs<'py>: Sized + private::Sealed {
53+
#[doc(hidden)]
54+
fn call(
55+
self,
56+
function: Borrowed<'_, 'py, PyAny>,
57+
kwargs: Borrowed<'_, 'py, PyDict>,
58+
token: private::Token,
59+
) -> PyResult<Bound<'py, PyAny>>;
60+
61+
#[doc(hidden)]
62+
fn call_positional(
63+
self,
64+
function: Borrowed<'_, 'py, PyAny>,
65+
token: private::Token,
66+
) -> PyResult<Bound<'py, PyAny>>;
67+
68+
#[doc(hidden)]
69+
fn call_method_positional(
70+
self,
71+
object: Borrowed<'_, 'py, PyAny>,
72+
method_name: Borrowed<'_, 'py, PyString>,
73+
_: private::Token,
74+
) -> PyResult<Bound<'py, PyAny>> {
75+
object
76+
.getattr(method_name)
77+
.and_then(|method| method.call1(self))
78+
}
79+
}
80+
81+
impl<'py> PyCallArgs<'py> for () {
82+
fn call(
83+
self,
84+
function: Borrowed<'_, 'py, PyAny>,
85+
kwargs: Borrowed<'_, 'py, PyDict>,
86+
token: private::Token,
87+
) -> PyResult<Bound<'py, PyAny>> {
88+
let args = self.into_pyobject_or_pyerr(function.py())?;
89+
args.call(function, kwargs, token)
90+
}
91+
92+
fn call_positional(
93+
self,
94+
function: Borrowed<'_, 'py, PyAny>,
95+
token: private::Token,
96+
) -> PyResult<Bound<'py, PyAny>> {
97+
let args = self.into_pyobject_or_pyerr(function.py())?;
98+
args.call_positional(function, token)
99+
}
100+
}
101+
102+
impl<'py> PyCallArgs<'py> for Bound<'py, PyTuple> {
103+
fn call(
104+
self,
105+
function: Borrowed<'_, 'py, PyAny>,
106+
kwargs: Borrowed<'_, '_, PyDict>,
107+
_: private::Token,
108+
) -> PyResult<Bound<'py, PyAny>> {
109+
unsafe {
110+
ffi::PyObject_Call(function.as_ptr(), self.as_ptr(), kwargs.as_ptr())
111+
.assume_owned_or_err(function.py())
112+
}
113+
}
114+
115+
fn call_positional(
116+
self,
117+
function: Borrowed<'_, 'py, PyAny>,
118+
_: private::Token,
119+
) -> PyResult<Bound<'py, PyAny>> {
120+
unsafe {
121+
ffi::PyObject_Call(function.as_ptr(), self.as_ptr(), std::ptr::null_mut())
122+
.assume_owned_or_err(function.py())
123+
}
124+
}
125+
}
126+
127+
impl<'py> PyCallArgs<'py> for Py<PyTuple> {
128+
fn call(
129+
self,
130+
function: Borrowed<'_, 'py, PyAny>,
131+
kwargs: Borrowed<'_, '_, PyDict>,
132+
_: private::Token,
133+
) -> PyResult<Bound<'py, PyAny>> {
134+
unsafe {
135+
ffi::PyObject_Call(function.as_ptr(), self.as_ptr(), kwargs.as_ptr())
136+
.assume_owned_or_err(function.py())
137+
}
138+
}
139+
140+
fn call_positional(
141+
self,
142+
function: Borrowed<'_, 'py, PyAny>,
143+
_: private::Token,
144+
) -> PyResult<Bound<'py, PyAny>> {
145+
unsafe {
146+
ffi::PyObject_Call(function.as_ptr(), self.as_ptr(), std::ptr::null_mut())
147+
.assume_owned_or_err(function.py())
148+
}
149+
}
150+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,7 @@ mod internal_tricks;
428428
mod internal;
429429

430430
pub mod buffer;
431+
pub mod call;
431432
pub mod conversion;
432433
mod conversions;
433434
#[cfg(feature = "experimental-async")]

src/types/any.rs

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::call::PyCallArgs;
12
use crate::class::basic::CompareOp;
23
use crate::conversion::{AsPyPointer, FromPyObjectBound, IntoPyObject};
34
use crate::err::{DowncastError, DowncastIntoError, PyErr, PyResult};
@@ -10,7 +11,7 @@ use crate::py_result_ext::PyResultExt;
1011
use crate::type_object::{PyTypeCheck, PyTypeInfo};
1112
#[cfg(not(any(PyPy, GraalPy)))]
1213
use crate::types::PySuper;
13-
use crate::types::{PyDict, PyIterator, PyList, PyString, PyTuple, PyType};
14+
use crate::types::{PyDict, PyIterator, PyList, PyString, PyType};
1415
use crate::{err, ffi, Borrowed, BoundObject, IntoPyObjectExt, Python};
1516
use std::cell::UnsafeCell;
1617
use std::cmp::Ordering;
@@ -436,7 +437,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
436437
/// ```
437438
fn call<A>(&self, args: A, kwargs: Option<&Bound<'py, PyDict>>) -> PyResult<Bound<'py, PyAny>>
438439
where
439-
A: IntoPyObject<'py, Target = PyTuple>;
440+
A: PyCallArgs<'py>;
440441

441442
/// Calls the object without arguments.
442443
///
@@ -491,7 +492,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
491492
/// ```
492493
fn call1<A>(&self, args: A) -> PyResult<Bound<'py, PyAny>>
493494
where
494-
A: IntoPyObject<'py, Target = PyTuple>;
495+
A: PyCallArgs<'py>;
495496

496497
/// Calls a method on the object.
497498
///
@@ -538,7 +539,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
538539
) -> PyResult<Bound<'py, PyAny>>
539540
where
540541
N: IntoPyObject<'py, Target = PyString>,
541-
A: IntoPyObject<'py, Target = PyTuple>;
542+
A: PyCallArgs<'py>;
542543

543544
/// Calls a method on the object without arguments.
544545
///
@@ -614,7 +615,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
614615
fn call_method1<N, A>(&self, name: N, args: A) -> PyResult<Bound<'py, PyAny>>
615616
where
616617
N: IntoPyObject<'py, Target = PyString>,
617-
A: IntoPyObject<'py, Target = PyTuple>;
618+
A: PyCallArgs<'py>;
618619

619620
/// Returns whether the object is considered to be true.
620621
///
@@ -1209,25 +1210,17 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
12091210

12101211
fn call<A>(&self, args: A, kwargs: Option<&Bound<'py, PyDict>>) -> PyResult<Bound<'py, PyAny>>
12111212
where
1212-
A: IntoPyObject<'py, Target = PyTuple>,
1213+
A: PyCallArgs<'py>,
12131214
{
1214-
fn inner<'py>(
1215-
any: &Bound<'py, PyAny>,
1216-
args: Borrowed<'_, 'py, PyTuple>,
1217-
kwargs: Option<&Bound<'py, PyDict>>,
1218-
) -> PyResult<Bound<'py, PyAny>> {
1219-
unsafe {
1220-
ffi::PyObject_Call(
1221-
any.as_ptr(),
1222-
args.as_ptr(),
1223-
kwargs.map_or(std::ptr::null_mut(), |dict| dict.as_ptr()),
1224-
)
1225-
.assume_owned_or_err(any.py())
1226-
}
1215+
if let Some(kwargs) = kwargs {
1216+
args.call(
1217+
self.as_borrowed(),
1218+
kwargs.as_borrowed(),
1219+
crate::call::private::Token,
1220+
)
1221+
} else {
1222+
args.call_positional(self.as_borrowed(), crate::call::private::Token)
12271223
}
1228-
1229-
let py = self.py();
1230-
inner(self, args.into_pyobject_or_pyerr(py)?.as_borrowed(), kwargs)
12311224
}
12321225

12331226
#[inline]
@@ -1237,9 +1230,9 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
12371230

12381231
fn call1<A>(&self, args: A) -> PyResult<Bound<'py, PyAny>>
12391232
where
1240-
A: IntoPyObject<'py, Target = PyTuple>,
1233+
A: PyCallArgs<'py>,
12411234
{
1242-
self.call(args, None)
1235+
args.call_positional(self.as_borrowed(), crate::call::private::Token)
12431236
}
12441237

12451238
#[inline]
@@ -1251,10 +1244,14 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
12511244
) -> PyResult<Bound<'py, PyAny>>
12521245
where
12531246
N: IntoPyObject<'py, Target = PyString>,
1254-
A: IntoPyObject<'py, Target = PyTuple>,
1247+
A: PyCallArgs<'py>,
12551248
{
1256-
self.getattr(name)
1257-
.and_then(|method| method.call(args, kwargs))
1249+
if kwargs.is_none() {
1250+
self.call_method1(name, args)
1251+
} else {
1252+
self.getattr(name)
1253+
.and_then(|method| method.call(args, kwargs))
1254+
}
12581255
}
12591256

12601257
#[inline]
@@ -1273,9 +1270,14 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
12731270
fn call_method1<N, A>(&self, name: N, args: A) -> PyResult<Bound<'py, PyAny>>
12741271
where
12751272
N: IntoPyObject<'py, Target = PyString>,
1276-
A: IntoPyObject<'py, Target = PyTuple>,
1273+
A: PyCallArgs<'py>,
12771274
{
1278-
self.call_method(name, args, None)
1275+
let name = name.into_pyobject_or_pyerr(self.py())?;
1276+
args.call_method_positional(
1277+
self.as_borrowed(),
1278+
name.as_borrowed(),
1279+
crate::call::private::Token,
1280+
)
12791281
}
12801282

12811283
fn is_truthy(&self) -> PyResult<bool> {

0 commit comments

Comments
 (0)