Skip to content

Commit 3ec966d

Browse files
Merge #2881
2881: Rework `PyAny::is_instance_of` for performance r=adamreichold a=davidhewitt I was talking to `@samuelcolvin` about the fastest way to identify object types (relevant e.g. for `pythonize` and also `pydantic-core`) and noticed that `PyAny::is_instance_of` is quite unoptimised because it expands to an ffi call to `PyObject_IsInstance`. This PR proposes `PyAny::is_instance_of::<T>(obj)` is changed to be equivalent to `T::is_type_of(obj)`, plus add a sprinkling of inlining. We often have implementations such as `PyDict_Check` which can pretty much be optimised away to just checking a bit on the type object. The accompanying benchmark to run through a bunch of object types is approx 40% faster after making this change. For completeness, I've also added `PyAny::is_exact_instance` and `PyAny::is_exact_instance_of`, to pair with `T::is_exact_type_of`. (This could be split into a separate PR if preferred.) Co-authored-by: David Hewitt <[email protected]>
2 parents c2986df + 248230b commit 3ec966d

File tree

11 files changed

+113
-22
lines changed

11 files changed

+113
-22
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ full = [
114114
"rust_decimal",
115115
]
116116

117+
[[bench]]
118+
name = "bench_any"
119+
harness = false
120+
117121
[[bench]]
118122
name = "bench_call"
119123
harness = false

benches/bench_any.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use criterion::{criterion_group, criterion_main, Bencher, Criterion};
2+
3+
use pyo3::{
4+
types::{
5+
PyBool, PyByteArray, PyBytes, PyDict, PyFloat, PyFrozenSet, PyInt, PyList, PyMapping,
6+
PySequence, PySet, PyString, PyTuple,
7+
},
8+
PyAny, Python,
9+
};
10+
11+
#[derive(PartialEq, Eq, Debug)]
12+
enum ObjectType {
13+
None,
14+
Bool,
15+
ByteArray,
16+
Bytes,
17+
Dict,
18+
Float,
19+
FrozenSet,
20+
Int,
21+
List,
22+
Set,
23+
Str,
24+
Tuple,
25+
Sequence,
26+
Mapping,
27+
Unknown,
28+
}
29+
30+
fn find_object_type(obj: &PyAny) -> ObjectType {
31+
if obj.is_none() {
32+
ObjectType::None
33+
} else if obj.is_instance_of::<PyBool>() {
34+
ObjectType::Bool
35+
} else if obj.is_instance_of::<PyByteArray>() {
36+
ObjectType::ByteArray
37+
} else if obj.is_instance_of::<PyBytes>() {
38+
ObjectType::Bytes
39+
} else if obj.is_instance_of::<PyDict>() {
40+
ObjectType::Dict
41+
} else if obj.is_instance_of::<PyFloat>() {
42+
ObjectType::Float
43+
} else if obj.is_instance_of::<PyFrozenSet>() {
44+
ObjectType::FrozenSet
45+
} else if obj.is_instance_of::<PyInt>() {
46+
ObjectType::Int
47+
} else if obj.is_instance_of::<PyList>() {
48+
ObjectType::List
49+
} else if obj.is_instance_of::<PySet>() {
50+
ObjectType::Set
51+
} else if obj.is_instance_of::<PyString>() {
52+
ObjectType::Str
53+
} else if obj.is_instance_of::<PyTuple>() {
54+
ObjectType::Tuple
55+
} else if obj.downcast::<PySequence>().is_ok() {
56+
ObjectType::Sequence
57+
} else if obj.downcast::<PyMapping>().is_ok() {
58+
ObjectType::Mapping
59+
} else {
60+
ObjectType::Unknown
61+
}
62+
}
63+
64+
fn bench_identify_object_type(b: &mut Bencher<'_>) {
65+
Python::with_gil(|py| {
66+
let obj = py.eval("object()", None, None).unwrap();
67+
68+
b.iter(|| find_object_type(obj));
69+
70+
assert_eq!(find_object_type(obj), ObjectType::Unknown);
71+
});
72+
}
73+
74+
fn criterion_benchmark(c: &mut Criterion) {
75+
c.bench_function("identify_object_type", bench_identify_object_type);
76+
}
77+
78+
criterion_group!(benches, criterion_benchmark);
79+
criterion_main!(benches);

guide/src/exception.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ use pyo3::Python;
7979
use pyo3::types::{PyBool, PyList};
8080

8181
Python::with_gil(|py| {
82-
assert!(PyBool::new(py, true).is_instance_of::<PyBool>().unwrap());
82+
assert!(PyBool::new(py, true).is_instance_of::<PyBool>());
8383
let list = PyList::new(py, &[1, 2, 3, 4]);
84-
assert!(!list.is_instance_of::<PyBool>().unwrap());
85-
assert!(list.is_instance_of::<PyList>().unwrap());
84+
assert!(!list.is_instance_of::<PyBool>());
85+
assert!(list.is_instance_of::<PyList>());
8686
});
8787
```
8888

newsfragments/2881.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`PyAny::is_instance_of::<T>(obj)` is now equivalent to `T::is_type_of(obj)`, and now returns `bool` instead of `PyResult<bool>`.

src/type_object.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,19 @@ pub unsafe trait PyTypeInfo: Sized {
4747
fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject;
4848

4949
/// Returns the safe abstraction over the type object.
50+
#[inline]
5051
fn type_object(py: Python<'_>) -> &PyType {
5152
unsafe { py.from_borrowed_ptr(Self::type_object_raw(py) as _) }
5253
}
5354

5455
/// Checks if `object` is an instance of this type or a subclass of this type.
56+
#[inline]
5557
fn is_type_of(object: &PyAny) -> bool {
5658
unsafe { ffi::PyObject_TypeCheck(object.as_ptr(), Self::type_object_raw(object.py())) != 0 }
5759
}
5860

5961
/// Checks if `object` is an instance of this type.
62+
#[inline]
6063
fn is_exact_type_of(object: &PyAny) -> bool {
6164
unsafe { ffi::Py_TYPE(object.as_ptr()) == Self::type_object_raw(object.py()) }
6265
}

src/types/any.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,7 @@ impl PyAny {
665665
/// Returns whether the object is considered to be None.
666666
///
667667
/// This is equivalent to the Python expression `self is None`.
668+
#[inline]
668669
pub fn is_none(&self) -> bool {
669670
unsafe { ffi::Py_None() == self.as_ptr() }
670671
}
@@ -778,7 +779,7 @@ impl PyAny {
778779
///
779780
/// Python::with_gil(|py| {
780781
/// let dict = PyDict::new(py);
781-
/// assert!(dict.is_instance_of::<PyAny>().unwrap());
782+
/// assert!(dict.is_instance_of::<PyAny>());
782783
/// let any: &PyAny = dict.as_ref();
783784
///
784785
/// assert!(any.downcast::<PyDict>().is_ok());
@@ -904,6 +905,7 @@ impl PyAny {
904905
/// Checks whether this object is an instance of type `ty`.
905906
///
906907
/// This is equivalent to the Python expression `isinstance(self, ty)`.
908+
#[inline]
907909
pub fn is_instance(&self, ty: &PyAny) -> PyResult<bool> {
908910
let result = unsafe { ffi::PyObject_IsInstance(self.as_ptr(), ty.as_ptr()) };
909911
err::error_on_minusone(self.py(), result)?;
@@ -914,8 +916,9 @@ impl PyAny {
914916
///
915917
/// This is equivalent to the Python expression `isinstance(self, T)`,
916918
/// if the type `T` is known at compile time.
917-
pub fn is_instance_of<T: PyTypeInfo>(&self) -> PyResult<bool> {
918-
self.is_instance(T::type_object(self.py()))
919+
#[inline]
920+
pub fn is_instance_of<T: PyTypeInfo>(&self) -> bool {
921+
T::is_type_of(self)
919922
}
920923

921924
/// Determines if self contains `value`.
@@ -1043,10 +1046,10 @@ class SimpleClass:
10431046
fn test_any_isinstance_of() {
10441047
Python::with_gil(|py| {
10451048
let x = 5.to_object(py).into_ref(py);
1046-
assert!(x.is_instance_of::<PyLong>().unwrap());
1049+
assert!(x.is_instance_of::<PyLong>());
10471050

10481051
let l = vec![x, x].to_object(py).into_ref(py);
1049-
assert!(l.is_instance_of::<PyList>().unwrap());
1052+
assert!(l.is_instance_of::<PyList>());
10501053
});
10511054
}
10521055

src/types/bytes.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,10 @@ mod tests {
219219
.unwrap_err();
220220

221221
let cow = Cow::<[u8]>::Borrowed(b"foobar").to_object(py);
222-
assert!(cow.as_ref(py).is_instance_of::<PyBytes>().unwrap());
222+
assert!(cow.as_ref(py).is_instance_of::<PyBytes>());
223223

224224
let cow = Cow::<[u8]>::Owned(b"foobar".to_vec()).to_object(py);
225-
assert!(cow.as_ref(py).is_instance_of::<PyBytes>().unwrap());
225+
assert!(cow.as_ref(py).is_instance_of::<PyBytes>());
226226
});
227227
}
228228
}

src/types/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ macro_rules! pyobject_native_type_info(
202202
}
203203

204204
$(
205+
#[inline]
205206
fn is_type_of(ptr: &$crate::PyAny) -> bool {
206207
use $crate::AsPyPointer;
207208
#[allow(unused_unsafe)]

src/types/sequence.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ where
297297
T: FromPyObject<'a>,
298298
{
299299
fn extract(obj: &'a PyAny) -> PyResult<Self> {
300-
if let Ok(true) = obj.is_instance_of::<PyString>() {
300+
if obj.is_instance_of::<PyString>() {
301301
return Err(PyTypeError::new_err("Can't extract `str` to `Vec`"));
302302
}
303303
extract_sequence(obj)

tests/test_datetime.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ fn test_date_check() {
6161
assert_check_exact!(PyDate_Check, PyDate_CheckExact, obj);
6262
assert_check_only!(PyDate_Check, PyDate_CheckExact, sub_obj);
6363
assert_check_only!(PyDate_Check, PyDate_CheckExact, sub_sub_obj);
64-
assert!(obj.is_instance_of::<PyDate>().unwrap());
65-
assert!(!obj.is_instance_of::<PyTime>().unwrap());
66-
assert!(!obj.is_instance_of::<PyDateTime>().unwrap());
64+
assert!(obj.is_instance_of::<PyDate>());
65+
assert!(!obj.is_instance_of::<PyTime>());
66+
assert!(!obj.is_instance_of::<PyDateTime>());
6767
});
6868
}
6969

@@ -76,9 +76,9 @@ fn test_time_check() {
7676
assert_check_exact!(PyTime_Check, PyTime_CheckExact, obj);
7777
assert_check_only!(PyTime_Check, PyTime_CheckExact, sub_obj);
7878
assert_check_only!(PyTime_Check, PyTime_CheckExact, sub_sub_obj);
79-
assert!(!obj.is_instance_of::<PyDate>().unwrap());
80-
assert!(obj.is_instance_of::<PyTime>().unwrap());
81-
assert!(!obj.is_instance_of::<PyDateTime>().unwrap());
79+
assert!(!obj.is_instance_of::<PyDate>());
80+
assert!(obj.is_instance_of::<PyTime>());
81+
assert!(!obj.is_instance_of::<PyDateTime>());
8282
});
8383
}
8484

@@ -94,9 +94,9 @@ fn test_datetime_check() {
9494
assert_check_exact!(PyDateTime_Check, PyDateTime_CheckExact, obj);
9595
assert_check_only!(PyDateTime_Check, PyDateTime_CheckExact, sub_obj);
9696
assert_check_only!(PyDateTime_Check, PyDateTime_CheckExact, sub_sub_obj);
97-
assert!(obj.is_instance_of::<PyDate>().unwrap());
98-
assert!(!obj.is_instance_of::<PyTime>().unwrap());
99-
assert!(obj.is_instance_of::<PyDateTime>().unwrap());
97+
assert!(obj.is_instance_of::<PyDate>());
98+
assert!(!obj.is_instance_of::<PyTime>());
99+
assert!(obj.is_instance_of::<PyDateTime>());
100100
});
101101
}
102102

tests/test_inheritance.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ fn is_subclass_and_is_instance() {
113113
assert!(sub_ty.is_subclass(base_ty).unwrap());
114114

115115
let obj = PyCell::new(py, SubClass::new()).unwrap();
116-
assert!(obj.is_instance_of::<SubClass>().unwrap());
117-
assert!(obj.is_instance_of::<BaseClass>().unwrap());
116+
assert!(obj.is_instance_of::<SubClass>());
117+
assert!(obj.is_instance_of::<BaseClass>());
118118
assert!(obj.is_instance(sub_ty).unwrap());
119119
assert!(obj.is_instance(base_ty).unwrap());
120120
});

0 commit comments

Comments
 (0)