Skip to content

Latest commit

 

History

History
1038 lines (774 loc) · 32.5 KB

File metadata and controls

1038 lines (774 loc) · 32.5 KB

API Reference

Namespace: vernier::bench Platform: Linux-only C++ Standard: C++20 (C++23 used when available)

Complete API documentation for the benchmarking framework. This reference covers all CPU and GPU APIs, profiling integration, and configuration options.


Table of Contents


Quick Start

#include <gtest/gtest.h>
#include "Perf.hpp"

namespace ub = vernier::bench;

PERF_TEST(MyComponent, Throughput) {
  UB_PERF_GUARD(perf);  // Creates PerfCase with config from command-line

  // Setup test data
  std::vector<int> data(10000, 42);

  // Warmup
  perf.warmup([&] {
    for (int i = 0; i < perf.cycles(); ++i) {
      volatile int result = processData(data);
      (void)result;
    }
  });

  // Measure
  volatile int sink = 0;
  auto result = perf.throughputLoop([&] {
    sink += processData(data);
  }, "throughput");

  // Validate
  EXPECT_GT(result.callsPerSecond, 10000) << "Throughput too low";
  EXPECT_STABLE_CV_CPU(result, perf.config());
}

PERF_MAIN()  // Standard main() with CSV export

Note: See demo/ for complete working examples with step-by-step walkthroughs.


CPU API

Test Macros

PERF_TEST(SuiteName, TestName)           // General performance test
PERF_THROUGHPUT(SuiteName, TestName)     // Throughput measurement
PERF_LATENCY(SuiteName, TestName)        // Latency distribution
PERF_CONTENTION(SuiteName, TestName)     // Multi-threaded test

UB_PERF_GUARD(varName)                   // Create scoped PerfCase
PERF_MAIN()                              // Main function with CSV export

PerfCase

Core CPU benchmark harness.

Constructor:

PerfCase(std::string testName, PerfConfig cfg);

Key methods:

// Warmup (run before measurements to prime caches)
void warmup(std::function<void()> fn);

// Measurement functions
PerfResult throughputLoop(std::function<void()> op,
                         std::string label = "throughput",
                         std::optional<MemoryProfile> memProfile = std::nullopt);

PerfResult measured(std::function<void()> fn,
                   std::string label = "measured");

PerfResult contentionRun(const std::function<void()>& worker,
                        std::string label = "contention");

// Hooks (optional)
void setBeforeMeasureHook(std::function<void(const PerfCase&)> hook);
void setAfterMeasureHook(std::function<void(const PerfCase&, const Stats&)> hook);

// Accessors
int cycles() const noexcept;
int repeats() const noexcept;
int threads() const noexcept;
int warmup() const noexcept;
const PerfConfig& config() const noexcept;
const std::string& testName() const noexcept;

Method semantics:

throughputLoop(op, label, memProfile)

  • Framework calls op() exactly cycles() times
  • Use for: Per-operation measurements
  • MemoryProfile: Specify per-operation bytes

measured(fn, label)

  • Framework calls fn() once (you do the loop inside)
  • Use for: Batch measurements, custom loop logic
  • Your lambda should perform exactly cycles() operations

contentionRun(worker, label)

  • Framework spawns threads() workers, each calling worker() in a loop
  • Use for: Multi-threaded scalability testing
  • Each worker performs cycles() operations

PerfResult

struct PerfResult {
  Stats stats{};                            // Statistical summary
  double callsPerSecond{};                  // Throughput (1e6 / median_us)
  std::string label;                        // Section label
  std::optional<MemoryProfile> memoryProfile{}; // Stored input profile (if provided)
  bool stable{true};                        // CV% below adaptive threshold
  double cvThreshold{0.05};                 // Threshold used (from recommendedCVThreshold)
};

Note: memoryProfile contains the input profile you passed (if any). Framework automatically prints bandwidth analysis - you don't need to access this field directly.

Stats

struct Stats {
  double median;   // 50th percentile (us per call)
  double p10;      // 10th percentile
  double p90;      // 90th percentile
  double min;      // Minimum latency
  double max;      // Maximum latency
  double mean;     // Arithmetic mean
  double stddev;   // Standard deviation
  double cv;       // Coefficient of variation (stddev/mean)
};

CV% interpretation:

  • < 0.05 (5%): Excellent stability
  • 0.05 - 0.10 (5-10%): Good
  • > 0.10 (>10%): High jitter - investigate

PerfConfig

struct PerfConfig {
  int cycles = 10000;               // Operations per repeat
  int repeats = 10;                 // Samples collected
  int warmup = 1;                   // Warmup repeats (0 = auto-scale)
  int threads = 1;                  // Worker threads
  int msgBytes = 64;                // Payload size (bytes)
  bool console = false;             // Echo to console (when relevant)
  bool nonBlocking = false;         // Non-blocking mode (when relevant)
  std::string minLevel = "INFO";    // DEBUG|INFO|WARNING|ERROR|FATAL
  std::optional<std::string> csv{}; // CSV output path

  // Profiling knobs (default off)
  std::string profileTool;             // "perf", "gperf", "bpftrace", "rapl", "callgrind"
  std::string profileArgs;             // Verbatim pass-through to the tool
  std::vector<std::string> bpfScripts; // Script names/paths: "fsync_latency", "write_latency"
  std::string artifactRoot;            // Profiler artifact directory
  int profileFrequency = 10000;        // Sampling Hz for CPU profilers
  bool profileAnalyze = false;         // Auto-run analysis after profiling

  bool quickMode = false;              // Apply reduced cycles/repeats
};

MemoryProfile

struct MemoryProfile {
  size_t bytesRead = 0;      // Input data size per operation (bytes)
  size_t bytesWritten = 0;   // Output data size per operation (bytes)
  size_t bytesAllocated = 0; // Heap allocations per operation (optional)

  // Helper methods
  [[nodiscard]] double bandwidthMBs(double durationUs) const;
  [[nodiscard]] double efficiency(double durationUs, double peakMBs) const;
};

Usage with throughputLoop (per-operation bytes):

// Specify bytes accessed PER OPERATION
ub::MemoryProfile memProfile{
  .bytesRead = 1024,     // Each call reads 1KB
  .bytesWritten = 1024,  // Each call writes 1KB
  .bytesAllocated = 0
};

auto result = perf.throughputLoop([&]{
  processData();  // Framework calls this cycles() times
}, "throughput", memProfile);

// Framework automatically prints:
// - Memory bandwidth (MB/s)
// - Estimated efficiency (% of theoretical peak)
// - Hints (CPU-bound vs memory-bound)

Currently supported by: throughputLoop() only

Output example:

Memory bandwidth: 9403.3 MB/s (1.0 MB read, 1.0 MB written per call)
Estimated efficiency: 78.4% of theoretical peak (~12000 MB/s)
Hint: High bandwidth utilization -> Memory-bound (consider memory layout)

Interpretation:

  • Efficiency < 10%: CPU-bound (algorithm optimization needed)
  • Efficiency > 50%: Memory-bound (memory layout optimization needed)

Profiler Integration

void attachProfilerHooks(PerfCase& perf, const PerfConfig& cfg);

Supported profilers (each self-registers via the backend registry):

CPU:

  • perf - Linux perf_events (stat / record / mem / c2c modes)
  • gperf - gperftools (CPU + optional heap sampling)
  • callgrind - valgrind callgrind (deterministic instruction counts)
  • bpftrace - BPF tracing (syscall / fsync latency scripts)
  • rapl - Intel RAPL (energy measurement)
  • massif - valgrind massif (heap usage timeline, ~20x overhead)
  • memcheck - valgrind memcheck (memory errors and leaks)
  • helgrind - valgrind helgrind / DRD (data races, lock-order violations; --profile-args drd selects DRD)
  • offcpu - bpftrace finish_task_switch (where threads spend blocked time)
  • heaptrack - heaptrack (low-overhead heap profiler, ~1.5x)
  • jemalloc - jemalloc prof sampling (~5-10%, LD_PRELOAD)

GPU:

  • nsight - NVIDIA Nsight Systems / Compute (auto-extracts the four canonical nsys stats reports)
  • compute-sanitizer - NVIDIA Compute Sanitizer (GPU memcheck / racecheck / synccheck / initcheck)
  • rocprof - AMD ROCm rocprof (kernel timing + Chrome-trace timeline)

Adjacent in-process instrumentation:

  • CUPTI - per-launch register count, static + dynamic shared memory, launch count populate the GPU CSV section on every PERF_GPU_* test (no --profile flag required).
  • NVTX - BENCH_NVTX_SCOPE("name") / BENCH_NVTX_MARK("name") macros label the nsys timeline; NsightProfiler auto-emits a range named after the test around each measured window.

Example:

PERF_TEST(MyComponent, Throughput) {
  UB_PERF_GUARD(perf);
  ub::attachProfilerHooks(perf, ub::detail::getPerfConfig());

  perf.warmup([&]{ /* ... */ });
  auto result = perf.throughputLoop([&]{ /* ... */ }, "op");
}

Command line:

# CPU profiling with perf
./MyComponent_PTEST --profile perf --gtest_filter="*Throughput"

# Heap profiling with gperftools
./MyComponent_PTEST --profile gperf --gtest_filter="*Throughput"

# Energy measurement with RAPL
./MyComponent_PTEST --profile rapl --csv results.csv

Validation Macros

// Adaptive CV% validation (threshold based on payload size)
// Pass entire config object (not just msgBytes)
EXPECT_STABLE_CV_CPU(result, perf.config());

// Manual threshold
EXPECT_LT(result.stats.cv, 0.10) << "High jitter";

Macro details:

EXPECT_STABLE_CV_CPU(result, cfg)
  • Parameters:

    • result: PerfResult from measured() or throughputLoop()
    • cfg: PerfConfig object (must have .msgBytes and .quickMode members)
  • Examples:

    EXPECT_STABLE_CV_CPU(result, perf.config());  // Correct
    EXPECT_STABLE_CV_CPU(result, getCfg());       // Correct (if getCfg() returns PerfConfig)
  • Automatic thresholds:

    • <64B: 20% (tiny payloads)
    • <256B: 10% (small payloads)
    • <1KB: 5% (medium payloads)
    • =1KB: 3% (large payloads)

    • Quick mode: +50% relaxation (capped at 30%)

GPU API

Test Macros

PERF_GPU_TEST(SuiteName, TestName)       // General GPU test
PERF_GPU_THROUGHPUT(SuiteName, TestName) // Throughput-focused test
PERF_GPU_LATENCY(SuiteName, TestName)    // Latency-focused test
PERF_GPU_COMPARISON(SuiteName, TestName) // CPU vs GPU comparison
PERF_GPU_BANDWIDTH(SuiteName, TestName)  // Memory bandwidth test
PERF_GPU_SCALING(SuiteName, TestName)    // Multi-GPU scaling test

UB_PERF_GPU_GUARD(varName)               // Create scoped PerfGpuCase
PERF_GPU_MAIN()                          // Main with GPU support

PerfGpuCase

Core GPU benchmark harness.

Key methods:

// CPU baseline (call before the GPU kernel to populate speedupVsCpu)
PerfResult cpuBaseline(CpuFn fn, std::string label = "cpu_baseline");

// Kernel builder (fluent interface). KernelFn = std::function<void(cudaStream_t)>.
CudaKernelBuilder cudaKernel(KernelFn kernel, std::string label = "cuda_kernel");

// Multi-GPU builder. MultiGpuKernelFn = std::function<void(int deviceId, cudaStream_t)>.
MultiGpuKernelBuilder cudaKernelMultiGpu(int deviceCount, MultiGpuKernelFn kernel,
                                         std::string label = "multi_gpu_kernel");

// Warmup the kernel before measuring
void cudaWarmup(KernelFn kernel);

// Accessors
int cycles() const noexcept;
int repeats() const noexcept;
const PerfConfig& cpuConfig() const noexcept;
const PerfGpuConfig& gpuConfig() const noexcept;
const std::string& testName() const noexcept;
cudaStream_t stream() const noexcept;

CudaKernelBuilder

Fluent interface for kernel configuration and measurement.

class CudaKernelBuilder {
public:
  // Configuration (optional)
  CudaKernelBuilder& withHostToDevice(const void* src, void* dst, size_t bytes);
  CudaKernelBuilder& withDeviceToHost(const void* src, void* dst, size_t bytes);
  CudaKernelBuilder& withLaunchConfig(dim3 grid, dim3 block, size_t sharedMemBytes = 0);
  CudaKernelBuilder& withDeviceId(int deviceId);

  // Measurement (label is set on cudaKernel(), not here)
  PerfGpuResult measure();
};

Usage pattern:

PERF_GPU_TEST(MyKernel, Throughput) {
  UB_PERF_GPU_GUARD(perf);

  // Setup
  std::vector<float> h_input(N);
  std::vector<float> h_output(N);
  float *d_input, *d_output;
  cudaMalloc(&d_input, N * sizeof(float));
  cudaMalloc(&d_output, N * sizeof(float));

  dim3 block(256);
  dim3 grid((N + 255) / 256);

  perf.cudaWarmup([&](cudaStream_t s) {
    myKernel<<<grid, block, 0, s>>>(d_input, d_output, N);
  });

  // Measure kernel performance
  auto result = perf.cudaKernel([&](cudaStream_t s) {
    myKernel<<<grid, block, 0, s>>>(d_input, d_output, N);
  }, "myKernel")
    .withHostToDevice(h_input.data(), d_input, N * sizeof(float))
    .withDeviceToHost(d_output, h_output.data(), N * sizeof(float))
    .withLaunchConfig(grid, block)
    .measure();

  // Validate
  EXPECT_GT(result.callsPerSecond, 1000) << "GPU throughput too low";
  EXPECT_STABLE_CV_GPU(result, perf.cpuConfig());

  cudaFree(d_input);
  cudaFree(d_output);
}

PerfGpuResult

struct PerfGpuResult {
  GpuStats stats{};         // Comprehensive GPU statistics (see GpuStats below)
  double kernelTimeUs{};    // CUDA event kernel time
  double transferTimeUs{};  // H2D + D2H transfer time
  double totalTimeUs{};     // Kernel + transfers
  double callsPerSecond{};  // 1e6 / totalTimeUs
  double speedupVsCpu{};    // Set by cpuBaseline() comparison
  std::string label;        // Kernel label from cudaKernel()
  int deviceId{-1};         // CUDA device ID
};

struct GpuStats {
  Stats cpuStats{};                    // CPU-side timing
  GpuDeviceInfo deviceInfo{};          // GPU model, compute capability
  MemoryTransferProfile transfers{};   // H2D/D2H bytes and bandwidthGBs()
  OccupancyMetrics occupancy{};        // achievedOccupancy, block size, warps
  ClockSpeedProfile clocks{};          // smClockMHzEnd, isThrottling()
  PowerThermalProfile powerThermal{};  // Power draw, temperature (NVML)
  CuptiKernelStats cupti{};            // In-process per-launch kernel metrics
  double kernelTimeMedianUs{};
  double transferTimeMedianUs{};
  double totalTimeMedianUs{};
  std::optional<MultiGpuMetrics> multiGpu{};        // Only in cudaKernelMultiGpu()
  std::optional<P2PTransferProfile> p2pProfile{};   // Only with P2P transfers
  std::optional<UnifiedMemoryProfile> unifiedMemory{}; // Only with managed memory
};

Validation Macros

// GPU CV% validation
EXPECT_STABLE_CV_GPU(gpuResult, perf.cpuConfig());

// Occupancy validation
EXPECT_GT(gpuResult.stats.occupancy.achievedOccupancy, 0.5)
  << "Low occupancy - kernel may be register/shared memory bound";

// Bandwidth validation
EXPECT_GT(gpuResult.stats.transfers.bandwidthGBs(), 10.0)
  << "Low transfer bandwidth - check PCIe link";

Profiler API

Common Interface

All profilers implement:

class Profiler {
public:
  virtual ~Profiler() = default;
  virtual std::string toolName() const noexcept = 0;
  virtual std::string artifactDir() const noexcept = 0;
  virtual void beforeMeasure() {}
  virtual void afterMeasure(const Stats& s) {}

  // Factory: returns a concrete profiler or no-op based on cfg.profileTool
  static std::unique_ptr<Profiler> make(const PerfConfig& cfg, const std::string& testName);
};

ProfilerPerf

Linux perf integration for CPU profiling.

Features:

  • Hardware performance counters
  • Call graph sampling
  • Cache miss analysis
  • Branch prediction analysis

Usage:

./MyComponent_PTEST --profile perf --gtest_filter="*Throughput"
# Generates: MyComponent.Throughput.perf/perf.data

Analysis:

perf report -i MyComponent.Throughput.perf/perf.data
perf annotate -i MyComponent.Throughput.perf/perf.data

ProfilerGperf

Google Performance Tools integration.

Features:

  • CPU profiling (sampling)
  • Heap profiling (--profile-args heap, or both for CPU + heap)

Usage:

# CPU profiling (default) -> MyComponent.Throughput.gperf/cpu.prof
./MyComponent_PTEST --profile gperf --gtest_filter="*Throughput"

# Heap profiling -> MyComponent.Throughput.gperf/heap.<N>.heap
./MyComponent_PTEST --profile gperf --profile-args heap

Analysis:

google-pprof --text ./MyComponent_PTEST MyComponent.Throughput.gperf/cpu.prof
google-pprof --pdf ./MyComponent_PTEST MyComponent.Throughput.gperf/cpu.prof > profile.pdf

ProfilerBpftrace

BPF-based kernel tracing.

Features:

  • Off-CPU analysis
  • Syscall tracing
  • Custom probe points

Usage:

# Requires root or CAP_BPF capability
sudo ./MyComponent_PTEST --profile bpftrace --bpf fsync_latency

ProfilerRAPL

Intel RAPL energy measurement.

Features:

  • Package energy consumption
  • DRAM energy consumption
  • Power efficiency analysis

Usage:

# Requires root for /dev/cpu/*/msr access
sudo ./MyComponent_PTEST --profile rapl --csv results.csv

ProfilerNsight (GPU)

NVIDIA Nsight Compute integration.

Features:

  • Kernel profiling
  • Memory bandwidth analysis
  • Occupancy analysis
  • Roofline model

Usage:

# Requires NVIDIA driver with profiling support
./MyGpuTest_PTEST --profile nsight --gtest_filter="*Kernel"

CSV Schema

CPU Columns

Column Type Description
test string Suite.TestName
cycles int Operations per repeat
repeats int Number of repeats
warmup int Warmup iterations used
threads int Worker thread count
msgBytes int Payload size (bytes)
console bool Console output enabled
nonBlocking bool Non-blocking mode enabled
minLevel string Minimum log level
wallMedian double Median latency (us)
wallP10 double 10th percentile (us)
wallP90 double 90th percentile (us)
wallMin double Minimum latency (us)
wallMax double Maximum latency (us)
wallMean double Mean latency (us)
wallStddev double Standard deviation (us)
wallCV double Coefficient of variation
callsPerSecond double Throughput (ops/sec)
stable bool CV below adaptive threshold
cvThreshold double Adaptive CV threshold used
profileTool string Profiler used (if any)
profileDir string Profiler artifact path
timestamp string ISO 8601 timestamp
gitHash string Short commit hash
hostname string Machine hostname
platform string Architecture (x86_64, arm64)

GPU Columns

Additional columns for GPU tests:

Column Type Description
gpuModel string GPU device name
computeCapability string CUDA compute capability
kernelTimeUs double GPU kernel time (us)
transferTimeUs double H2D + D2H time (us)
h2dBytes int64 Host-to-device bytes
d2hBytes int64 Device-to-host bytes
speedupVsCpu double GPU/CPU speedup ratio
memBandwidthGBs double Memory bandwidth (GB/s)
occupancy double Kernel occupancy [0.0-1.0]
smClockMHz double SM clock frequency
throttling bool Thermal throttling detected

Power and Thermal Columns

Column Type Description
powerDrawW double Average GPU package power (watts)
powerLimitW double Configured power-cap limit (watts)
temperatureC int Peak GPU temperature during the run (C)
temperatureDeltaC int Temperature delta from start to end (C)

Source: NVML. Populated on every NVIDIA GPU run.

CUPTI In-Process Kernel Metrics

Column Type Description
cuptiKernelLaunches int Kernel launches in the measure() window
cuptiRegistersMedian int Median registers per thread across launches
cuptiRegistersMax int Max registers per thread observed
cuptiStaticSmemBytes int Median static __shared__ allocation per launch
cuptiDynamicSmemBytes int Median dynamic shared memory passed at launch

Source: CUPTI Activity API. Populated when libcupti is linked at build time; no --profile flag required.

Multi-GPU Columns

Column Type Description
deviceId int Primary CUDA device ID
deviceCount int Number of GPUs used
multiGpuEfficiency double Parallel efficiency
p2pBandwidthGBs double Peer-to-peer bandwidth

Unified Memory Columns

Column Type Description
umPageFaults int64 GPU page faults
umH2DMigrations int64 Host-to-device migrations
umD2HMigrations int64 Device-to-host migrations
umMigrationTimeUs double Total migration overhead
umThrashing bool Thrashing detected

Configuration Flags

Core Flags

Flag Type Default Description
--cycles N int 10000 Operations per repeat
--repeats N int 10 Number of measurement repeats
--warmup N int 1 Warmup iterations (0 = auto)
--threads N int 1 Worker thread count
--msg-bytes N int 64 Payload size (bytes)
--quick bool false Fast iteration (fewer cycles/repeats)

Output Flags

Flag Type Default Description
--csv PATH string - CSV output path
--console bool false Echo to console (when relevant)
--nonblocking bool false Non-blocking mode (when relevant)
--min-level STR string "INFO" DEBUG|INFO|WARNING|ERROR|FATAL

Profiling Flags

Flag Type Default Description
--profile TOOL string - Profiler: perf|gperf|bpftrace|rapl|callgrind|massif|memcheck|helgrind|offcpu|heaptrack|jemalloc|nsight|compute-sanitizer|rocprof
--profile-args ARGS string - Profiler-specific arguments
--profile-output-dir path - Where backend artifacts land (alias of --artifact-root)
--profile-test-timeout int 300 Per-test watchdog seconds under --profile (0 disables)
--profile-check flag - Print binary readiness + per-backend env doctor, then exit
--artifact-root DIR string . Profiler output directory
--profile-frequency N int 10000 Sampling Hz for CPU profilers
--profile-analyze bool false Auto-run analysis after profiling
--bpf LIST string - BPF script names or paths (comma-separated), resolved under --bpf-scripts: e.g. fsync_latency,write_latency

GPU-Specific Flags

Flag Type Default Description
--gpu-device N int 0 CUDA device ID
--gpu-warmup N int 10 GPU warmup iterations
--gpu-memory MODE string explicit Memory strategy: explicit|unified|pinned|mapped
--min-speedup FLOAT double 1.0 Minimum expected GPU speedup

GoogleTest Integration

Flag Description
--gtest_filter=PATTERN Run specific tests (wildcards: *, ?)
--gtest_list_tests List available tests
--gtest_repeat=N Repeat test suite N times

Examples:

# Run all tests with CSV export
./MyComponent_PTEST --csv results.csv

# Quick iteration during development
./MyComponent_PTEST --quick --gtest_filter="*Throughput"

# Multi-threaded test with profiling
./MyComponent_PTEST --threads 8 --profile perf --gtest_filter="*Contention"

# GPU test with specific device
./MyGpuTest_PTEST --gpu-device 1 --gpu-memory unified --csv gpu_results.csv

# Production CI run
./MyComponent_PTEST --cycles 100000 --repeats 30 --csv ci_results.csv

Helper Functions

Global Config Access

const PerfConfig& vernier::bench::detail::getPerfConfig();

Access global configuration parsed from command-line flags. Used for profiler integration and custom validation.

Metadata Capture

// Capture all metadata
std::tuple<std::string, std::string, std::string, std::string>
captureMetadata(bool cacheMetadata = true);

// Individual functions
std::string captureTimestamp();   // ISO 8601 format
std::string captureGitHash();     // Short commit hash
std::string captureHostname();    // Machine name
std::string capturePlatform();    // Architecture (x86_64, arm64, etc.)

Usage:

auto [timestamp, gitHash, hostname, platform] = captureMetadata();
std::printf("Test run: %s on %s (%s) [%s]\n",
            timestamp.c_str(), hostname.c_str(),
            platform.c_str(), gitHash.c_str());

Timing Utilities

// Get current time in microseconds
double nowUs();

High-precision wall-clock timing using std::chrono::high_resolution_clock.

Statistics

// Compute all statistics from raw samples
Stats summarize(std::vector<double>& samples);

Note: Input vector is modified (sorted for percentile calculation).

Preventing Compiler Optimization

Use the volatile keyword pattern:

// Force compiler to compute result
int sum = computeSum(data);
volatile int result = sum;  // Prevents dead code elimination

// OR: Accumulate into volatile
volatile int sink = 0;
auto result = perf.throughputLoop([&] {
  sink += computeValue();
}, "operation");

Why volatile?

  • Simple and portable
  • Works with all compilers
  • Clear intent to code reviewers
  • Template-friendly (no function calls needed)

Thread Safety

  • PerfCase: Not thread-safe (create one per test)
  • GpuPerfCase: Not thread-safe (create one per test)
  • contentionRun: Thread-safe (uses internal synchronization)
  • PerfRegistry: Thread-safe (uses mutex for CSV updates)

Best practices:

// CORRECT: One PerfCase per test
PERF_TEST(MyComponent, Test1) {
  UB_PERF_GUARD(perf);  // Independent instance
  // ...
}

PERF_TEST(MyComponent, Test2) {
  UB_PERF_GUARD(perf);  // Independent instance
  // ...
}

// INCORRECT: Sharing PerfCase across threads
PerfCase globalPerf{"Test", cfg};  // Don't do this!

Performance Considerations

Measurement Overhead

Operation Overhead Impact
Wall-clock timing ~100-200ns Negligible for >1us workloads
CUDA event timing ~1-2us Negligible for >10us kernels
CSV write ~1ms Per test (not per sample)

Guideline: Overhead is negligible when workload >100x measurement cost.

Memory Usage

  • Each repeat stores one double (8 bytes)
  • Default 10 repeats = 80 bytes per test
  • GPU tests add transfer arrays and device memory
  • CSV export: ~200 bytes per row

Warmup Impact

Platform First Run After Warmup Notes
CPU 10-100us ~stable Caches, branch predictor
GPU 50-200ms ~stable JIT compilation, GPU wakeup

Auto-scaling warmup (when warmup=0):

  • Small workloads (<1000 cycles): 5 warmup iterations
  • Medium workloads (<10000 cycles): 3 warmup iterations
  • Large workloads (>=10000 cycles): 1 warmup iteration

Common Patterns

Basic Throughput Measurement

PERF_TEST(MyComponent, Throughput) {
  UB_PERF_GUARD(perf);

  std::vector<int> data(10000);
  perf.warmup([&] {
    for (int i = 0; i < perf.cycles(); ++i) {
      volatile int result = process(data);
      (void)result;
    }
  });

  volatile int sink = 0;
  auto result = perf.throughputLoop([&] {
    sink += process(data);
  }, "throughput");

  EXPECT_GT(result.callsPerSecond, 10000);
  EXPECT_STABLE_CV_CPU(result, perf.config());
}

A/B Comparison

PERF_TEST(MyComponent, CompareAlgorithms) {
  UB_PERF_GUARD(perf);

  // Test baseline
  volatile int sink = 0;
  auto baseline = perf.throughputLoop([&] {
    sink += algorithmA(data);
  }, "baseline");

  // Test optimized
  auto optimized = perf.throughputLoop([&] {
    sink += algorithmB(data);
  }, "optimized");

  double speedup = baseline.stats.median / optimized.stats.median;
  std::printf("Speedup: %.2fx\n", speedup);

  EXPECT_GT(speedup, 1.1) << "Optimization <10% improvement";
}

Memory Bandwidth Analysis

PERF_TEST(Memory, Bandwidth) {
  UB_PERF_GUARD(perf);

  const size_t SIZE = 1024 * 1024;
  std::vector<uint8_t> data(SIZE);

  ub::MemoryProfile memProfile{
    .bytesRead = SIZE,
    .bytesWritten = 0,
    .bytesAllocated = 0
  };

  volatile uint64_t sink = 0;
  auto result = perf.throughputLoop([&] {
    sink += processData(data.data(), SIZE);
  }, "bandwidth", memProfile);

  // Framework prints bandwidth automatically
  (void)sink;
}

Multi-threaded Scalability

PERF_TEST(SharedResource, Scalability) {
  UB_PERF_GUARD(perf);

  std::atomic<int64_t> counter{0};

  auto result = perf.contentionRun([&](int threadId) {
    for (int i = 0; i < perf.cycles(); ++i) {
      counter.fetch_add(1);
    }
  }, "contention");

  double efficiency = result.callsPerSecond /
                     (perf.threads() * expectedSingleThreadThroughput);

  std::printf("Parallel efficiency: %.1f%%\n", efficiency * 100);
}

See Also