// Benchmarks for Array/Array coefficient-wise ops whose // NEON (Packet2l/Packet2ul) implementations may be polyfilled: negate, // abs, multiply, min, max, and comparison-driven select. // SPDX-FileCopyrightText: The Eigen Authors // SPDX-License-Identifier: MPL-2.0 #include #include #include #include #include namespace Eigen { namespace { // Scalar references for the signed integer types benchmarked below, computed through the // unsigned type so they don't invoke the UB `-x`/`x*y` hit at the minimum representable value // or on signed overflow -- matching what the vectorized ops actually (wrap around to) compute. template ::value, int> = 0> Scalar RefNegate(Scalar x) { using UScalar = typename std::make_unsigned::type; return static_cast(UScalar(0) - static_cast(x)); } template ::value, int> = 0> Scalar RefNegate(Scalar x) { return static_cast(Scalar(0) - x); } template ::value, int> = 0> Scalar RefAbs(Scalar x) { return x < Scalar(0) ? RefNegate(x) : x; } template ::value, int> = 0> Scalar RefAbs(Scalar x) { return x; } template Scalar RefMul(Scalar a, Scalar b) { using UScalar = typename std::make_unsigned::type; return static_cast(static_cast(a) * static_cast(b)); } template void BM_Negate(benchmark::State& state) { const Index n = state.range(0); using A = ArrayX; A a = A::Random(n); A b(n); for (auto _ : state) { b = -a; benchmark::DoNotOptimize(b.data()); } for (Index i = 0; i < n; ++i) { if (b[i] != RefNegate(a[i])) { state.SkipWithError("Negate: materialized result does not match scalar reference"); break; } } state.SetBytesProcessed(state.iterations() * n * sizeof(Scalar) * 2); } template void BM_Abs(benchmark::State& state) { const Index n = state.range(0); using A = ArrayX; A a = A::Random(n); A b(n); for (auto _ : state) { b = a.abs(); benchmark::DoNotOptimize(b.data()); } for (Index i = 0; i < n; ++i) { if (b[i] != RefAbs(a[i])) { state.SkipWithError("Abs: materialized result does not match scalar reference"); break; } } state.SetBytesProcessed(state.iterations() * n * sizeof(Scalar) * 2); } template void BM_Mul(benchmark::State& state) { const Index n = state.range(0); using A = ArrayX; A a = A::Random(n); A b = A::Random(n); A c(n); for (auto _ : state) { c = a * b; benchmark::DoNotOptimize(c.data()); } for (Index i = 0; i < n; ++i) { if (c[i] != RefMul(a[i], b[i])) { state.SkipWithError("Mul: materialized result does not match scalar reference"); break; } } state.SetBytesProcessed(state.iterations() * n * sizeof(Scalar) * 3); } template void BM_Min(benchmark::State& state) { const Index n = state.range(0); using A = ArrayX; A a = A::Random(n); A b = A::Random(n); A c(n); for (auto _ : state) { c = a.min(b); benchmark::DoNotOptimize(c.data()); } for (Index i = 0; i < n; ++i) { if (c[i] != (std::min)(a[i], b[i])) { state.SkipWithError("Min: materialized result does not match scalar reference"); break; } } state.SetBytesProcessed(state.iterations() * n * sizeof(Scalar) * 3); } template void BM_Max(benchmark::State& state) { const Index n = state.range(0); using A = ArrayX; A a = A::Random(n); A b = A::Random(n); A c(n); for (auto _ : state) { c = a.max(b); benchmark::DoNotOptimize(c.data()); } for (Index i = 0; i < n; ++i) { if (c[i] != (std::max)(a[i], b[i])) { state.SkipWithError("Max: materialized result does not match scalar reference"); break; } } state.SetBytesProcessed(state.iterations() * n * sizeof(Scalar) * 3); } // `(c, d)` must be independent of the predicate inputs `(a, b)`: (a OP b).select(a, b) is // algebraically `min`/`max` for `Lt`/`Le` and identically `b` for `Eq`, letting an optimizer erase // the comparison (GCC 13/x86-64 turned `BM_SelectEq` into a `memcpy`). #define BENCH_CWISE_SELECT(NAME, OP) \ template \ void BM_Select##NAME(benchmark::State& state) { \ const Index n = state.range(0); \ using A = ArrayX; \ A a = A::Random(n); \ A b = A::Random(n); \ A c = A::Random(n); \ A d = A::Random(n); \ A out(n); \ for (auto _ : state) { \ out = (a OP b).select(c, d); \ benchmark::DoNotOptimize(out.data()); \ } \ for (Index i = 0; i < n; ++i) { \ const Scalar expected = (a[i] OP b[i]) ? c[i] : d[i]; \ if (out[i] != expected) { \ state.SkipWithError("Select" #NAME ": materialized result does not match scalar reference"); \ break; \ } \ } \ state.SetBytesProcessed(state.iterations() * n * sizeof(Scalar) * 5); \ } BENCH_CWISE_SELECT(Lt, <) BENCH_CWISE_SELECT(Le, <=) BENCH_CWISE_SELECT(Eq, ==) // Kept small enough that the working set (up to 5 arrays, for BENCH_CWISE_SELECT) // stays within a typical L1D cache, so timings reflect compute cost rather than // memory bandwidth/latency. #define INT64_SIZES ->Arg(32)->Arg(64)->Arg(128)->Arg(256)->Arg(512) BENCHMARK(BM_Negate) INT64_SIZES->Name("Negate_int64"); BENCHMARK(BM_Abs) INT64_SIZES->Name("Abs_int64"); BENCHMARK(BM_Abs) INT64_SIZES->Name("Abs_uint64"); BENCHMARK(BM_Mul) INT64_SIZES->Name("Mul_int64"); BENCHMARK(BM_Mul) INT64_SIZES->Name("Mul_uint64"); BENCHMARK(BM_Min) INT64_SIZES->Name("Min_int64"); BENCHMARK(BM_Min) INT64_SIZES->Name("Min_uint64"); BENCHMARK(BM_Max) INT64_SIZES->Name("Max_int64"); BENCHMARK(BM_Max) INT64_SIZES->Name("Max_uint64"); BENCHMARK(BM_SelectLt) INT64_SIZES->Name("SelectLt_int64"); BENCHMARK(BM_SelectLt) INT64_SIZES->Name("SelectLt_uint64"); BENCHMARK(BM_SelectLe) INT64_SIZES->Name("SelectLe_int64"); BENCHMARK(BM_SelectLe) INT64_SIZES->Name("SelectLe_uint64"); BENCHMARK(BM_SelectEq) INT64_SIZES->Name("SelectEq_int64"); BENCHMARK(BM_SelectEq) INT64_SIZES->Name("SelectEq_uint64"); } // namespace } // namespace Eigen