blob: 3aba55a0fa068c506e9745c128f4e67d37874793 [file]
// Microbenchmarks for ThreadPoolDevice scheduling overhead.
// SPDX-FileCopyrightText: The Eigen Authors
// SPDX-License-Identifier: MPL-2.0
#define EIGEN_USE_THREADS
#include <atomic>
#include <map>
#include <memory>
#include <mutex>
#include <benchmark/benchmark.h>
#include <Eigen/ThreadPool>
#include <contrib/Eigen/Tensor>
using Eigen::Index;
using Eigen::TensorOpCost;
using Eigen::ThreadPool;
using Eigen::ThreadPoolDevice;
// Returns a per-thread-count singleton device, lazily created on first use.
// Pools are reused across benchmark iterations so thread-creation cost does
// not enter the measurement, and varying thread counts across benchmarks is
// supported without later calls silently colliding with the first one.
static ThreadPoolDevice& device(int threads) {
static std::mutex mu;
static std::map<int, std::unique_ptr<ThreadPool>> pools;
static std::map<int, std::unique_ptr<ThreadPoolDevice>> devs;
std::lock_guard<std::mutex> lock(mu);
auto it = devs.find(threads);
if (it != devs.end()) return *it->second;
auto pool = std::unique_ptr<ThreadPool>(new ThreadPool(threads));
auto* dev = new ThreadPoolDevice(pool.get(), threads);
pools.emplace(threads, std::move(pool));
devs.emplace(threads, std::unique_ptr<ThreadPoolDevice>(dev));
return *dev;
}
// ---- enqueue overhead: empty lambda, wait via shared counter -------------
static void BM_EnqueueEmpty(benchmark::State& state) {
const int threads = state.range(0);
auto& dev = device(threads);
const int batch = 1024;
for (auto _ : state) {
std::atomic<int> remaining{batch};
for (int i = 0; i < batch; ++i) {
dev.enqueue([&remaining] { remaining.fetch_sub(1, std::memory_order_release); });
}
while (remaining.load(std::memory_order_acquire) != 0) {
}
}
state.SetItemsProcessed(state.iterations() * batch);
state.counters["threads"] = threads;
}
BENCHMARK(BM_EnqueueEmpty)->Arg(8)->UseRealTime();
// ---- parallelFor with tiny per-element cost: dispatch overhead dominates --
static void BM_ParallelForTiny(benchmark::State& state) {
const Index n = state.range(0);
const int threads = state.range(1);
auto& dev = device(threads);
// Cost just large enough that the cost model decides to parallelize.
const TensorOpCost cost(/*bytes_loaded=*/4, /*bytes_stored=*/4, /*compute_cycles=*/4);
std::atomic<Index> sink{0};
for (auto _ : state) {
dev.parallelFor(n, cost,
[&sink](Index first, Index last) { sink.fetch_add(last - first, std::memory_order_relaxed); });
}
benchmark::DoNotOptimize(sink.load());
state.SetItemsProcessed(state.iterations() * n);
state.counters["threads"] = threads;
}
BENCHMARK(BM_ParallelForTiny)->Args({256, 8})->Args({4096, 8})->Args({65536, 8})->Args({1048576, 8})->UseRealTime();
// ---- parallelFor with realistic per-element compute work ------------------
static void BM_ParallelForCompute(benchmark::State& state) {
const Index n = state.range(0);
const int threads = state.range(1);
auto& dev = device(threads);
const TensorOpCost cost(/*bytes_loaded=*/8, /*bytes_stored=*/8, /*compute_cycles=*/32);
std::vector<double> data(n, 1.0);
for (auto _ : state) {
dev.parallelFor(n, cost, [&data](Index first, Index last) {
for (Index i = first; i < last; ++i) {
data[i] = data[i] * 1.0000001 + 1e-12;
}
});
benchmark::DoNotOptimize(data.data());
}
state.SetItemsProcessed(state.iterations() * n);
state.counters["threads"] = threads;
}
BENCHMARK(BM_ParallelForCompute)->Args({4096, 8})->Args({65536, 8})->Args({1048576, 8})->UseRealTime();
// ---- parallelForAsync: measures async dispatch + heap context overhead ----
static void BM_ParallelForAsync(benchmark::State& state) {
const Index n = state.range(0);
const int threads = state.range(1);
auto& dev = device(threads);
const TensorOpCost cost(8, 8, 4);
for (auto _ : state) {
std::atomic<int> done_flag{0};
dev.parallelForAsync(
n, cost, [](Index, Index) {}, [&done_flag] { done_flag.store(1, std::memory_order_release); });
while (done_flag.load(std::memory_order_acquire) == 0) {
}
}
state.SetItemsProcessed(state.iterations() * n);
state.counters["threads"] = threads;
}
BENCHMARK(BM_ParallelForAsync)->Args({4096, 8})->Args({65536, 8})->Args({1048576, 8})->UseRealTime();
// ---- ThreadPoolDevice::memcpy at various sizes ----------------------------
static void BM_DeviceMemcpy(benchmark::State& state) {
const size_t bytes = state.range(0);
const int threads = state.range(1);
auto& dev = device(threads);
std::vector<char> src(bytes), dst(bytes);
for (auto _ : state) {
dev.memcpy(dst.data(), src.data(), bytes);
benchmark::DoNotOptimize(dst.data());
}
state.SetBytesProcessed(state.iterations() * bytes);
state.counters["threads"] = threads;
}
BENCHMARK(BM_DeviceMemcpy)
->Args({16 << 10, 8})
->Args({256 << 10, 8})
->Args({4 << 20, 8})
->Args({64 << 20, 8})
->UseRealTime();
BENCHMARK_MAIN();