Skip to content

Commit 06401a6

Browse files
Use vectorcall (where possible) when calling Python functions
This works without any changes to user code. The way it works is by creating a methods on `IntoPy` to call functions, and specializing them for tuples. This currently supports only non-kwargs for methods, and kwargs with somewhat slow approach (converting from PyDict) for functions. This can be improved, but that will require additional API. We may consider adding more impls IntoPy<Py<PyTuple>> that specialize (for example, for arrays and `Vec`), but this i a good start.
1 parent 57d85b1 commit 06401a6

File tree

5 files changed

+285
-53
lines changed

5 files changed

+285
-53
lines changed

newsfragments/4456.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve performance of calls to Python by using the vectorcall calling convention where possible.

pyo3-ffi/src/cpython/abstract_.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ extern "C" {
4040
}
4141

4242
#[cfg(Py_3_8)]
43-
const PY_VECTORCALL_ARGUMENTS_OFFSET: size_t =
43+
pub const PY_VECTORCALL_ARGUMENTS_OFFSET: size_t =
4444
1 << (8 * std::mem::size_of::<size_t>() as size_t - 1);
4545

4646
#[cfg(Py_3_8)]

src/conversion.rs

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
//! Defines conversions between Rust and Python types.
22
use crate::err::PyResult;
3+
use crate::ffi_ptr_ext::FfiPtrExt;
34
#[cfg(feature = "experimental-inspect")]
45
use crate::inspect::types::TypeInfo;
56
use crate::pyclass::boolean_struct::False;
67
use crate::types::any::PyAnyMethods;
7-
use crate::types::PyTuple;
8+
use crate::types::{PyDict, PyString, PyTuple};
89
use crate::{
910
ffi, Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyErr, PyObject, PyRef, PyRefMut, Python,
1011
};
@@ -172,6 +173,90 @@ pub trait IntoPy<T>: Sized {
172173
fn type_output() -> TypeInfo {
173174
TypeInfo::Any
174175
}
176+
177+
// The following methods are helpers to use the vectorcall API where possible.
178+
// They are overridden on tuples to perform a vectorcall.
179+
// Be careful when you're implementing these: they can never refer to `Bound` call methods,
180+
// as those refer to these methods, so this will create an infinite recursion.
181+
#[doc(hidden)]
182+
#[inline]
183+
fn __py_call_vectorcall1<'py>(
184+
self,
185+
py: Python<'py>,
186+
function: Borrowed<'_, 'py, PyAny>,
187+
) -> PyResult<Bound<'py, PyAny>>
188+
where
189+
Self: IntoPy<Py<PyTuple>>,
190+
{
191+
#[inline]
192+
fn inner<'py>(
193+
py: Python<'py>,
194+
function: Borrowed<'_, 'py, PyAny>,
195+
args: Bound<'py, PyTuple>,
196+
) -> PyResult<Bound<'py, PyAny>> {
197+
unsafe {
198+
ffi::PyObject_Call(function.as_ptr(), args.as_ptr(), std::ptr::null_mut())
199+
.assume_owned_or_err(py)
200+
}
201+
}
202+
inner(
203+
py,
204+
function,
205+
<Self as IntoPy<Py<PyTuple>>>::into_py(self, py).into_bound(py),
206+
)
207+
}
208+
209+
#[doc(hidden)]
210+
#[inline]
211+
fn __py_call_vectorcall<'py>(
212+
self,
213+
py: Python<'py>,
214+
function: Borrowed<'_, 'py, PyAny>,
215+
kwargs: Option<Borrowed<'_, '_, PyDict>>,
216+
) -> PyResult<Bound<'py, PyAny>>
217+
where
218+
Self: IntoPy<Py<PyTuple>>,
219+
{
220+
#[inline]
221+
fn inner<'py>(
222+
py: Python<'py>,
223+
function: Borrowed<'_, 'py, PyAny>,
224+
args: Bound<'py, PyTuple>,
225+
kwargs: Option<Borrowed<'_, '_, PyDict>>,
226+
) -> PyResult<Bound<'py, PyAny>> {
227+
unsafe {
228+
ffi::PyObject_Call(
229+
function.as_ptr(),
230+
args.as_ptr(),
231+
kwargs.map_or_else(std::ptr::null_mut, |kwargs| kwargs.as_ptr()),
232+
)
233+
.assume_owned_or_err(py)
234+
}
235+
}
236+
inner(
237+
py,
238+
function,
239+
<Self as IntoPy<Py<PyTuple>>>::into_py(self, py).into_bound(py),
240+
kwargs,
241+
)
242+
}
243+
244+
#[doc(hidden)]
245+
#[inline]
246+
fn __py_call_method_vectorcall1<'py>(
247+
self,
248+
_py: Python<'py>,
249+
object: Borrowed<'_, 'py, PyAny>,
250+
method_name: Bound<'py, PyString>,
251+
) -> PyResult<Bound<'py, PyAny>>
252+
where
253+
Self: IntoPy<Py<PyTuple>>,
254+
{
255+
// Don't `self.into_py()`! This will lose the optimization of vectorcall.
256+
object
257+
.getattr(method_name)
258+
.and_then(|method| method.call1(self))
259+
}
175260
}
176261

177262
/// Defines a conversion from a Rust type to a Python object, which may fail.
@@ -502,6 +587,68 @@ impl IntoPy<Py<PyTuple>> for () {
502587
fn into_py(self, py: Python<'_>) -> Py<PyTuple> {
503588
PyTuple::empty(py).unbind()
504589
}
590+
591+
#[inline]
592+
fn __py_call_vectorcall1<'py>(
593+
self,
594+
py: Python<'py>,
595+
function: Borrowed<'_, 'py, PyAny>,
596+
) -> PyResult<Bound<'py, PyAny>> {
597+
unsafe {
598+
cfg_if::cfg_if! {
599+
if #[cfg(all(
600+
not(PyPy),
601+
not(GraalPy),
602+
any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // PyObject_CallNoArgs was added to python in 3.9 but to limited API in 3.10
603+
))] {
604+
// Optimized path on python 3.9+
605+
ffi::PyObject_CallNoArgs(function.as_ptr()).assume_owned_or_err(py)
606+
} else {
607+
ffi::PyObject_Call(function.as_ptr(), PyTuple::empty(py).as_ptr(), std::ptr::null_mut()).assume_owned_or_err(py)
608+
}
609+
}
610+
}
611+
}
612+
613+
#[inline]
614+
fn __py_call_vectorcall<'py>(
615+
self,
616+
py: Python<'py>,
617+
function: Borrowed<'_, 'py, PyAny>,
618+
kwargs: Option<Borrowed<'_, '_, PyDict>>,
619+
) -> PyResult<Bound<'py, PyAny>> {
620+
match kwargs {
621+
Some(kwargs) => unsafe {
622+
ffi::PyObject_Call(
623+
function.as_ptr(),
624+
PyTuple::empty(py).as_ptr(),
625+
kwargs.as_ptr(),
626+
)
627+
.assume_owned_or_err(py)
628+
},
629+
None => <Self as IntoPy<Py<PyTuple>>>::__py_call_vectorcall1(self, py, function),
630+
}
631+
}
632+
633+
#[inline]
634+
#[allow(clippy::used_underscore_binding)]
635+
fn __py_call_method_vectorcall1<'py>(
636+
self,
637+
_py: Python<'py>,
638+
object: Borrowed<'_, 'py, PyAny>,
639+
method_name: Bound<'py, PyString>,
640+
) -> PyResult<Bound<'py, PyAny>> {
641+
cfg_if::cfg_if! {
642+
if #[cfg(all(Py_3_9, not(any(Py_LIMITED_API, PyPy, GraalPy))))] {
643+
// Optimized path on python 3.9+
644+
unsafe {
645+
ffi::PyObject_CallMethodNoArgs(object.as_ptr(), method_name.as_ptr()).assume_owned_or_err(_py)
646+
}
647+
} else {
648+
object.getattr(method_name).and_then(|method| method.call0())
649+
}
650+
}
651+
}
505652
}
506653

507654
impl<'py> IntoPyObject<'py> for () {

src/types/any.rs

Lines changed: 27 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,46 +1160,25 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
11601160
args: impl IntoPy<Py<PyTuple>>,
11611161
kwargs: Option<&Bound<'_, PyDict>>,
11621162
) -> PyResult<Bound<'py, PyAny>> {
1163-
fn inner<'py>(
1164-
any: &Bound<'py, PyAny>,
1165-
args: Bound<'_, PyTuple>,
1166-
kwargs: Option<&Bound<'_, PyDict>>,
1167-
) -> PyResult<Bound<'py, PyAny>> {
1168-
unsafe {
1169-
ffi::PyObject_Call(
1170-
any.as_ptr(),
1171-
args.as_ptr(),
1172-
kwargs.map_or(std::ptr::null_mut(), |dict| dict.as_ptr()),
1173-
)
1174-
.assume_owned_or_err(any.py())
1175-
}
1176-
}
1177-
1178-
let py = self.py();
1179-
inner(self, args.into_py(py).into_bound(py), kwargs)
1163+
args.__py_call_vectorcall(
1164+
self.py(),
1165+
self.as_borrowed(),
1166+
kwargs.map(Bound::as_borrowed),
1167+
)
11801168
}
11811169

1170+
#[inline]
11821171
fn call0(&self) -> PyResult<Bound<'py, PyAny>> {
1183-
cfg_if::cfg_if! {
1184-
if #[cfg(all(
1185-
not(PyPy),
1186-
not(GraalPy),
1187-
any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // PyObject_CallNoArgs was added to python in 3.9 but to limited API in 3.10
1188-
))] {
1189-
// Optimized path on python 3.9+
1190-
unsafe {
1191-
ffi::PyObject_CallNoArgs(self.as_ptr()).assume_owned_or_err(self.py())
1192-
}
1193-
} else {
1194-
self.call((), None)
1195-
}
1196-
}
1172+
// This will use optimized call (if available), as per `<() as IntoPy<Py<PyTuple>::__py_call_vectorcall1()`.
1173+
self.call1(())
11971174
}
11981175

1176+
#[inline]
11991177
fn call1(&self, args: impl IntoPy<Py<PyTuple>>) -> PyResult<Bound<'py, PyAny>> {
1200-
self.call(args, None)
1178+
args.__py_call_vectorcall1(self.py(), self.as_borrowed())
12011179
}
12021180

1181+
#[inline]
12031182
fn call_method<N, A>(
12041183
&self,
12051184
name: N,
@@ -1210,35 +1189,35 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
12101189
N: IntoPy<Py<PyString>>,
12111190
A: IntoPy<Py<PyTuple>>,
12121191
{
1213-
self.getattr(name)
1214-
.and_then(|method| method.call(args, kwargs))
1192+
// Don't `args.into_py()`! This will lose the optimization of vectorcall.
1193+
match kwargs {
1194+
Some(_) => self
1195+
.getattr(name)
1196+
.and_then(|method| method.call(args, kwargs)),
1197+
None => self.call_method1(name, args),
1198+
}
12151199
}
12161200

1201+
#[inline]
12171202
fn call_method0<N>(&self, name: N) -> PyResult<Bound<'py, PyAny>>
12181203
where
12191204
N: IntoPy<Py<PyString>>,
12201205
{
1221-
cfg_if::cfg_if! {
1222-
if #[cfg(all(Py_3_9, not(any(Py_LIMITED_API, PyPy, GraalPy))))] {
1223-
let py = self.py();
1224-
1225-
// Optimized path on python 3.9+
1226-
unsafe {
1227-
let name = name.into_py(py).into_bound(py);
1228-
ffi::PyObject_CallMethodNoArgs(self.as_ptr(), name.as_ptr()).assume_owned_or_err(py)
1229-
}
1230-
} else {
1231-
self.call_method(name, (), None)
1232-
}
1233-
}
1206+
// This will use optimized call (if available), as per `<() as IntoPy<Py<PyTuple>::__py_call_method_vectorcall1()`.
1207+
self.call_method1(name, ())
12341208
}
12351209

1210+
#[inline]
12361211
fn call_method1<N, A>(&self, name: N, args: A) -> PyResult<Bound<'py, PyAny>>
12371212
where
12381213
N: IntoPy<Py<PyString>>,
12391214
A: IntoPy<Py<PyTuple>>,
12401215
{
1241-
self.call_method(name, args, None)
1216+
args.__py_call_method_vectorcall1(
1217+
self.py(),
1218+
self.as_borrowed(),
1219+
name.into_py(self.py()).into_bound(self.py()),
1220+
)
12421221
}
12431222

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

0 commit comments

Comments
 (0)