blob: 4bdcf482cd619e45acc2981554f4d61574131d53 [file] [log] [blame]
use alloc::vec::Vec;
use core::cell::RefCell;
use compiler_builtins::float::Float;
/// Fuzz with these many items to ensure equal functions
pub const CHECK_ITER_ITEMS: u32 = 10_000;
/// Benchmark with this many items to get a variety
pub const BENCH_ITER_ITEMS: u32 = 500;
/// Still run benchmarks/tests but don't check correctness between compiler-builtins and
/// builtin system functions functions
pub fn skip_sys_checks(test_name: &str) -> bool {
const ALWAYS_SKIPPED: &[&str] = &[
// FIXME(f16_f128): system symbols have incorrect results
// <https://github.com/rust-lang/compiler-builtins/issues/617>
"extend_f16_f32",
"trunc_f32_f16",
"trunc_f64_f16",
];
// FIXME(f16_f128): system symbols have incorrect results
// <https://github.com/rust-lang/compiler-builtins/issues/617#issuecomment-2125914639>
const X86_NO_SSE_SKIPPED: &[&str] = &[
"add_f128", "sub_f128", "mul_f128", "div_f128", "powi_f32", "powi_f64",
];
// FIXME(llvm): system symbols have incorrect results on Windows
// <https://github.com/rust-lang/compiler-builtins/issues/617#issuecomment-2121359807>
const WINDOWS_SKIPPED: &[&str] = &[
"conv_f32_u128",
"conv_f32_i128",
"conv_f64_u128",
"conv_f64_i128",
];
if cfg!(target_arch = "arm") {
// The Arm symbols need a different ABI that our macro doesn't handle, just skip it
return true;
}
if ALWAYS_SKIPPED.contains(&test_name) {
return true;
}
if cfg!(x86_no_sse) && X86_NO_SSE_SKIPPED.contains(&test_name) {
return true;
}
if cfg!(target_family = "windows") && WINDOWS_SKIPPED.contains(&test_name) {
return true;
}
false
}
/// Still run benchmarks/tests but don't check correctness between compiler-builtins and
/// assembly functions
pub fn skip_asm_checks(_test_name: &str) -> bool {
// Nothing to skip at this time
false
}
/// Create a comparison of the system symbol, compiler_builtins, and optionally handwritten
/// assembly.
///
/// # Safety
///
/// The signature must be correct and any assembly must be sound.
#[macro_export]
macro_rules! float_bench {
(
// Name of this benchmark
name: $name:ident,
// The function signature to be tested
sig: ($($arg:ident: $arg_ty:ty),*) -> $ret_ty:ty,
// Path to the crate in compiler_builtins
crate_fn: $crate_fn:path,
// Optional alias on ppc
$( crate_fn_ppc: $crate_fn_ppc:path, )?
// Name of the system symbol
sys_fn: $sys_fn:ident,
// Optional alias on ppc
$( sys_fn_ppc: $sys_fn_ppc:path, )?
// Meta saying whether the system symbol is available
sys_available: $sys_available:meta,
// An optional function to validate the results of two functions are equal, if not
// just `$ret_ty::check_eq`
$( output_eq: $output_eq:expr, )?
// Assembly implementations, if any.
asm: [
$(
#[cfg($asm_meta:meta)] {
$($asm_tt:tt)*
}
);*
$(;)?
]
$(,)?
) => {paste::paste! {
// SAFETY: macro invocation must use the correct signature
#[cfg($sys_available)]
unsafe extern "C" {
/// Binding for the system function
#[cfg(not(any(target_arch = "powerpc", target_arch = "powerpc64")))]
fn $sys_fn($($arg: $arg_ty),*) -> $ret_ty;
#[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
float_bench! { @coalesce_fn $($sys_fn_ppc)? =>
fn $sys_fn($($arg: $arg_ty),*) -> $ret_ty;
}
}
fn $name(c: &mut Criterion) {
use core::hint::black_box;
use compiler_builtins::float::Float;
use $crate::bench::TestIO;
#[inline(never)] // equalize with external calls
fn crate_fn($($arg: $arg_ty),*) -> $ret_ty {
#[cfg(not(any(target_arch = "powerpc", target_arch = "powerpc64")))]
let target_crate_fn = $crate_fn;
// On PPC, use an alias if specified
#[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
let target_crate_fn = float_bench!(@coalesce $($crate_fn_ppc)?, $crate_fn);
target_crate_fn( $($arg),* )
}
#[inline(always)] // already a branch
#[cfg($sys_available)]
fn sys_fn($($arg: $arg_ty),*) -> $ret_ty {
#[cfg(not(any(target_arch = "powerpc", target_arch = "powerpc64")))]
let target_sys_fn = $sys_fn;
// On PPC, use an alias if specified
#[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
let target_sys_fn = float_bench!(@coalesce $($sys_fn_ppc)?, $sys_fn);
unsafe { target_sys_fn( $($arg),* ) }
}
#[inline(never)] // equalize with external calls
#[cfg(any( $($asm_meta),* ))]
fn asm_fn($(mut $arg: $arg_ty),*) -> $ret_ty {
use core::arch::asm;
$(
#[cfg($asm_meta)]
unsafe { $($asm_tt)* }
)*
}
let testvec = <($($arg_ty),*)>::make_testvec($crate::bench::CHECK_ITER_ITEMS);
let benchvec = <($($arg_ty),*)>::make_testvec($crate::bench::BENCH_ITER_ITEMS);
let test_name = stringify!($name);
let check_eq = float_bench!(@coalesce $($output_eq)?, $ret_ty::check_eq);
// Verify math lines up. We run the crate functions even if we don't validate the
// output here to make sure there are no panics or crashes.
#[cfg($sys_available)]
for ($($arg),*) in testvec.iter().copied() {
let crate_res = crate_fn($($arg),*);
let sys_res = sys_fn($($arg),*);
if $crate::bench::skip_sys_checks(test_name) {
continue;
}
assert!(
check_eq(crate_res, sys_res),
"{test_name}{:?}: crate: {crate_res:?}, sys: {sys_res:?}",
($($arg),* ,)
);
}
#[cfg(any( $($asm_meta),* ))]
{
for ($($arg),*) in testvec.iter().copied() {
let crate_res = crate_fn($($arg),*);
let asm_res = asm_fn($($arg),*);
if $crate::bench::skip_asm_checks(test_name) {
continue;
}
assert!(
check_eq(crate_res, asm_res),
"{test_name}{:?}: crate: {crate_res:?}, asm: {asm_res:?}",
($($arg),* ,)
);
}
}
let mut group = c.benchmark_group(test_name);
group.bench_function("compiler-builtins", |b| b.iter(|| {
for ($($arg),*) in benchvec.iter().copied() {
black_box(crate_fn( $(black_box($arg)),* ));
}
}));
#[cfg($sys_available)]
group.bench_function("system", |b| b.iter(|| {
for ($($arg),*) in benchvec.iter().copied() {
black_box(sys_fn( $(black_box($arg)),* ));
}
}));
#[cfg(any( $($asm_meta),* ))]
group.bench_function(&format!(
"assembly ({} {})", std::env::consts::ARCH, std::env::consts::FAMILY
), |b| b.iter(|| {
for ($($arg),*) in benchvec.iter().copied() {
black_box(asm_fn( $(black_box($arg)),* ));
}
}));
group.finish();
}
}};
// Allow overriding a default
(@coalesce $specified:expr, $default:expr) => { $specified };
(@coalesce, $default:expr) => { $default };
// Allow overriding a function name
(@coalesce_fn $specified:ident => fn $default_name:ident $($tt:tt)+) => {
fn $specified $($tt)+
};
(@coalesce_fn => fn $default_name:ident $($tt:tt)+) => {
fn $default_name $($tt)+
};
}
/// A type used as either an input or output to/from a benchmark function.
pub trait TestIO: Sized {
fn make_testvec(len: u32) -> Vec<Self>;
fn check_eq(a: Self, b: Self) -> bool;
}
macro_rules! impl_testio {
(float $($f_ty:ty),+) => {$(
impl TestIO for $f_ty {
fn make_testvec(len: u32) -> Vec<Self> {
// refcell because fuzz_* takes a `Fn`
let ret = RefCell::new(Vec::new());
crate::fuzz_float(len, |a| ret.borrow_mut().push(a));
ret.into_inner()
}
fn check_eq(a: Self, b: Self) -> bool {
Float::eq_repr(a, b)
}
}
impl TestIO for ($f_ty, $f_ty) {
fn make_testvec(len: u32) -> Vec<Self> {
// refcell because fuzz_* takes a `Fn`
let ret = RefCell::new(Vec::new());
crate::fuzz_float_2(len, |a, b| ret.borrow_mut().push((a, b)));
ret.into_inner()
}
fn check_eq(_a: Self, _b: Self) -> bool {
unimplemented!()
}
}
)*};
(int $($i_ty:ty),+) => {$(
impl TestIO for $i_ty {
fn make_testvec(len: u32) -> Vec<Self> {
// refcell because fuzz_* takes a `Fn`
let ret = RefCell::new(Vec::new());
crate::fuzz(len, |a| ret.borrow_mut().push(a));
ret.into_inner()
}
fn check_eq(a: Self, b: Self) -> bool {
a == b
}
}
impl TestIO for ($i_ty, $i_ty) {
fn make_testvec(len: u32) -> Vec<Self> {
// refcell because fuzz_* takes a `Fn`
let ret = RefCell::new(Vec::new());
crate::fuzz_2(len, |a, b| ret.borrow_mut().push((a, b)));
ret.into_inner()
}
fn check_eq(_a: Self, _b: Self) -> bool {
unimplemented!()
}
}
)*};
((float, int) ($f_ty:ty, $i_ty:ty)) => {
impl TestIO for ($f_ty, $i_ty) {
fn make_testvec(len: u32) -> Vec<Self> {
// refcell because fuzz_* takes a `Fn`
let ivec = RefCell::new(Vec::new());
let fvec = RefCell::new(Vec::new());
crate::fuzz(len.isqrt(), |a| ivec.borrow_mut().push(a));
crate::fuzz_float(len.isqrt(), |a| fvec.borrow_mut().push(a));
let mut ret = Vec::new();
let ivec = ivec.into_inner();
let fvec = fvec.into_inner();
for f in fvec {
for i in &ivec {
ret.push((f, *i));
}
}
ret
}
fn check_eq(_a: Self, _b: Self) -> bool {
unimplemented!()
}
}
}
}
#[cfg(f16_enabled)]
impl_testio!(float f16);
impl_testio!(float f32, f64);
#[cfg(f128_enabled)]
impl_testio!(float f128);
impl_testio!(int i8, i16, i32, i64, i128, isize);
impl_testio!(int u8, u16, u32, u64, u128, usize);
impl_testio!((float, int)(f32, i32));
impl_testio!((float, int)(f64, i32));
#[cfg(f128_enabled)]
impl_testio!((float, int)(f128, i32));