blob: c33325fdd02598d210cfa5c8280bfad37be812d6 [file]
// Benchmarks for Eigen Tensor morphing operations: reshape, slice, chip, pad, strided slice.
// SPDX-FileCopyrightText: The Eigen Authors
// SPDX-License-Identifier: MPL-2.0
#define EIGEN_USE_THREADS
#include <benchmark/benchmark.h>
#include <contrib/Eigen/Tensor>
#include <Eigen/ThreadPool>
using namespace Eigen;
typedef float Scalar;
// --- Reshape (zero-cost if no evaluation needed; force eval via assignment) ---
static void BM_Reshape(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> A(M, N);
A.setRandom();
Eigen::array<Index, 1> new_shape = {M * N};
for (auto _ : state) {
Tensor<Scalar, 1> B = A.reshape(new_shape);
benchmark::DoNotOptimize(B.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * M * N * sizeof(Scalar));
}
// --- Slice ---
static void BM_Slice(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> A(M, N);
A.setRandom();
int sliceM = M / 2;
int sliceN = N / 2;
Eigen::array<Index, 2> offsets = {0, 0};
Eigen::array<Index, 2> extents = {sliceM, sliceN};
for (auto _ : state) {
Tensor<Scalar, 2> B = A.slice(offsets, extents);
benchmark::DoNotOptimize(B.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * sliceM * sliceN * sizeof(Scalar));
}
// --- Chip (extract a sub-tensor along one dimension) ---
static void BM_Chip(benchmark::State& state) {
const int D0 = state.range(0);
const int D1 = state.range(1);
const int D2 = state.range(2);
Tensor<Scalar, 3> A(D0, D1, D2);
A.setRandom();
for (auto _ : state) {
Tensor<Scalar, 2> B = A.chip(0, 0);
benchmark::DoNotOptimize(B.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * D1 * D2 * sizeof(Scalar));
}
// --- Pad ---
static void BM_Pad(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
const int padSize = state.range(2);
Tensor<Scalar, 2> A(M, N);
A.setRandom();
Eigen::array<std::pair<int, int>, 2> paddings;
paddings[0] = {padSize, padSize};
paddings[1] = {padSize, padSize};
for (auto _ : state) {
Tensor<Scalar, 2> B = A.pad(paddings);
benchmark::DoNotOptimize(B.data());
benchmark::ClobberMemory();
}
int outM = M + 2 * padSize;
int outN = N + 2 * padSize;
state.SetBytesProcessed(state.iterations() * outM * outN * sizeof(Scalar));
}
// --- Strided slice ---
// Covers the packet fast paths of the strided-slice evaluator: contiguous
// inner loads (inner stride 1), reversed inner loads (inner stride -1), and
// the scalar gather fallback (non-unit inner stride).
static void BM_StridedSliceRead(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
const Index s0 = state.range(2);
const Index s1 = state.range(3);
Tensor<Scalar, 2> A(M, N);
A.setRandom();
const Eigen::array<Index, 2> start = {s0 > 0 ? 1 : M - 2, s1 > 0 ? 1 : N - 2};
const Eigen::array<Index, 2> stop = {s0 > 0 ? M - 1 : 0, s1 > 0 ? N - 1 : 0};
const Eigen::array<Index, 2> strides = {s0, s1};
Tensor<Scalar, 2> B;
for (auto _ : state) {
B = A.stridedSlice(start, stop, strides);
benchmark::DoNotOptimize(B.data());
benchmark::ClobberMemory();
}
// Validate outside the timed loop.
for (Index i = 0; i < B.dimension(0); ++i) {
for (Index j = 0; j < B.dimension(1); ++j) {
if (B(i, j) != A(start[0] + i * s0, start[1] + j * s1)) {
state.SkipWithError("validation failed");
return;
}
}
}
state.SetBytesProcessed(state.iterations() * B.size() * sizeof(Scalar));
}
// Strided slice feeding a packet-math kernel the compiler cannot rescue by
// auto-vectorizing the scalar loop.
static void BM_StridedSliceExp(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
const Index s0 = state.range(2);
const Index s1 = state.range(3);
Tensor<Scalar, 2> A(M, N);
A.setRandom();
const Eigen::array<Index, 2> start = {s0 > 0 ? 1 : M - 2, s1 > 0 ? 1 : N - 2};
const Eigen::array<Index, 2> stop = {s0 > 0 ? M - 1 : 0, s1 > 0 ? N - 1 : 0};
const Eigen::array<Index, 2> strides = {s0, s1};
Tensor<Scalar, 2> B;
for (auto _ : state) {
B = A.stridedSlice(start, stop, strides).exp();
benchmark::DoNotOptimize(B.data());
benchmark::ClobberMemory();
}
for (Index i = 0; i < B.dimension(0); ++i) {
for (Index j = 0; j < B.dimension(1); ++j) {
const Scalar expected = std::exp(A(start[0] + i * s0, start[1] + j * s1));
if (std::abs(B(i, j) - expected) > 1e-4f * std::abs(expected)) {
state.SkipWithError("validation failed");
return;
}
}
}
state.SetBytesProcessed(state.iterations() * B.size() * sizeof(Scalar));
}
static void BM_StridedSliceWrite(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
const Index s0 = state.range(2);
const Index s1 = state.range(3);
const Eigen::array<Index, 2> start = {s0 > 0 ? 1 : M - 2, s1 > 0 ? 1 : N - 2};
const Eigen::array<Index, 2> stop = {s0 > 0 ? M - 1 : 0, s1 > 0 ? N - 1 : 0};
const Eigen::array<Index, 2> strides = {s0, s1};
Tensor<Scalar, 2> B(M, N);
B.setZero();
// Size the source from the slice itself, so it tracks the stride sweep.
Tensor<Scalar, 2> A = B.stridedSlice(start, stop, strides);
A.setRandom();
for (auto _ : state) {
B.stridedSlice(start, stop, strides) = A;
benchmark::DoNotOptimize(B.data());
benchmark::ClobberMemory();
}
for (Index i = 0; i < A.dimension(0); ++i) {
for (Index j = 0; j < A.dimension(1); ++j) {
if (B(start[0] + i * s0, start[1] + j * s1) != A(i, j)) {
state.SkipWithError("validation failed");
return;
}
}
}
state.SetBytesProcessed(state.iterations() * A.size() * sizeof(Scalar));
}
// Expression assigned through a strided-slice destination. The rhs block has
// no raw buffer, so this exercises the expression side of writeBlock: direct
// assignment into the destination for a unit inner stride, and the
// materialize-into-temp path otherwise.
static void BM_StridedSliceWriteExp(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
const Index s0 = state.range(2);
const Index s1 = state.range(3);
const Eigen::array<Index, 2> start = {s0 > 0 ? 1 : M - 2, s1 > 0 ? 1 : N - 2};
const Eigen::array<Index, 2> stop = {s0 > 0 ? M - 1 : 0, s1 > 0 ? N - 1 : 0};
const Eigen::array<Index, 2> strides = {s0, s1};
Tensor<Scalar, 2> B(M, N);
B.setZero();
// Size the source from the slice itself, so it tracks the stride sweep.
Tensor<Scalar, 2> A = B.stridedSlice(start, stop, strides);
A.setRandom();
for (auto _ : state) {
B.stridedSlice(start, stop, strides) = A.exp();
benchmark::DoNotOptimize(B.data());
benchmark::ClobberMemory();
}
for (Index i = 0; i < A.dimension(0); ++i) {
for (Index j = 0; j < A.dimension(1); ++j) {
const Scalar expected = std::exp(A(i, j));
if (std::abs(B(start[0] + i * s0, start[1] + j * s1) - expected) > 1e-4f * std::abs(expected)) {
state.SkipWithError("validation failed");
return;
}
}
}
state.SetBytesProcessed(state.iterations() * A.size() * sizeof(Scalar));
}
// --- ThreadPool variants ---
static void BM_Slice_ThreadPool(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
const int threads = state.range(2);
Tensor<Scalar, 2> A(M, N);
A.setRandom();
int sliceM = M / 2;
int sliceN = N / 2;
Eigen::array<Index, 2> offsets = {0, 0};
Eigen::array<Index, 2> extents = {sliceM, sliceN};
ThreadPool tp(threads);
ThreadPoolDevice dev(&tp, threads);
Tensor<Scalar, 2> B(sliceM, sliceN);
for (auto _ : state) {
B.device(dev) = A.slice(offsets, extents);
benchmark::DoNotOptimize(B.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * sliceM * sliceN * sizeof(Scalar));
state.counters["threads"] = threads;
}
static void BM_Pad_ThreadPool(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
const int threads = state.range(2);
Tensor<Scalar, 2> A(M, N);
A.setRandom();
Eigen::array<std::pair<int, int>, 2> paddings;
paddings[0] = {4, 4};
paddings[1] = {4, 4};
int outM = M + 8;
int outN = N + 8;
ThreadPool tp(threads);
ThreadPoolDevice dev(&tp, threads);
Tensor<Scalar, 2> B(outM, outN);
for (auto _ : state) {
B.device(dev) = A.pad(paddings);
benchmark::DoNotOptimize(B.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * outM * outN * sizeof(Scalar));
state.counters["threads"] = threads;
}
// clang-format off
#define MORPH_SIZES \
->Args({256, 256})->Args({1024, 1024})
#define CHIP_SIZES \
->Args({32, 256, 256})->Args({64, 128, 128})->Args({8, 512, 512})
#define PAD_SIZES \
->Args({256, 256, 1})->Args({256, 256, 4})->Args({256, 256, 16}) \
->Args({1024, 1024, 1})->Args({1024, 1024, 4})->Args({1024, 1024, 16})
#define STRIDED_SLICE_SIZES \
->Args({256, 256, 1, 1})->Args({1024, 1024, 1, 1}) /* contiguous inner */ \
->Args({256, 256, -1, -1})->Args({1024, 1024, -1, -1}) /* reversed inner */ \
->Args({256, 256, 2, 2})->Args({1024, 1024, 2, 2}) /* gather fallback */
#define MORPH_THREADPOOL_SIZES \
->Args({256, 256, 1})->Args({256, 256, 2})->Args({256, 256, 4}) \
->Args({256, 256, 8})->Args({256, 256, 12})->Args({256, 256, 16}) \
->Args({1024, 1024, 1})->Args({1024, 1024, 2})->Args({1024, 1024, 4}) \
->Args({1024, 1024, 8})->Args({1024, 1024, 12})->Args({1024, 1024, 16})
// clang-format on
BENCHMARK(BM_Reshape) MORPH_SIZES;
BENCHMARK(BM_Slice) MORPH_SIZES;
BENCHMARK(BM_Chip) CHIP_SIZES;
BENCHMARK(BM_Pad) PAD_SIZES;
BENCHMARK(BM_StridedSliceRead) STRIDED_SLICE_SIZES;
BENCHMARK(BM_StridedSliceExp) STRIDED_SLICE_SIZES;
BENCHMARK(BM_StridedSliceWrite) STRIDED_SLICE_SIZES;
BENCHMARK(BM_StridedSliceWriteExp) STRIDED_SLICE_SIZES;
BENCHMARK(BM_Slice_ThreadPool) MORPH_THREADPOOL_SIZES->UseRealTime();
BENCHMARK(BM_Pad_ThreadPool) MORPH_THREADPOOL_SIZES->UseRealTime();