blob: e0c9608400cf911cc43aaeee37be3aa7a21d09b9 [file]
// Copyright 2026 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// Microbenchmarks for frontier operations (compare, merge, is_satisfied).
//
// These operations are on the latency-critical path: network frontier
// propagation → tracker advance → waiter dispatch. The merge fast path
// (same-axis-set epoch-max) executes on every network frontier update in
// steady state. Understanding the cost at various entry counts is essential
// for validating that frontier propagation stays within the "invisible"
// latency budget relative to network RTT.
//
// Build/run:
// iree-bazel-run //runtime/src/iree/async:frontier_benchmark
//
// With detailed stats:
// iree-bazel-run //runtime/src/iree/async:frontier_benchmark -- \
// --benchmark_counters_tabular=true
//
// Key things to look for in results:
// - Fast-path vs slow-path gap: quantifies the value of same-axis detection.
// - Path 2 vs path 3 gap: quantifies the cost of entry movement.
// - Entry count scaling: should be linear. Non-linear growth indicates
// cache effects or branch prediction degradation.
// - "Update" vs "NoChange" merge: isolates the store cost from comparison.
#include <cstdint>
#include <cstring>
#include "benchmark/benchmark.h"
#include "iree/async/frontier.h"
namespace {
//===----------------------------------------------------------------------===//
// Frontier construction helpers
//===----------------------------------------------------------------------===//
// Maximum entry count we benchmark. 128 entries × 16 bytes = 2KB of entry data,
// well within L1 cache but large enough to show scaling behavior.
static constexpr int kMaxEntries = 128;
// Stack storage for a frontier with up to kMaxEntries entries.
struct FrontierStorage {
alignas(16) uint8_t data[sizeof(iree_async_frontier_t) +
kMaxEntries * sizeof(iree_async_frontier_entry_t)];
iree_async_frontier_t* frontier() {
return reinterpret_cast<iree_async_frontier_t*>(data);
}
};
// Populates a frontier with N entries using sequential device indices on the
// given machine. Axes are sorted (device 0, 1, 2, ...) and epochs start at
// base_epoch and increment by epoch_stride.
//
// This gives deterministic, sorted, non-trivial values that the compiler
// cannot constant-fold through a function call boundary.
static void PopulateFrontier(iree_async_frontier_t* frontier, int entry_count,
uint8_t machine, uint64_t base_epoch,
uint64_t epoch_stride) {
iree_async_frontier_initialize(frontier, static_cast<uint8_t>(entry_count));
for (int i = 0; i < entry_count; ++i) {
frontier->entries[i].axis =
iree_async_axis_make_queue(1, machine, static_cast<uint8_t>(i), 0);
frontier->entries[i].epoch = base_epoch + i * epoch_stride;
}
}
// Populates a frontier with even-indexed device axes (0, 2, 4, ...).
// Used for interleaved merge benchmarks.
static void PopulateFrontierEvenAxes(iree_async_frontier_t* frontier,
int entry_count, uint8_t machine,
uint64_t base_epoch) {
iree_async_frontier_initialize(frontier, static_cast<uint8_t>(entry_count));
for (int i = 0; i < entry_count; ++i) {
frontier->entries[i].axis =
iree_async_axis_make_queue(1, machine, static_cast<uint8_t>(i * 2), 0);
frontier->entries[i].epoch = base_epoch + i;
}
}
// Populates a frontier with odd-indexed device axes (1, 3, 5, ...).
// Used for interleaved merge benchmarks.
static void PopulateFrontierOddAxes(iree_async_frontier_t* frontier,
int entry_count, uint8_t machine,
uint64_t base_epoch) {
iree_async_frontier_initialize(frontier, static_cast<uint8_t>(entry_count));
for (int i = 0; i < entry_count; ++i) {
frontier->entries[i].axis = iree_async_axis_make_queue(
1, machine, static_cast<uint8_t>(i * 2 + 1), 0);
frontier->entries[i].epoch = base_epoch + i;
}
}
// Populates a frontier with a half-overlap pattern relative to a sequential
// frontier. The first half of entries share axes with a sequential frontier
// (devices 0..N/2-1), the second half are unique (devices N..N+N/2-1).
static void PopulateFrontierHalfOverlap(iree_async_frontier_t* frontier,
int entry_count, uint8_t machine,
int base_sequential_count,
uint64_t base_epoch) {
iree_async_frontier_initialize(frontier, static_cast<uint8_t>(entry_count));
int overlap_count = entry_count / 2;
int unique_count = entry_count - overlap_count;
// Overlapping entries: same devices as the first half of the sequential set.
for (int i = 0; i < overlap_count; ++i) {
frontier->entries[i].axis =
iree_async_axis_make_queue(1, machine, static_cast<uint8_t>(i), 0);
frontier->entries[i].epoch = base_epoch + i;
}
// Unique entries: devices beyond the sequential set's range.
for (int i = 0; i < unique_count; ++i) {
frontier->entries[overlap_count + i].axis = iree_async_axis_make_queue(
1, machine, static_cast<uint8_t>(base_sequential_count + i), 0);
frontier->entries[overlap_count + i].epoch = base_epoch + overlap_count + i;
}
}
// Saves a copy of a frontier's entries for reset between iterations.
struct FrontierSnapshot {
uint8_t entry_count;
iree_async_frontier_entry_t entries[kMaxEntries];
void capture(const iree_async_frontier_t* frontier) {
entry_count = frontier->entry_count;
memcpy(entries, frontier->entries,
entry_count * sizeof(iree_async_frontier_entry_t));
}
void restore(iree_async_frontier_t* frontier) const {
frontier->entry_count = entry_count;
memcpy(frontier->entries, entries,
entry_count * sizeof(iree_async_frontier_entry_t));
}
};
//===----------------------------------------------------------------------===//
// Compare benchmarks
//===----------------------------------------------------------------------===//
// Fast path: identical axis sets, all epochs equal (result: EQUAL).
// This measures the cost of the axis-matching check + epoch comparison loop
// with perfect branch prediction (no direction changes).
static void BM_Compare_FastPath_Equal(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_a, storage_b;
PopulateFrontier(storage_a.frontier(), entry_count, 0, 100, 10);
PopulateFrontier(storage_b.frontier(), entry_count, 0, 100, 10);
for (auto _ : state) {
auto result =
iree_async_frontier_compare(storage_a.frontier(), storage_b.frontier());
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Compare_FastPath_Equal)
->Arg(1)
->Arg(2)
->Arg(4)
->Arg(8)
->Arg(16)
->Arg(32)
->Arg(64)
->Arg(128);
// Fast path: identical axis sets, a dominates b (result: AFTER).
// All of a's epochs are higher. Measures the same-axes fast path where the
// branch (epoch_a > epoch_b) is always taken.
static void BM_Compare_FastPath_ADominates(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_a, storage_b;
PopulateFrontier(storage_a.frontier(), entry_count, 0, 200, 10);
PopulateFrontier(storage_b.frontier(), entry_count, 0, 100, 10);
for (auto _ : state) {
auto result =
iree_async_frontier_compare(storage_a.frontier(), storage_b.frontier());
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Compare_FastPath_ADominates)->Arg(4)->Arg(16)->Arg(64)->Arg(128);
// Fast path: identical axis sets, concurrent (a ahead on evens, b on odds).
// This is the worst case for the fast path because it cannot early-exit until
// it has seen both a less and a greater epoch.
static void BM_Compare_FastPath_Concurrent(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_a, storage_b;
PopulateFrontier(storage_a.frontier(), entry_count, 0, 100, 10);
PopulateFrontier(storage_b.frontier(), entry_count, 0, 100, 10);
// Make a ahead on even indices, b ahead on odd indices.
for (int i = 0; i < entry_count; ++i) {
if (i % 2 == 0) {
storage_a.frontier()->entries[i].epoch += 50;
} else {
storage_b.frontier()->entries[i].epoch += 50;
}
}
for (auto _ : state) {
auto result =
iree_async_frontier_compare(storage_a.frontier(), storage_b.frontier());
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Compare_FastPath_Concurrent)->Arg(4)->Arg(16)->Arg(64)->Arg(128);
// Slow path: completely disjoint axis sets (no overlap).
// a has machine=0 axes, b has machine=1 axes. Every axis in each frontier is
// unique to it, so the result is always CONCURRENT. Measures the merge-scan
// with the 3-way branch always taking the < or > path (never ==).
static void BM_Compare_SlowPath_Disjoint(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_a, storage_b;
PopulateFrontier(storage_a.frontier(), entry_count, 0, 100, 10);
PopulateFrontier(storage_b.frontier(), entry_count, 1, 100, 10);
for (auto _ : state) {
auto result =
iree_async_frontier_compare(storage_a.frontier(), storage_b.frontier());
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Compare_SlowPath_Disjoint)->Arg(4)->Arg(16)->Arg(64)->Arg(128);
// Slow path: interleaved axis sets (a has even devices, b has odd devices).
// The merge-scan alternates between a-advance and b-advance on every step.
// This gives unpredictable branching (alternating < and > comparisons).
static void BM_Compare_SlowPath_Interleaved(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_a, storage_b;
PopulateFrontierEvenAxes(storage_a.frontier(), entry_count, 0, 100);
PopulateFrontierOddAxes(storage_b.frontier(), entry_count, 0, 100);
for (auto _ : state) {
auto result =
iree_async_frontier_compare(storage_a.frontier(), storage_b.frontier());
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Compare_SlowPath_Interleaved)->Arg(4)->Arg(16)->Arg(64);
// Slow path: half overlap (50% shared axes, 50% unique to each).
// This is the realistic "topology changed slightly" case.
static void BM_Compare_SlowPath_HalfOverlap(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_a, storage_b;
PopulateFrontier(storage_a.frontier(), entry_count, 0, 100, 10);
PopulateFrontierHalfOverlap(storage_b.frontier(), entry_count, 0, entry_count,
100);
for (auto _ : state) {
auto result =
iree_async_frontier_compare(storage_a.frontier(), storage_b.frontier());
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Compare_SlowPath_HalfOverlap)->Arg(4)->Arg(16)->Arg(64);
//===----------------------------------------------------------------------===//
// Merge benchmarks: path 2 (same axis set, epoch-max in place)
//===----------------------------------------------------------------------===//
// Path 2: source has higher epochs on all entries (all stores execute).
// This is the steady-state hot path: same topology, epochs advancing.
// We reset target epochs between iterations to ensure stores actually happen.
static void BM_Merge_EpochMax_AllUpdate(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_target, storage_source;
PopulateFrontier(storage_target.frontier(), entry_count, 0, 100, 10);
PopulateFrontier(storage_source.frontier(), entry_count, 0, 200, 10);
// Save target state for reset.
FrontierSnapshot snapshot;
snapshot.capture(storage_target.frontier());
for (auto _ : state) {
snapshot.restore(storage_target.frontier());
iree_async_frontier_merge(storage_target.frontier(),
static_cast<uint8_t>(entry_count),
storage_source.frontier());
benchmark::DoNotOptimize(storage_target.frontier()->entries[0].epoch);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Merge_EpochMax_AllUpdate)
->Arg(1)
->Arg(2)
->Arg(4)
->Arg(8)
->Arg(16)
->Arg(32)
->Arg(64)
->Arg(128);
// Path 2: source has lower epochs on all entries (no stores, comparison only).
// This measures the pure comparison cost without any memory writes.
// Realistic scenario: receiving a redundant/old frontier update.
static void BM_Merge_EpochMax_NoneUpdate(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_target, storage_source;
PopulateFrontier(storage_target.frontier(), entry_count, 0, 200, 10);
PopulateFrontier(storage_source.frontier(), entry_count, 0, 100, 10);
for (auto _ : state) {
iree_async_frontier_merge(storage_target.frontier(),
static_cast<uint8_t>(entry_count),
storage_source.frontier());
benchmark::DoNotOptimize(storage_target.frontier()->entries[0].epoch);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Merge_EpochMax_NoneUpdate)
->Arg(1)
->Arg(2)
->Arg(4)
->Arg(8)
->Arg(16)
->Arg(32)
->Arg(64)
->Arg(128);
// Path 2: half of entries update (alternating higher/lower epochs).
// Tests branch prediction with unpredictable update pattern.
static void BM_Merge_EpochMax_HalfUpdate(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_target, storage_source;
PopulateFrontier(storage_target.frontier(), entry_count, 0, 100, 10);
PopulateFrontier(storage_source.frontier(), entry_count, 0, 100, 10);
// Source is ahead on even indices, behind on odd indices.
for (int i = 0; i < entry_count; ++i) {
if (i % 2 == 0) {
storage_source.frontier()->entries[i].epoch += 50;
} else {
storage_source.frontier()->entries[i].epoch -= 50;
}
}
FrontierSnapshot snapshot;
snapshot.capture(storage_target.frontier());
for (auto _ : state) {
snapshot.restore(storage_target.frontier());
iree_async_frontier_merge(storage_target.frontier(),
static_cast<uint8_t>(entry_count),
storage_source.frontier());
benchmark::DoNotOptimize(storage_target.frontier()->entries[0].epoch);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Merge_EpochMax_HalfUpdate)->Arg(4)->Arg(16)->Arg(64)->Arg(128);
//===----------------------------------------------------------------------===//
// Merge benchmarks: path 3 (different axis sets, right-to-left merge)
//===----------------------------------------------------------------------===//
// Path 3: source axes entirely after target axes (append pattern).
// All source entries go at the end, target entries don't move.
// This is the cheapest path-3 case: no target entry shifting.
static void BM_Merge_RightToLeft_Append(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
const int capacity = entry_count * 2;
FrontierStorage storage_target, storage_source;
PopulateFrontier(storage_target.frontier(), entry_count, 0, 100, 10);
PopulateFrontier(storage_source.frontier(), entry_count, 1, 100, 10);
FrontierSnapshot snapshot;
snapshot.capture(storage_target.frontier());
for (auto _ : state) {
snapshot.restore(storage_target.frontier());
iree_async_frontier_merge(storage_target.frontier(),
static_cast<uint8_t>(capacity),
storage_source.frontier());
benchmark::DoNotOptimize(storage_target.frontier()->entries[0].epoch);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Merge_RightToLeft_Append)->Arg(4)->Arg(16)->Arg(64);
// Path 3: source axes entirely before target axes (prepend pattern).
// All target entries shift right by source_count positions.
// This is the most expensive path-3 case: maximum target movement.
static void BM_Merge_RightToLeft_Prepend(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
const int capacity = entry_count * 2;
FrontierStorage storage_target, storage_source;
// Target on machine=1 (higher axis values), source on machine=0 (lower).
PopulateFrontier(storage_target.frontier(), entry_count, 1, 100, 10);
PopulateFrontier(storage_source.frontier(), entry_count, 0, 100, 10);
FrontierSnapshot snapshot;
snapshot.capture(storage_target.frontier());
for (auto _ : state) {
snapshot.restore(storage_target.frontier());
iree_async_frontier_merge(storage_target.frontier(),
static_cast<uint8_t>(capacity),
storage_source.frontier());
benchmark::DoNotOptimize(storage_target.frontier()->entries[0].epoch);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Merge_RightToLeft_Prepend)->Arg(4)->Arg(16)->Arg(64);
// Path 3: interleaved axes (target has even devices, source has odd).
// Every source entry inserts between two target entries.
// This is the worst case for branch prediction in the merge scan.
static void BM_Merge_RightToLeft_Interleaved(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
const int capacity = entry_count * 2;
FrontierStorage storage_target, storage_source;
PopulateFrontierEvenAxes(storage_target.frontier(), entry_count, 0, 100);
PopulateFrontierOddAxes(storage_source.frontier(), entry_count, 0, 100);
FrontierSnapshot snapshot;
snapshot.capture(storage_target.frontier());
for (auto _ : state) {
snapshot.restore(storage_target.frontier());
iree_async_frontier_merge(storage_target.frontier(),
static_cast<uint8_t>(capacity),
storage_source.frontier());
benchmark::DoNotOptimize(storage_target.frontier()->entries[0].epoch);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Merge_RightToLeft_Interleaved)->Arg(4)->Arg(16)->Arg(64);
// Path 3: half overlap (50% shared axes get epoch-max, 50% new axes inserted).
// Realistic case: two machines with partially shared topology.
static void BM_Merge_RightToLeft_HalfOverlap(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
// Worst case: entry_count shared + entry_count/2 new = 1.5× original.
const int capacity = entry_count + entry_count;
FrontierStorage storage_target, storage_source;
PopulateFrontier(storage_target.frontier(), entry_count, 0, 100, 10);
PopulateFrontierHalfOverlap(storage_source.frontier(), entry_count, 0,
entry_count, 200);
FrontierSnapshot snapshot;
snapshot.capture(storage_target.frontier());
for (auto _ : state) {
snapshot.restore(storage_target.frontier());
iree_async_frontier_merge(storage_target.frontier(),
static_cast<uint8_t>(capacity),
storage_source.frontier());
benchmark::DoNotOptimize(storage_target.frontier()->entries[0].epoch);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Merge_RightToLeft_HalfOverlap)->Arg(4)->Arg(16)->Arg(64);
//===----------------------------------------------------------------------===//
// Merge benchmarks: path selection overhead
//===----------------------------------------------------------------------===//
// Measures path 1 (source empty). This is the O(1) early exit.
// Establishes the baseline measurement overhead (function call + branch).
static void BM_Merge_SourceEmpty(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_target, storage_source;
PopulateFrontier(storage_target.frontier(), entry_count, 0, 100, 10);
iree_async_frontier_initialize(storage_source.frontier(), 0);
for (auto _ : state) {
iree_async_frontier_merge(storage_target.frontier(),
static_cast<uint8_t>(entry_count),
storage_source.frontier());
benchmark::DoNotOptimize(storage_target.frontier()->entries[0].epoch);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Merge_SourceEmpty)->Arg(4)->Arg(64);
// Measures the fast-path detection overhead: same entry count but DIFFERENT
// axes, forcing fall-through to path 3. Compares to
// BM_Merge_EpochMax_NoneUpdate which has matching axes (stays in path 2). The
// difference is the cost of the axis-matching scan that discovers mismatch +
// path 3 execution.
static void BM_Merge_FastPathMiss(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
const int capacity = entry_count * 2;
FrontierStorage storage_target, storage_source;
PopulateFrontier(storage_target.frontier(), entry_count, 0, 100, 10);
// Same entry count, but different axes (machine=1 vs machine=0).
// The fast-path check compares axes, finds mismatch on entry 0, falls
// through.
PopulateFrontier(storage_source.frontier(), entry_count, 1, 100, 10);
FrontierSnapshot snapshot;
snapshot.capture(storage_target.frontier());
for (auto _ : state) {
snapshot.restore(storage_target.frontier());
iree_async_frontier_merge(storage_target.frontier(),
static_cast<uint8_t>(capacity),
storage_source.frontier());
benchmark::DoNotOptimize(storage_target.frontier()->entries[0].epoch);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Merge_FastPathMiss)->Arg(4)->Arg(16)->Arg(64);
//===----------------------------------------------------------------------===//
// IsSatisfied benchmarks
//===----------------------------------------------------------------------===//
// All entries satisfied (full scan, no early exit).
// This is the common case in steady state: the frontier has been reached.
static void BM_IsSatisfied_AllSatisfied(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_frontier;
PopulateFrontier(storage_frontier.frontier(), entry_count, 0, 100, 10);
// Current epochs are ahead of the frontier on all axes.
iree_async_frontier_entry_t current_epochs[kMaxEntries];
for (int i = 0; i < entry_count; ++i) {
current_epochs[i].axis =
iree_async_axis_make_queue(1, 0, static_cast<uint8_t>(i), 0);
current_epochs[i].epoch = 200 + i * 10;
}
for (auto _ : state) {
bool result = iree_async_frontier_is_satisfied(storage_frontier.frontier(),
current_epochs, entry_count);
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_IsSatisfied_AllSatisfied)
->Arg(1)
->Arg(2)
->Arg(4)
->Arg(8)
->Arg(16)
->Arg(32)
->Arg(64)
->Arg(128);
// First entry not satisfied (immediate early exit).
// Best case: one comparison then return false.
static void BM_IsSatisfied_FirstFails(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_frontier;
PopulateFrontier(storage_frontier.frontier(), entry_count, 0, 100, 10);
// Current epochs: first axis is behind, rest are ahead.
iree_async_frontier_entry_t current_epochs[kMaxEntries];
for (int i = 0; i < entry_count; ++i) {
current_epochs[i].axis =
iree_async_axis_make_queue(1, 0, static_cast<uint8_t>(i), 0);
current_epochs[i].epoch = (i == 0) ? 50 : 200 + i * 10;
}
for (auto _ : state) {
bool result = iree_async_frontier_is_satisfied(storage_frontier.frontier(),
current_epochs, entry_count);
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_IsSatisfied_FirstFails)->Arg(4)->Arg(16)->Arg(64)->Arg(128);
// Last entry not satisfied (almost-full scan then early exit).
// Worst case that still exits early: scans N-1 entries before finding failure.
static void BM_IsSatisfied_LastFails(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_frontier;
PopulateFrontier(storage_frontier.frontier(), entry_count, 0, 100, 10);
// Current epochs: all ahead except the last axis.
iree_async_frontier_entry_t current_epochs[kMaxEntries];
for (int i = 0; i < entry_count; ++i) {
current_epochs[i].axis =
iree_async_axis_make_queue(1, 0, static_cast<uint8_t>(i), 0);
current_epochs[i].epoch = (i == entry_count - 1) ? 50 : 200 + i * 10;
}
for (auto _ : state) {
bool result = iree_async_frontier_is_satisfied(storage_frontier.frontier(),
current_epochs, entry_count);
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_IsSatisfied_LastFails)->Arg(4)->Arg(16)->Arg(64)->Arg(128);
// Midpoint failure: scans exactly half the entries before finding failure.
// Measures the average-case early exit behavior.
static void BM_IsSatisfied_MidpointFails(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_frontier;
PopulateFrontier(storage_frontier.frontier(), entry_count, 0, 100, 10);
// Current epochs: ahead on first half, behind on midpoint.
iree_async_frontier_entry_t current_epochs[kMaxEntries];
int midpoint = entry_count / 2;
for (int i = 0; i < entry_count; ++i) {
current_epochs[i].axis =
iree_async_axis_make_queue(1, 0, static_cast<uint8_t>(i), 0);
current_epochs[i].epoch = (i == midpoint) ? 50 : 200 + i * 10;
}
for (auto _ : state) {
bool result = iree_async_frontier_is_satisfied(storage_frontier.frontier(),
current_epochs, entry_count);
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_IsSatisfied_MidpointFails)->Arg(16)->Arg(64)->Arg(128);
// Current epochs have extra axes beyond what the frontier references.
// The is_satisfied scan must skip past irrelevant axes in current_epochs.
// Measures the cost of the inner while-loop that advances past smaller axes.
static void BM_IsSatisfied_SparseCurrentEpochs(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_frontier;
// Frontier references even-indexed axes only.
PopulateFrontierEvenAxes(storage_frontier.frontier(), entry_count, 0, 100);
// Current epochs include ALL axes (even + odd), so the scan skips odds.
iree_async_frontier_entry_t current_epochs[kMaxEntries * 2];
int current_count = entry_count * 2;
for (int i = 0; i < current_count; ++i) {
current_epochs[i].axis =
iree_async_axis_make_queue(1, 0, static_cast<uint8_t>(i), 0);
current_epochs[i].epoch = 200 + i;
}
for (auto _ : state) {
bool result = iree_async_frontier_is_satisfied(
storage_frontier.frontier(), current_epochs, current_count);
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_IsSatisfied_SparseCurrentEpochs)->Arg(4)->Arg(16)->Arg(64);
//===----------------------------------------------------------------------===//
// Validate benchmarks
//===----------------------------------------------------------------------===//
// Valid frontier (full scan of both loops).
// Establishes the cost of a debug-path validation pass.
static void BM_Validate(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage;
PopulateFrontier(storage.frontier(), entry_count, 0, 100, 10);
for (auto _ : state) {
iree_status_t status = iree_async_frontier_validate(storage.frontier());
benchmark::DoNotOptimize(status);
// status is ok_status (NULL pointer), no free needed.
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Validate)->Arg(1)->Arg(4)->Arg(16)->Arg(64)->Arg(128);
//===----------------------------------------------------------------------===//
// End-to-end scenario benchmarks
//===----------------------------------------------------------------------===//
// Simulates the steady-state frontier propagation hot path:
// receive frontier → compare to local → merge if newer.
// This is what happens on every network frontier update.
static void BM_Scenario_NetworkUpdate(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_local, storage_remote;
PopulateFrontier(storage_local.frontier(), entry_count, 0, 100, 10);
PopulateFrontier(storage_remote.frontier(), entry_count, 0, 110, 10);
FrontierSnapshot snapshot;
snapshot.capture(storage_local.frontier());
for (auto _ : state) {
snapshot.restore(storage_local.frontier());
// Compare to see if update is newer.
auto comparison = iree_async_frontier_compare(storage_local.frontier(),
storage_remote.frontier());
benchmark::DoNotOptimize(comparison);
// If remote is ahead, merge it in.
if (comparison == IREE_ASYNC_FRONTIER_BEFORE) {
iree_async_frontier_merge(storage_local.frontier(),
static_cast<uint8_t>(entry_count),
storage_remote.frontier());
}
benchmark::DoNotOptimize(storage_local.frontier()->entries[0].epoch);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Scenario_NetworkUpdate)
->Arg(1)
->Arg(2)
->Arg(4)
->Arg(8)
->Arg(16)
->Arg(32)
->Arg(64);
// Simulates checking if a pending operation can execute:
// a waiter's frontier is checked against the tracker's current state.
// This happens on every advance() call for every pending waiter.
static void BM_Scenario_WaiterCheck(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_waiter;
PopulateFrontier(storage_waiter.frontier(), entry_count, 0, 100, 10);
// Tracker state: most axes have advanced past the waiter's requirements,
// but we vary which one is "last to arrive" to avoid perfect prediction.
iree_async_frontier_entry_t tracker_epochs[kMaxEntries];
for (int i = 0; i < entry_count; ++i) {
tracker_epochs[i].axis =
iree_async_axis_make_queue(1, 0, static_cast<uint8_t>(i), 0);
// All epochs at or above the frontier's targets.
tracker_epochs[i].epoch = 100 + i * 10;
}
for (auto _ : state) {
bool satisfied = iree_async_frontier_is_satisfied(
storage_waiter.frontier(), tracker_epochs, entry_count);
benchmark::DoNotOptimize(satisfied);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_Scenario_WaiterCheck)->Arg(1)->Arg(4)->Arg(8)->Arg(16)->Arg(32);
//===----------------------------------------------------------------------===//
// FindUndominated benchmarks
//===----------------------------------------------------------------------===//
// Benchmarks for iree_async_frontier_find_undominated: the combined tier 1/2
// extraction for wait resolution. Measures how quickly we can identify which
// axes in a semaphore's frontier are not yet dominated by the queue's frontier.
//
// The hot path is the fast path (identical axis sets) which fires when the
// same set of queues has been signaling the semaphore in steady state.
// The slow path fires when topologies change (new queue joins, device added).
// Output storage for undominated entries. Reused across iterations.
static iree_async_frontier_entry_t g_undominated_entries[kMaxEntries];
// Fast path: reference dominates all entries (all epochs >=).
// This is the best case for same-queue sequential work: the queue's frontier
// is ahead on every axis, so all waits are FIFO-elided.
static void BM_FindUndominated_FastPath_AllDominated(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_ref, storage_tgt;
PopulateFrontier(storage_ref.frontier(), entry_count, 0, 200, 10);
PopulateFrontier(storage_tgt.frontier(), entry_count, 0, 100, 10);
for (auto _ : state) {
uint8_t count = iree_async_frontier_find_undominated(
storage_ref.frontier(), storage_tgt.frontier(),
static_cast<uint8_t>(entry_count), g_undominated_entries);
benchmark::DoNotOptimize(count);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_FindUndominated_FastPath_AllDominated)
->Arg(1)
->Arg(2)
->Arg(4)
->Arg(8)
->Arg(16)
->Arg(32)
->Arg(64)
->Arg(128);
// Fast path: all entries undominated (target ahead on every axis).
// This is the worst case for the fast path: every entry triggers a write.
// Measures the combined comparison + output write cost.
static void BM_FindUndominated_FastPath_AllUndominated(
benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_ref, storage_tgt;
PopulateFrontier(storage_ref.frontier(), entry_count, 0, 100, 10);
PopulateFrontier(storage_tgt.frontier(), entry_count, 0, 200, 10);
for (auto _ : state) {
uint8_t count = iree_async_frontier_find_undominated(
storage_ref.frontier(), storage_tgt.frontier(),
static_cast<uint8_t>(entry_count), g_undominated_entries);
benchmark::DoNotOptimize(count);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_FindUndominated_FastPath_AllUndominated)
->Arg(1)
->Arg(2)
->Arg(4)
->Arg(8)
->Arg(16)
->Arg(32)
->Arg(64)
->Arg(128);
// Fast path: alternating dominated/undominated entries.
// Worst case for branch prediction on the output write.
static void BM_FindUndominated_FastPath_HalfUndominated(
benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_ref, storage_tgt;
// Reference: even indices have high epochs, odd have low.
PopulateFrontier(storage_ref.frontier(), entry_count, 0, 100, 10);
PopulateFrontier(storage_tgt.frontier(), entry_count, 0, 100, 10);
// Make alternating entries undominated.
for (int i = 0; i < entry_count; ++i) {
if (i % 2 == 0) {
storage_tgt.frontier()->entries[i].epoch =
storage_ref.frontier()->entries[i].epoch + 50;
}
}
for (auto _ : state) {
uint8_t count = iree_async_frontier_find_undominated(
storage_ref.frontier(), storage_tgt.frontier(),
static_cast<uint8_t>(entry_count), g_undominated_entries);
benchmark::DoNotOptimize(count);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_FindUndominated_FastPath_HalfUndominated)
->Arg(4)
->Arg(8)
->Arg(16)
->Arg(32)
->Arg(64)
->Arg(128);
// Slow path: completely disjoint axis sets (ref on machine 0, tgt on machine
// 1). All target entries are undominated. Maximum output write volume.
static void BM_FindUndominated_SlowPath_Disjoint(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_ref, storage_tgt;
PopulateFrontier(storage_ref.frontier(), entry_count, 0, 100, 10);
PopulateFrontier(storage_tgt.frontier(), entry_count, 1, 100, 10);
for (auto _ : state) {
uint8_t count = iree_async_frontier_find_undominated(
storage_ref.frontier(), storage_tgt.frontier(),
static_cast<uint8_t>(entry_count), g_undominated_entries);
benchmark::DoNotOptimize(count);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_FindUndominated_SlowPath_Disjoint)
->Arg(4)
->Arg(8)
->Arg(16)
->Arg(32)
->Arg(64)
->Arg(128);
// Slow path: interleaved axes (ref on even devices, tgt on odd devices).
// Every merge-scan step takes the target-only branch. Worst case for branch
// prediction in the 3-way merge scan.
static void BM_FindUndominated_SlowPath_Interleaved(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_ref, storage_tgt;
PopulateFrontierEvenAxes(storage_ref.frontier(), entry_count, 0, 100);
PopulateFrontierOddAxes(storage_tgt.frontier(), entry_count, 0, 100);
for (auto _ : state) {
uint8_t count = iree_async_frontier_find_undominated(
storage_ref.frontier(), storage_tgt.frontier(),
static_cast<uint8_t>(entry_count), g_undominated_entries);
benchmark::DoNotOptimize(count);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_FindUndominated_SlowPath_Interleaved)
->Arg(4)
->Arg(8)
->Arg(16)
->Arg(32)
->Arg(64);
// Slow path: half-overlap pattern. First half of target shares axes with
// reference (mixed dominated/undominated), second half is unique (always
// undominated). Realistic topology-change scenario.
static void BM_FindUndominated_SlowPath_HalfOverlap(benchmark::State& state) {
const int entry_count = static_cast<int>(state.range(0));
FrontierStorage storage_ref, storage_tgt;
PopulateFrontier(storage_ref.frontier(), entry_count, 0, 100, 10);
PopulateFrontierHalfOverlap(storage_tgt.frontier(), entry_count, 0,
entry_count, 200);
for (auto _ : state) {
uint8_t count = iree_async_frontier_find_undominated(
storage_ref.frontier(), storage_tgt.frontier(),
static_cast<uint8_t>(entry_count), g_undominated_entries);
benchmark::DoNotOptimize(count);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(BM_FindUndominated_SlowPath_HalfOverlap)
->Arg(4)
->Arg(8)
->Arg(16)
->Arg(32)
->Arg(64);
} // namespace