blob: 5c9d675c9cb4e736e1ae0e7521256299f01bf92c [file]
// Benchmarks for Eigen Tensor concatenation: standalone assigns and
// compound expressions that read from a concatenation. Tests both the
// outer-axis path (each operand slab is a single contiguous run, so
// TensorBlockIO collapses to one memcpy per slab) and the inner-axis
// path (the slab walk per row is short).
//
// 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;
// --- Standalone concat along axis 0 (innermost for ColMajor) ---
static void BM_Concat2D_Axis0(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> A(M, N);
Tensor<Scalar, 2> B(M, N);
Tensor<Scalar, 2> dst(2 * M, N);
A.setRandom();
B.setRandom();
for (auto _ : state) {
dst = A.concatenate(B, 0);
benchmark::DoNotOptimize(dst.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * 2 * M * N * sizeof(Scalar) * 2);
}
// --- Standalone concat along axis 1 (outermost for ColMajor 2D) ---
static void BM_Concat2D_Axis1(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> A(M, N);
Tensor<Scalar, 2> B(M, N);
Tensor<Scalar, 2> dst(M, 2 * N);
A.setRandom();
B.setRandom();
for (auto _ : state) {
dst = A.concatenate(B, 1);
benchmark::DoNotOptimize(dst.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * M * 2 * N * sizeof(Scalar) * 2);
}
// --- Standalone concat 3D along axis 2 (outermost for ColMajor) ---
static void BM_Concat3D_OuterAxis(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);
Tensor<Scalar, 3> B(D0, D1, D2);
Tensor<Scalar, 3> dst(D0, D1, 2 * D2);
A.setRandom();
B.setRandom();
for (auto _ : state) {
dst = A.concatenate(B, 2);
benchmark::DoNotOptimize(dst.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * D0 * D1 * 2 * D2 * sizeof(Scalar) * 2);
}
// --- Compound: (concat(A, B) + C) — concat feeds cwise binary ---
static void BM_Concat2D_Plus(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> A(M, N);
Tensor<Scalar, 2> B(M, N);
Tensor<Scalar, 2> C(2 * M, N);
Tensor<Scalar, 2> dst(2 * M, N);
A.setRandom();
B.setRandom();
C.setRandom();
for (auto _ : state) {
dst = A.concatenate(B, 0) + C;
benchmark::DoNotOptimize(dst.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * 2 * M * N * sizeof(Scalar) * 3);
}
// --- Compound: ((concat(A, B) + C) * D).sqrt() — concat in longer chain ---
static void BM_Concat2D_Chain(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> A(M, N);
Tensor<Scalar, 2> B(M, N);
Tensor<Scalar, 2> C(2 * M, N);
Tensor<Scalar, 2> D(2 * M, N);
Tensor<Scalar, 2> dst(2 * M, N);
A.setRandom();
B.setRandom();
C.setRandom();
C = C.abs();
D.setRandom();
D = D.abs();
for (auto _ : state) {
dst = ((A.concatenate(B, 0) + C) * D).sqrt();
benchmark::DoNotOptimize(dst.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * 2 * M * N * sizeof(Scalar) * 4);
}
// --- Compound: (concat(A, B) along axis 1 + C) — outer-axis concat feeds a
// cwise binary. Each operand slab is a contiguous run, so block evaluation
// returns it as a zero-copy view and the chain stays fully streaming
// (no scratch round-trip), unlike the inner-axis BM_Concat2D_Plus. ---
static void BM_Concat2D_Axis1_Plus(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> A(M, N);
Tensor<Scalar, 2> B(M, N);
Tensor<Scalar, 2> C(M, 2 * N);
Tensor<Scalar, 2> dst(M, 2 * N);
A.setRandom();
B.setRandom();
C.setRandom();
for (auto _ : state) {
dst = A.concatenate(B, 1) + C;
benchmark::DoNotOptimize(dst.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * M * 2 * N * sizeof(Scalar) * 3);
}
// --- Compound: ((concat(A, B) along axis 1 + C) * D).sqrt() — outer-axis
// analogue of BM_Concat2D_Chain. The zero-copy view path keeps this case off
// the materialize-then-reread trade-off that BM_Concat2D_Chain pays. ---
static void BM_Concat2D_Axis1_Chain(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> A(M, N);
Tensor<Scalar, 2> B(M, N);
Tensor<Scalar, 2> C(M, 2 * N);
Tensor<Scalar, 2> D(M, 2 * N);
Tensor<Scalar, 2> dst(M, 2 * N);
A.setRandom();
B.setRandom();
C.setRandom();
C = C.abs();
D.setRandom();
D = D.abs();
for (auto _ : state) {
dst = ((A.concatenate(B, 1) + C) * D).sqrt();
benchmark::DoNotOptimize(dst.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * M * 2 * N * sizeof(Scalar) * 4);
}
// --- Compound: concat of two raw tensors with an outer reduction-ish op ---
// .square() forces another cwise pass after the concat.
static void BM_Concat3D_OuterAxis_Square(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);
Tensor<Scalar, 3> B(D0, D1, D2);
Tensor<Scalar, 3> dst(D0, D1, 2 * D2);
A.setRandom();
B.setRandom();
for (auto _ : state) {
dst = A.concatenate(B, 2).square();
benchmark::DoNotOptimize(dst.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * D0 * D1 * 2 * D2 * sizeof(Scalar) * 2);
}
// --- Threaded standalone concat ---
static void BM_Concat2D_Axis1_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);
Tensor<Scalar, 2> B(M, N);
Tensor<Scalar, 2> dst(M, 2 * N);
A.setRandom();
B.setRandom();
ThreadPool tp(threads);
ThreadPoolDevice dev(&tp, threads);
for (auto _ : state) {
dst.device(dev) = A.concatenate(B, 1);
benchmark::DoNotOptimize(dst.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * M * 2 * N * sizeof(Scalar) * 2);
state.counters["threads"] = threads;
}
// --- Threaded compound: (concat(A, B) + C) * D ---
static void BM_Concat2D_Chain_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);
Tensor<Scalar, 2> B(M, N);
Tensor<Scalar, 2> C(2 * M, N);
Tensor<Scalar, 2> D(2 * M, N);
Tensor<Scalar, 2> dst(2 * M, N);
A.setRandom();
B.setRandom();
C.setRandom();
D.setRandom();
ThreadPool tp(threads);
ThreadPoolDevice dev(&tp, threads);
for (auto _ : state) {
dst.device(dev) = (A.concatenate(B, 0) + C) * D;
benchmark::DoNotOptimize(dst.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * 2 * M * N * sizeof(Scalar) * 4);
state.counters["threads"] = threads;
}
// --- Threaded outer-axis compound: (concat(A, B) along axis 1 + C) * D ---
static void BM_Concat2D_Axis1_Chain_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);
Tensor<Scalar, 2> B(M, N);
Tensor<Scalar, 2> C(M, 2 * N);
Tensor<Scalar, 2> D(M, 2 * N);
Tensor<Scalar, 2> dst(M, 2 * N);
A.setRandom();
B.setRandom();
C.setRandom();
D.setRandom();
ThreadPool tp(threads);
ThreadPoolDevice dev(&tp, threads);
for (auto _ : state) {
dst.device(dev) = (A.concatenate(B, 1) + C) * D;
benchmark::DoNotOptimize(dst.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * M * 2 * N * sizeof(Scalar) * 4);
state.counters["threads"] = threads;
}
// --- Writes through a concatenation destination (lvalue writeBlock path) ---
static void BM_ConcatWrite2D_Axis0(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> A(M, N);
Tensor<Scalar, 2> B(M, N);
Tensor<Scalar, 2> src(2 * M, N);
src.setRandom();
A.setZero();
B.setZero();
for (auto _ : state) {
A.concatenate(B, 0) = src;
benchmark::DoNotOptimize(A.data());
benchmark::DoNotOptimize(B.data());
benchmark::ClobberMemory();
}
for (int i = 0; i < M; ++i) {
for (int j = 0; j < N; ++j) {
if (A(i, j) != src(i, j) || B(i, j) != src(M + i, j)) {
state.SkipWithError("validation failed");
return;
}
}
}
state.SetBytesProcessed(state.iterations() * 2ll * M * N * sizeof(Scalar) * 2);
}
static void BM_ConcatWrite2D_Axis1(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> A(M, N);
Tensor<Scalar, 2> B(M, N);
Tensor<Scalar, 2> src(M, 2 * N);
src.setRandom();
A.setZero();
B.setZero();
for (auto _ : state) {
A.concatenate(B, 1) = src;
benchmark::DoNotOptimize(A.data());
benchmark::DoNotOptimize(B.data());
benchmark::ClobberMemory();
}
for (int i = 0; i < M; ++i) {
for (int j = 0; j < N; ++j) {
if (A(i, j) != src(i, j) || B(i, j) != src(i, N + j)) {
state.SkipWithError("validation failed");
return;
}
}
}
state.SetBytesProcessed(state.iterations() * 2ll * M * N * sizeof(Scalar) * 2);
}
// Lazy right-hand-side blocks: writeBlock materializes the cwise expression
// into a temporary before splitting it at the axis.
static void BM_ConcatWrite2D_Axis1_Plus(benchmark::State& state) {
const int M = state.range(0);
const int N = state.range(1);
Tensor<Scalar, 2> A(M, N);
Tensor<Scalar, 2> B(M, N);
Tensor<Scalar, 2> src(M, 2 * N);
src.setRandom();
A.setZero();
B.setZero();
for (auto _ : state) {
A.concatenate(B, 1) = src + src.constant(1.0f);
benchmark::DoNotOptimize(A.data());
benchmark::DoNotOptimize(B.data());
benchmark::ClobberMemory();
}
for (int i = 0; i < M; ++i) {
for (int j = 0; j < N; ++j) {
if (A(i, j) != src(i, j) + 1.0f || B(i, j) != src(i, N + j) + 1.0f) {
state.SkipWithError("validation failed");
return;
}
}
}
state.SetBytesProcessed(state.iterations() * 2ll * M * N * sizeof(Scalar) * 2);
}
// clang-format off
#define CONCAT2D_SIZES \
->Args({256, 256})->Args({1024, 1024})->Args({4096, 4096})
#define CONCAT3D_SIZES \
->Args({64, 64, 64})->Args({128, 128, 128})->Args({256, 256, 64})
#define CONCAT2D_THREAD_SIZES \
->Args({1024, 1024, 1})->Args({1024, 1024, 4})->Args({1024, 1024, 8}) \
->Args({4096, 4096, 1})->Args({4096, 4096, 4})->Args({4096, 4096, 8})
// clang-format on
BENCHMARK(BM_Concat2D_Axis0) CONCAT2D_SIZES->UseRealTime();
BENCHMARK(BM_Concat2D_Axis1) CONCAT2D_SIZES->UseRealTime();
BENCHMARK(BM_Concat3D_OuterAxis) CONCAT3D_SIZES->UseRealTime();
BENCHMARK(BM_Concat2D_Plus) CONCAT2D_SIZES->UseRealTime();
BENCHMARK(BM_Concat2D_Chain) CONCAT2D_SIZES->UseRealTime();
BENCHMARK(BM_Concat2D_Axis1_Plus) CONCAT2D_SIZES->UseRealTime();
BENCHMARK(BM_Concat2D_Axis1_Chain) CONCAT2D_SIZES->UseRealTime();
BENCHMARK(BM_Concat3D_OuterAxis_Square) CONCAT3D_SIZES->UseRealTime();
BENCHMARK(BM_ConcatWrite2D_Axis0) CONCAT2D_SIZES->UseRealTime();
BENCHMARK(BM_ConcatWrite2D_Axis1) CONCAT2D_SIZES->UseRealTime();
BENCHMARK(BM_ConcatWrite2D_Axis1_Plus) CONCAT2D_SIZES->UseRealTime();
BENCHMARK(BM_Concat2D_Axis1_ThreadPool) CONCAT2D_THREAD_SIZES->UseRealTime();
BENCHMARK(BM_Concat2D_Chain_ThreadPool) CONCAT2D_THREAD_SIZES->UseRealTime();
BENCHMARK(BM_Concat2D_Axis1_Chain_ThreadPool) CONCAT2D_THREAD_SIZES->UseRealTime();
BENCHMARK_MAIN();