| // 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(); |