diff --git a/Cargo.toml b/Cargo.toml index 111834ca9..d76aa8a8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,10 +34,23 @@ rustc-hash = "2.0" [dev-dependencies] pyo3 = { version = "0.28.0", default-features = false, features = ["auto-initialize"]} nalgebra = { version = ">=0.30, <0.35", default-features = false, features = ["std"] } +criterion = "0.8.2" [build-dependencies] pyo3-build-config = { version = "0.28", features = ["resolve-config"]} +[[bench]] +name = "array" +harness = false + +[[bench]] +name = "array_like" +harness = false + +[[bench]] +name = "borrow" +harness = false + [package.metadata.docs.rs] all-features = true diff --git a/benches/array.rs b/benches/array.rs index 156a89c56..8d6693499 100644 --- a/benches/array.rs +++ b/benches/array.rs @@ -1,54 +1,54 @@ -#![feature(test)] - -extern crate test; -use test::{black_box, Bencher}; - -use std::ops::Range; +use std::{hint::black_box, ops::Range}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use numpy::{PyArray1, PyArray2, PyArray3}; use pyo3::{types::PyAnyMethods, Bound, IntoPyObjectExt, Python}; -#[bench] -fn extract_success(bencher: &mut Bencher) { +fn extract_success(c: &mut Criterion) { Python::attach(|py| { let any = PyArray2::::zeros(py, (10, 10), false).into_any(); - bencher.iter(|| { - black_box(&any) - .extract::>>() - .unwrap() + c.bench_function("extract_success", |b| { + b.iter(|| { + black_box(&any) + .extract::>>() + .unwrap() + }); }); }); } -#[bench] -fn extract_failure(bencher: &mut Bencher) { +fn extract_failure(c: &mut Criterion) { Python::attach(|py| { let any = PyArray2::::zeros(py, (10, 10), false).into_any(); - bencher.iter(|| { - black_box(&any) - .extract::>>() - .unwrap_err() + c.bench_function("extract_failure", |b| { + b.iter(|| { + black_box(&any) + .extract::>>() + .unwrap_err() + }); }); }); } -#[bench] -fn cast_success(bencher: &mut Bencher) { +fn cast_success(c: &mut Criterion) { Python::attach(|py| { let any = PyArray2::::zeros(py, (10, 10), false).into_any(); - bencher.iter(|| black_box(&any).cast::>().unwrap()); + c.bench_function("cast_success", |b| { + b.iter(|| black_box(&any).cast::>().unwrap()); + }); }); } -#[bench] -fn cast_failure(bencher: &mut Bencher) { +fn cast_failure(c: &mut Criterion) { Python::attach(|py| { let any = PyArray2::::zeros(py, (10, 10), false).into_any(); - bencher.iter(|| black_box(&any).cast::>().unwrap_err()); + c.bench_function("cast_failure", |b| { + b.iter(|| black_box(&any).cast::>().unwrap_err()); + }) }); } @@ -62,139 +62,111 @@ impl Iterator for Iter { } } -fn from_iter(bencher: &mut Bencher, size: usize) { - Python::attach(|py| { - bencher.iter(|| { - let iter = black_box(Iter(0..size)); +fn from_iter(c: &mut Criterion) { + const SIZES: &[usize] = &[2_usize.pow(5), 2_usize.pow(10), 2_usize.pow(15)]; - PyArray1::from_iter(py, iter) + let mut group = c.benchmark_group("from_iter"); + for &size in SIZES { + Python::attach(|py| { + group.throughput(Throughput::Elements(size as u64)); + group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, _size| { + b.iter(|| { + let iter = black_box(Iter(0..size)); + black_box(PyArray1::from_iter(py, iter)); + }); + }); }); - }); -} - -#[bench] -fn from_iter_small(bencher: &mut Bencher) { - from_iter(bencher, 2_usize.pow(5)); -} - -#[bench] -fn from_iter_medium(bencher: &mut Bencher) { - from_iter(bencher, 2_usize.pow(10)); -} - -#[bench] -fn from_iter_large(bencher: &mut Bencher) { - from_iter(bencher, 2_usize.pow(15)); + } } -fn from_slice(bencher: &mut Bencher, size: usize) { - let vec = (0..size).collect::>(); +fn from_slice(c: &mut Criterion) { + const SIZES: &[usize] = &[2_usize.pow(5), 2_usize.pow(10), 2_usize.pow(15)]; - Python::attach(|py| { - bencher.iter(|| { - let slice = black_box(&vec); + let mut group = c.benchmark_group("from_slice"); + for &size in SIZES { + let vec = (0..size).collect::>(); - PyArray1::from_slice(py, slice) + Python::attach(|py| { + group.throughput(Throughput::Elements(size as u64)); + group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, _size| { + b.iter(|| { + let slice = black_box(&vec[..]); + black_box(PyArray1::from_slice(py, slice)); + }); + }); }); - }); -} - -#[bench] -fn from_slice_small(bencher: &mut Bencher) { - from_slice(bencher, 2_usize.pow(5)); -} - -#[bench] -fn from_slice_medium(bencher: &mut Bencher) { - from_slice(bencher, 2_usize.pow(10)); -} - -#[bench] -fn from_slice_large(bencher: &mut Bencher) { - from_slice(bencher, 2_usize.pow(15)); + } } -fn from_object_slice(bencher: &mut Bencher, size: usize) { - let vec = Python::attach(|py| { - (0..size) - .map(|val| val.into_py_any(py).unwrap()) - .collect::>() - }); +fn from_object_slice(c: &mut Criterion) { + const SIZES: &[usize] = &[2_usize.pow(5), 2_usize.pow(10), 2_usize.pow(15)]; - Python::attach(|py| { - bencher.iter(|| { - let slice = black_box(&vec); + let mut group = c.benchmark_group("from_object_slice"); + for &size in SIZES { + Python::attach(|py| { + let vec = (0..size) + .map(|val| val.into_py_any(py).unwrap()) + .collect::>(); - PyArray1::from_slice(py, slice) + group.throughput(Throughput::Elements(size as u64)); + group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, _size| { + b.iter(|| { + let slice = black_box(&vec[..]); + black_box(PyArray1::from_slice(py, slice)); + }); + }); }); - }); -} - -#[bench] -fn from_object_slice_small(bencher: &mut Bencher) { - from_object_slice(bencher, 2_usize.pow(5)); -} - -#[bench] -fn from_object_slice_medium(bencher: &mut Bencher) { - from_object_slice(bencher, 2_usize.pow(10)); -} - -#[bench] -fn from_object_slice_large(bencher: &mut Bencher) { - from_object_slice(bencher, 2_usize.pow(15)); + } } -fn from_vec2(bencher: &mut Bencher, size: usize) { - let vec2 = vec![vec![0; size]; size]; +fn from_vec2(c: &mut Criterion) { + const SIZES: &[usize] = &[2_usize.pow(3), 2_usize.pow(5), 2_usize.pow(8)]; - Python::attach(|py| { - bencher.iter(|| { - let vec2 = black_box(&vec2); + let mut group = c.benchmark_group("from_vec2"); + for &size in SIZES { + let vec2 = vec![vec![0; size]; size]; - PyArray2::from_vec2(py, vec2).unwrap() + Python::attach(|py| { + group.throughput(Throughput::Elements(size.pow(2) as u64)); + group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, _size| { + b.iter(|| { + let vec2 = black_box(&vec2); + black_box(PyArray2::from_vec2(py, vec2).unwrap()); + }); + }); }); - }); -} - -#[bench] -fn from_vec2_small(bencher: &mut Bencher) { - from_vec2(bencher, 2_usize.pow(3)); -} - -#[bench] -fn from_vec2_medium(bencher: &mut Bencher) { - from_vec2(bencher, 2_usize.pow(5)); -} - -#[bench] -fn from_vec2_large(bencher: &mut Bencher) { - from_vec2(bencher, 2_usize.pow(8)); + } } -fn from_vec3(bencher: &mut Bencher, size: usize) { - let vec3 = vec![vec![vec![0; size]; size]; size]; +fn from_vec3(c: &mut Criterion) { + const SIZES: &[usize] = &[2_usize.pow(2), 2_usize.pow(4), 2_usize.pow(5)]; - Python::attach(|py| { - bencher.iter(|| { - let vec3 = black_box(&vec3); + let mut group = c.benchmark_group("from_vec3"); + for &size in SIZES { + let vec3 = vec![vec![vec![0; size]; size]; size]; - PyArray3::from_vec3(py, vec3).unwrap() + Python::attach(|py| { + group.throughput(Throughput::Elements(size.pow(3) as u64)); + group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, _size| { + b.iter(|| { + let vec3 = black_box(&vec3); + black_box(PyArray3::from_vec3(py, vec3).unwrap()); + }); + }); }); - }); -} - -#[bench] -fn from_vec3_small(bencher: &mut Bencher) { - from_vec3(bencher, 2_usize.pow(2)); -} - -#[bench] -fn from_vec3_medium(bencher: &mut Bencher) { - from_vec3(bencher, 2_usize.pow(4)); + } } -#[bench] -fn from_vec3_large(bencher: &mut Bencher) { - from_vec3(bencher, 2_usize.pow(5)); -} +criterion_group!( + benches, + extract_success, + extract_failure, + cast_success, + cast_failure, + from_iter, + from_slice, + from_object_slice, + from_vec2, + from_vec3 +); +criterion_main!(benches); diff --git a/benches/array_like.rs b/benches/array_like.rs new file mode 100644 index 000000000..80f108259 --- /dev/null +++ b/benches/array_like.rs @@ -0,0 +1,104 @@ +use std::hint::black_box; + +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use numpy::{PyArrayLike1, PyArrayLike2, PyArrayLike3}; +use pyo3::{ + ffi::c_str, + types::{PyAnyMethods, PyDict}, + Python, +}; + +fn extract_array_like_1(c: &mut Criterion) { + const SIZES: &[usize] = &[2_usize.pow(5), 2_usize.pow(10), 2_usize.pow(15)]; + + let mut group = c.benchmark_group("extract_array_like_1"); + for &size in SIZES { + Python::attach(|py| { + let locals = PyDict::new(py); + locals.set_item("size", size).unwrap(); + + let list = py + .eval( + c_str!("[float(i) for i in range(size)]"), + Some(&locals), + None, + ) + .unwrap(); + + group.throughput(Throughput::Elements(size as u64)); + group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, _size| { + b.iter(|| { + let list = black_box(&list); + + let _array: PyArrayLike1<'_, f64> = black_box(list.extract().unwrap()); + }); + }); + }); + } +} + +fn extract_array_like_2(c: &mut Criterion) { + const SIZES: &[usize] = &[2_usize.pow(3), 2_usize.pow(5), 2_usize.pow(8)]; + + let mut group = c.benchmark_group("extract_array_like_2"); + for &size in SIZES { + Python::attach(|py| { + let locals = PyDict::new(py); + locals.set_item("size", size).unwrap(); + + let list = py + .eval( + c_str!("[[float(i + j) for i in range(size)] for j in range(size)]"), + Some(&locals), + None, + ) + .unwrap(); + + group.throughput(Throughput::Elements(size.pow(2) as u64)); + group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, _size| { + b.iter(|| { + let list = black_box(&list); + + let _array: PyArrayLike2<'_, f64> = black_box(list.extract().unwrap()); + }); + }); + }); + } +} + +fn extract_array_like_3(c: &mut Criterion) { + const SIZES: &[usize] = &[2_usize.pow(2), 2_usize.pow(4), 2_usize.pow(5)]; + + let mut group = c.benchmark_group("extract_array_like_3"); + for &size in SIZES { + Python::attach(|py| { + let locals = PyDict::new(py); + locals.set_item("size", size).unwrap(); + + let list = py + .eval( + c_str!("[[[float(i + j + k) for i in range(size)] for j in range(size)] for k in range(size)]"), + Some(&locals), + None, + ) + .unwrap(); + + group.throughput(Throughput::Elements(size.pow(3) as u64)); + group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, _size| { + b.iter(|| { + let list = black_box(&list); + + let _array: PyArrayLike3<'_, f64> = black_box(list.extract().unwrap()); + }); + }); + }); + } +} + +criterion_group!( + benches, + extract_array_like_1, + extract_array_like_2, + extract_array_like_3 +); +criterion_main!(benches); diff --git a/benches/borrow.rs b/benches/borrow.rs index 346a7f7be..fa425db64 100644 --- a/benches/borrow.rs +++ b/benches/borrow.rs @@ -1,48 +1,57 @@ -#![feature(test)] - -extern crate test; -use test::{black_box, Bencher}; +use std::hint::black_box; +use criterion::{criterion_group, criterion_main, Criterion}; use numpy::{PyArray, PyArrayMethods}; use pyo3::Python; -#[bench] -fn initial_shared_borrow(bencher: &mut Bencher) { +fn initial_shared_borrow(c: &mut Criterion) { Python::attach(|py| { let array = PyArray::::zeros(py, (6, 5, 4, 3, 2, 1), false); - bencher.iter(|| { - let array = black_box(&array); + c.bench_function("initial_shared_borrow", |bencher| { + bencher.iter(|| { + let array = black_box(&array); - let _shared = array.readonly(); + let _shared = black_box(array.readonly()); + }); }); }); } -#[bench] -fn additional_shared_borrow(bencher: &mut Bencher) { +fn additional_shared_borrow(c: &mut Criterion) { Python::attach(|py| { let array = PyArray::::zeros(py, (6, 5, 4, 3, 2, 1), false); let _shared = (0..128).map(|_| array.readonly()).collect::>(); - bencher.iter(|| { - let array = black_box(&array); + c.bench_function("additional_shared_borrow", |bencher| { + bencher.iter(|| { + let array = black_box(&array); - let _shared = array.readonly(); + let _shared = black_box(array.readonly()); + }); }); }); } -#[bench] -fn exclusive_borrow(bencher: &mut Bencher) { +fn exclusive_borrow(c: &mut Criterion) { Python::attach(|py| { let array = PyArray::::zeros(py, (6, 5, 4, 3, 2, 1), false); - bencher.iter(|| { - let array = black_box(&array); + c.bench_function("exclusive_borrow", |bencher| { + bencher.iter(|| { + let array = black_box(&array); - let _exclusive = array.readwrite(); + let _exclusive = black_box(array.readwrite()); + }); }); }); } + +criterion_group!( + benches, + initial_shared_borrow, + additional_shared_borrow, + exclusive_borrow +); +criterion_main!(benches); diff --git a/src/array_like.rs b/src/array_like.rs index dfea56165..40a60802a 100644 --- a/src/array_like.rs +++ b/src/array_like.rs @@ -2,15 +2,11 @@ use std::marker::PhantomData; use std::ops::Deref; use ndarray::{Array1, Dimension, Ix0, Ix1, Ix2, Ix3, Ix4, Ix5, Ix6, IxDyn}; -use pyo3::{ - intern, - sync::PyOnceLock, - types::{PyAnyMethods, PyDict}, - Borrowed, FromPyObject, Py, PyAny, PyErr, PyResult, -}; +use pyo3::{types::PyAnyMethods, Borrowed, FromPyObject, PyAny, PyErr, PyResult}; -use crate::array::PyArrayMethods; -use crate::{get_array_module, Element, IntoPyArray, PyArray, PyReadonlyArray, PyUntypedArray}; +use crate::npyffi::NPY_ARRAY_FORCECAST; +use crate::{array::PyArrayMethods, PY_ARRAY_API}; +use crate::{Element, IntoPyArray, PyArray, PyReadonlyArray, PyUntypedArray}; pub trait Coerce: Sealed { const ALLOW_TYPE_CHANGE: bool; @@ -166,24 +162,31 @@ where } } - static AS_ARRAY: PyOnceLock> = PyOnceLock::new(); - - let as_array = AS_ARRAY - .get_or_try_init(py, || { - get_array_module(py)?.getattr("asarray").map(Into::into) - })? - .bind(py); - - let kwargs = if C::ALLOW_TYPE_CHANGE { - let kwargs = PyDict::new(py); - kwargs.set_item(intern!(py, "dtype"), T::get_dtype(py))?; - Some(kwargs) + let (dtype, flags) = if C::ALLOW_TYPE_CHANGE { + (Some(T::get_dtype(py)), NPY_ARRAY_FORCECAST) } else { - None + (None, 0) + }; + + let newtype = dtype + .map(|dt| dt.into_ptr().cast()) + .unwrap_or_else(std::ptr::null_mut); + + let array = unsafe { + let ptr = PY_ARRAY_API.PyArray_FromAny( + py, + ob.as_ptr(), + newtype, + 0, + 0, + flags, + std::ptr::null_mut(), + ); + + pyo3::Bound::from_owned_ptr_or_err(py, ptr)? }; - let array = as_array.call((ob,), kwargs.as_ref())?.extract()?; - Ok(Self(array, PhantomData)) + Ok(Self(array.extract()?, PhantomData)) } } diff --git a/x.py b/x.py index 9970e4565..1e1f9fe6b 100755 --- a/x.py +++ b/x.py @@ -88,9 +88,6 @@ def test(args): def bench(args): - if not nightly(): - sys.exit("Benchmarks require a nightly build of the Rust compiler.") - if args.name is None: run("cargo", "bench", "--all-features", "--benches") else: @@ -151,9 +148,7 @@ def prune(args): test_parser.set_defaults(func=test) test_parser.add_argument("name", nargs="?", help="Test target name") - bench_parser = subparsers.add_parser( - "bench", aliases=["b"], help="Benchmarks (requires nightly)" - ) + bench_parser = subparsers.add_parser("bench", aliases=["b"], help="Benchmarks") bench_parser.set_defaults(func=bench) bench_parser.add_argument("name", nargs="?", help="Benchmark target name")