improve performance of successful int extract by ~30%

add newsfragment

formatting

skip slow path on 3.8+

formatting

cfg if,else

formatting again

dedicated macro, change int_convert_u64_or_i64 too

add float tests

force index call for PyLong_AsUnsignedLongLong

perform PyLong check for 3.8 too

perform PyLong check for <3.10
This commit is contained in:
Samuel Colvin 2024-01-14 15:39:05 +00:00
parent 1520b058e8
commit 0e876d94d6
No known key found for this signature in database
GPG Key ID: 23D29BED9828A51C
2 changed files with 62 additions and 26 deletions

View File

@ -0,0 +1 @@
Improve performance of `extract::<i64>` (and other integer types) by avoiding call to `__index__()` converting the value to an integer for 3.10+. Gives performance improvement of around 30% for successful extraction.

View File

@ -44,8 +44,39 @@ macro_rules! int_fits_larger_int {
}; };
} }
macro_rules! extract_int {
($obj:ident, $error_val:expr, $pylong_as:expr) => {
extract_int!($obj, $error_val, $pylong_as, false)
};
($obj:ident, $error_val:expr, $pylong_as:expr, $force_index_call: literal) => {
// In python 3.8+ `PyLong_AsLong` and friends takes care of calling `PyNumber_Index`,
// however 3.8 & 3.9 do lossy conversion of floats, hence we only use the
// simplest logic for 3.10+ where that was fixed - python/cpython#82180.
// `PyLong_AsUnsignedLongLong` does not call `PyNumber_Index`, hence the `force_index_call` argument
// See https://github.com/PyO3/pyo3/pull/3742 for detials
if cfg!(Py_3_10) && !$force_index_call {
err_if_invalid_value($obj.py(), $error_val, unsafe { $pylong_as($obj.as_ptr()) })
} else if let Ok(long) = $obj.downcast::<crate::types::PyLong>() {
// fast path - checking for subclass of `int` just checks a bit in the type $object
err_if_invalid_value($obj.py(), $error_val, unsafe { $pylong_as(long.as_ptr()) })
} else {
unsafe {
let num = ffi::PyNumber_Index($obj.as_ptr());
if num.is_null() {
Err(PyErr::fetch($obj.py()))
} else {
let result = err_if_invalid_value($obj.py(), $error_val, $pylong_as(num));
ffi::Py_DECREF(num);
result
}
}
}
};
}
macro_rules! int_convert_u64_or_i64 { macro_rules! int_convert_u64_or_i64 {
($rust_type:ty, $pylong_from_ll_or_ull:expr, $pylong_as_ll_or_ull:expr) => { ($rust_type:ty, $pylong_from_ll_or_ull:expr, $pylong_as_ll_or_ull:expr, $force_index_call:literal) => {
impl ToPyObject for $rust_type { impl ToPyObject for $rust_type {
#[inline] #[inline]
fn to_object(&self, py: Python<'_>) -> PyObject { fn to_object(&self, py: Python<'_>) -> PyObject {
@ -64,18 +95,8 @@ macro_rules! int_convert_u64_or_i64 {
} }
} }
impl<'source> FromPyObject<'source> for $rust_type { impl<'source> FromPyObject<'source> for $rust_type {
fn extract(ob: &'source PyAny) -> PyResult<$rust_type> { fn extract(obj: &'source PyAny) -> PyResult<$rust_type> {
let ptr = ob.as_ptr(); extract_int!(obj, !0, $pylong_as_ll_or_ull, $force_index_call)
unsafe {
let num = ffi::PyNumber_Index(ptr);
if num.is_null() {
Err(PyErr::fetch(ob.py()))
} else {
let result = err_if_invalid_value(ob.py(), !0, $pylong_as_ll_or_ull(num));
ffi::Py_DECREF(num);
result
}
}
} }
#[cfg(feature = "experimental-inspect")] #[cfg(feature = "experimental-inspect")]
@ -106,17 +127,7 @@ macro_rules! int_fits_c_long {
impl<'source> FromPyObject<'source> for $rust_type { impl<'source> FromPyObject<'source> for $rust_type {
fn extract(obj: &'source PyAny) -> PyResult<Self> { fn extract(obj: &'source PyAny) -> PyResult<Self> {
let ptr = obj.as_ptr(); let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?;
let val = unsafe {
let num = ffi::PyNumber_Index(ptr);
if num.is_null() {
Err(PyErr::fetch(obj.py()))
} else {
let val = err_if_invalid_value(obj.py(), -1, ffi::PyLong_AsLong(num));
ffi::Py_DECREF(num);
val
}
}?;
<$rust_type>::try_from(val) <$rust_type>::try_from(val)
.map_err(|e| exceptions::PyOverflowError::new_err(e.to_string())) .map_err(|e| exceptions::PyOverflowError::new_err(e.to_string()))
} }
@ -146,7 +157,7 @@ int_fits_c_long!(i64);
// manual implementation for i64 on systems with 32-bit long // manual implementation for i64 on systems with 32-bit long
#[cfg(any(target_pointer_width = "32", target_os = "windows"))] #[cfg(any(target_pointer_width = "32", target_os = "windows"))]
int_convert_u64_or_i64!(i64, ffi::PyLong_FromLongLong, ffi::PyLong_AsLongLong); int_convert_u64_or_i64!(i64, ffi::PyLong_FromLongLong, ffi::PyLong_AsLongLong, false);
#[cfg(all(target_pointer_width = "64", not(target_os = "windows")))] #[cfg(all(target_pointer_width = "64", not(target_os = "windows")))]
int_fits_c_long!(isize); int_fits_c_long!(isize);
@ -159,7 +170,8 @@ int_fits_larger_int!(usize, u64);
int_convert_u64_or_i64!( int_convert_u64_or_i64!(
u64, u64,
ffi::PyLong_FromUnsignedLongLong, ffi::PyLong_FromUnsignedLongLong,
ffi::PyLong_AsUnsignedLongLong ffi::PyLong_AsUnsignedLongLong,
true
); );
#[cfg(not(Py_LIMITED_API))] #[cfg(not(Py_LIMITED_API))]
@ -738,4 +750,27 @@ mod tests {
test_nonzero_common!(nonzero_usize, NonZeroUsize); test_nonzero_common!(nonzero_usize, NonZeroUsize);
test_nonzero_common!(nonzero_i128, NonZeroI128); test_nonzero_common!(nonzero_i128, NonZeroI128);
test_nonzero_common!(nonzero_u128, NonZeroU128); test_nonzero_common!(nonzero_u128, NonZeroU128);
#[test]
fn test_i64_bool() {
Python::with_gil(|py| {
let obj = true.to_object(py);
assert_eq!(1, obj.extract::<i64>(py).unwrap());
let obj = false.to_object(py);
assert_eq!(0, obj.extract::<i64>(py).unwrap());
})
}
#[test]
fn test_i64_f64() {
Python::with_gil(|py| {
let obj = 12.34f64.to_object(py);
let err = obj.extract::<i64>(py).unwrap_err();
assert!(err.is_instance_of::<crate::exceptions::PyTypeError>(py));
// with no remainder
let obj = 12f64.to_object(py);
let err = obj.extract::<i64>(py).unwrap_err();
assert!(err.is_instance_of::<crate::exceptions::PyTypeError>(py));
})
}
} }