Namespace: vernier::bench
Platform: Linux-only
C++ Standard: C++20 (C++23 used when available)
Complete guide to advanced features, patterns, and best practices for the benchmarking framework.
- Memory Profiling Deep Dive
- Parameterized Tests
- CSV Schema Reference
- Profiler Integration
- Quick Iteration Mode
- Custom Configuration
- GPU Advanced Topics
MemoryProfile tracks memory operations to calculate bandwidth and identify CPU-bound vs memory-bound code.
struct MemoryProfile {
size_t bytesRead; // Input data read from memory
size_t bytesWritten; // Output data written to memory
size_t bytesAllocated; // Heap allocations during operation
// Calculate total bandwidth (MB/s) -- read + write only (allocations excluded)
double bandwidthMBs(double durationUs) const {
const double totalBytes = bytesRead + bytesWritten;
const double durationS = durationUs / 1e6;
return (totalBytes / durationS) / 1e6; // MB/s
}
// Calculate efficiency vs theoretical peak
double efficiency(double durationUs, double peakMBs) const {
return (bandwidthMBs(durationUs) / peakMBs) * 100.0;
}
};bytesRead - Total bytes loaded from memory into CPU/cache:
// Reading array
bytesRead = sizeof(data);
// Reading struct members
bytesRead = sizeof(input.field1) + sizeof(input.field2);
// Reading via pointer dereference
bytesRead = input_size_in_bytes;bytesWritten - Total bytes stored from CPU/cache to memory:
// Writing array
bytesWritten = sizeof(output);
// Writing struct members
bytesWritten = sizeof(result.x) + sizeof(result.y);
// Modifying in-place
bytesWritten = bytesRead; // Read-modify-writebytesAllocated - Heap allocations during operation:
// No allocations (typical for performance tests)
bytesAllocated = 0;
// Testing allocation overhead
bytesAllocated = malloc_size * allocation_count;
// Vector resize
bytesAllocated = vec.capacity() * sizeof(T);| Scenario | bytesRead | bytesWritten | bytesAllocated |
|---|---|---|---|
| Read-only scan | Size of input | 0 | 0 |
| Write-only fill | 0 | Size of output | 0 |
| Transform (read->write) | Input size | Output size | 0 |
| In-place modify | Data size | Data size | 0 |
| With allocation | Input size | Output size | Alloc size |
Throughput: How much data your algorithm processes
// Process 1MB of payload data
throughput = 1 MB / time = 1000 MB/sBandwidth: Total memory system utilization
// But you read 1MB AND write 1MB
bandwidth = (1 MB read + 1 MB write) / time = 2000 MB/sKey insight: Bandwidth can be much higher than throughput!
PERF_TEST(Codec, Encode) {
UB_PERF_GUARD(perf);
ub::attachProfilerHooks(perf, ub::detail::getPerfConfig());
const size_t INPUT_SIZE = 1024; // 1KB input
const size_t OUTPUT_SIZE = 2048; // 2KB output (with framing overhead)
std::vector<uint8_t> input(INPUT_SIZE);
std::vector<uint8_t> output(OUTPUT_SIZE);
// Define memory profile
ub::MemoryProfile memProfile{
.bytesRead = INPUT_SIZE, // Read entire input
.bytesWritten = OUTPUT_SIZE, // Write entire output
.bytesAllocated = 0 // No heap allocations
};
perf.warmup([&] {
for (int i = 0; i < perf.cycles(); ++i) {
volatile int result = encode(input.data(), INPUT_SIZE,
output.data(), OUTPUT_SIZE);
(void)result;
}
});
volatile int sink = 0;
auto result = perf.throughputLoop([&] {
sink += encode(input.data(), INPUT_SIZE, output.data(), OUTPUT_SIZE);
}, "encode", memProfile);
// Output shows:
// - Throughput: 1000 MB/s of input processed
// - Bandwidth: 3000 MB/s total memory traffic (1MB read + 2MB write)
// - Efficiency: 25% of DDR4 peak (~12 GB/s)
(void)sink;
}Omit when:
- Quick development iteration
- Testing pure compute (no memory I/O)
- Bandwidth isn't relevant to your optimization
// Simple version - just get throughput
auto result = perf.throughputLoop([&] {
work();
}, "simple");
// CSV: wallMedian, callsPerSecond (same schema either way)Include when:
- Analyzing memory-bound code
- Need bandwidth in reports
- Identifying cache effects
- Production benchmarks
// Detailed version - with bandwidth analysis
ub::MemoryProfile mp{INPUT_SIZE, OUTPUT_SIZE, 0};
auto result = perf.throughputLoop([&] {
work();
}, "detailed", mp);
// CSV schema is unchanged; bandwidth from the MemoryProfile is printed to the
// console, not written as extra CSV columns.Performance impact: None - it's just metadata for the console bandwidth report.
There is no PERF_TEST_P macro! For parameterized tests, use standard GoogleTest TEST_P.
This will NOT compile:
PERF_TEST_P(MyTest, Case) { // ERROR: PERF_TEST_P doesn't exist!
const int size = GetParam();
// ...
}Correct approach:
class MyTest : public ::testing::TestWithParam<int> {
protected:
const ub::PerfConfig& getCfg() {
return ub::detail::getPerfConfig();
}
};
TEST_P(MyTest, Case) { // Use standard GoogleTest TEST_P
const int size = GetParam();
ub::PerfConfig cfg = getCfg();
cfg.msgBytes = size;
std::string testName = ::testing::UnitTest::GetInstance()
->current_test_info()->test_suite_name() +
std::string(".") +
::testing::UnitTest::GetInstance()
->current_test_info()->name();
ub::PerfCase perf{testName, cfg};
ub::attachProfilerHooks(perf, cfg);
// Test implementation...
}
INSTANTIATE_TEST_SUITE_P(
Params,
MyTest,
::testing::Values(64, 256, 1024)
);For simple tests without parameters:
PERF_TEST(MyComponent, Basic) {
UB_PERF_GUARD(perf); // Gets "MyComponent.Basic" automatically
ub::attachProfilerHooks(perf, ub::detail::getPerfConfig());
// Test code...
}For parameter sweeps, manually construct PerfCase with custom config:
PERF_TEST(MyComponent, PayloadSweep) {
for (int payloadSize : {64, 256, 1024, 4096}) {
// Get default config
ub::PerfConfig cfg = ub::detail::getPerfConfig();
// Override specific field
cfg.msgBytes = payloadSize;
// Create custom PerfCase (can't use UB_PERF_GUARD here!)
std::string testName = "MyComponent.PayloadSweep/" + std::to_string(payloadSize);
ub::PerfCase perf{testName, cfg};
ub::attachProfilerHooks(perf, cfg);
// Setup test data
std::vector<uint8_t> data(payloadSize);
// Run test
perf.warmup([&] {
for (int i = 0; i < perf.cycles(); ++i) {
volatile auto result = process(data.data(), payloadSize);
(void)result;
}
});
volatile uint64_t sink = 0;
auto result = perf.throughputLoop([&] {
sink += process(data.data(), payloadSize);
}, "throughput");
// Each iteration gets separate CSV row with different msgBytes
(void)sink;
}
}UB_PERF_GUARD(perf) limitations:
- Gets test name from GoogleTest automatically
- Uses global config (can't override per-parameter)
- All parameter values would have same
msgBytesin CSV
Manual PerfCase constructor:
- Set custom test name with parameter value
- Override config fields per parameter
- Each parameter gets unique CSV row
For more complex parameter combinations, use standard GoogleTest fixtures:
class PayloadTest : public ::testing::TestWithParam<int> {
protected:
const ub::PerfConfig& getCfg() {
return ub::detail::getPerfConfig();
}
};
TEST_P(PayloadTest, Encode) { // Use TEST_P, not PERF_TEST_P
const int payloadSize = GetParam();
// Custom config with parameter
ub::PerfConfig cfg = ub::detail::getPerfConfig();
cfg.msgBytes = payloadSize;
// Custom name with parameter
std::string testName = ::testing::UnitTest::GetInstance()
->current_test_info()->test_suite_name() +
std::string(".") +
::testing::UnitTest::GetInstance()
->current_test_info()->name();
ub::PerfCase perf{testName, cfg};
ub::attachProfilerHooks(perf, cfg);
// Test implementation...
}
INSTANTIATE_TEST_SUITE_P(
PayloadSizes,
PayloadTest,
::testing::Values(64, 256, 1024, 4096)
);CSV output:
test,msgBytes,wallMedian,callsPerSecond
PayloadTest/PayloadSizes.Encode/64,64,0.123,8130000
PayloadTest/PayloadSizes.Encode/256,256,0.456,2192000
PayloadTest/PayloadSizes.Encode/1024,1024,1.234,810000
PayloadTest/PayloadSizes.Encode/4096,4096,4.567,219000
- Use
UB_PERF_GUARDfor simple tests with no parameters - Use manual constructor for parameter sweeps
- Always set
cfg.msgBytesto actual payload size - Include parameter in test name for CSV clarity
- Call
attachProfilerHookswith your custom config
All CPU performance tests produce CSV with these columns:
| Column | Type | Description | Example |
|---|---|---|---|
test |
string | Test name (Suite.Name) | "MyComponent.Throughput" |
cycles |
int | Operations per repeat | 10000 |
repeats |
int | Samples collected | 10 |
warmup |
int | Warmup iterations | 1000 |
threads |
int | Thread count | 1 |
msgBytes |
int | Payload size (bytes) | 1024 |
console |
bool | Console output enabled | true |
nonBlocking |
bool | Non-blocking I/O mode | false |
minLevel |
string | Minimum log level | "INFO" |
wallMedian |
double | Median per-call time | 0.543 |
wallP10 |
double | 10th percentile | 0.512 |
wallP90 |
double | 90th percentile | 0.587 |
wallMin |
double | Minimum time | 0.498 |
wallMax |
double | Maximum time | 0.623 |
wallMean |
double | Mean time | 0.546 |
wallStddev |
double | Standard deviation | 0.023 |
wallCV |
double | Coefficient of variation | 0.042 |
callsPerSecond |
double | Throughput | 1843317.0 |
stable |
bool | CV below adaptive threshold | 1 |
cvThreshold |
double | Adaptive CV threshold used | 0.05 |
| Column | Type | Description | Example |
|---|---|---|---|
profileTool |
string | Profiler used | "perf" |
profileDir |
string | Artifact directory | "./perf-data" |
timestamp |
string | ISO 8601 timestamp | "2024-11-02T10:30:45Z" |
gitHash |
string | Git commit (short) | "a3f9c2b" |
hostname |
string | Machine name | "perf-test-01" |
platform |
string | Architecture | "x86_64" |
Note: Memory bandwidth analysis from MemoryProfile is printed to the console
during test execution but is not exported to CSV columns. The CSV captures timing
and throughput metrics. For bandwidth data, parse the console output or add custom
columns via the PerfRegistry.
See GPU_GUIDE.md for complete GPU column schema.
Plot scaling curves:
bench-plot plot results.csv --output plots/Compare implementations:
bench compare baseline.csv optimized.csvStatistical analysis:
bench compare baseline.csv candidate.csv --threshold 5Filter by test:
grep "MyComponent.Encode" results.csv > encode_only.csvvoid attachProfilerHooks(PerfCase& perf, const PerfConfig& cfg);Purpose:
Enables command-line profilers for this test case. Must be called to activate --profile flag.
When to call:
- Right after
UB_PERF_GUARD(perf)in every test - Right after manual
PerfCaseconstructor for parameterized tests
What it does:
- Checks if user passed
--profileflag - Creates appropriate profiler backend (perf, gperf, etc.)
- Attaches before/after hooks to PerfCase
- Enables automatic data collection
- Adds profiler columns to CSV output
What happens if omitted:
- Test runs normally
--profileflag is silently ignored for this test- No profiler data collected
- Missing profiler columns in CSV
Example:
PERF_TEST(MyComponent, Throughput) {
UB_PERF_GUARD(perf);
ub::attachProfilerHooks(perf, ub::detail::getPerfConfig()); // Required!
// If user runs: ./test --profile perf
// This test will collect perf data
// Without attachProfilerHooks(), --profile is silently ignored
}Best practice: Always call it - zero overhead if --profile not used.
Every backend below self-registers via the profiler registry; bench doctor
(or --profile-check) walks the list and reports each one's environment
readiness with the exact remediation hint.
| Profiler | Layer | Purpose | Requirements | Overhead |
|---|---|---|---|---|
perf |
CPU | Hardware counters (stat/record/mem/c2c) |
Linux, kernel.perf_event_paranoid<=1 |
~5% |
gperf |
CPU | gperftools sampling (CPU + optional heap) | libgperftools | ~10% |
callgrind |
CPU | Deterministic instruction counts | valgrind | ~20-50x |
bpftrace |
CPU | Kernel tracing (fsync / write latency, etc.) | Linux BPF + root / CAP_BPF | <1% |
rapl |
CPU | Package energy consumption | Intel CPU + MSR access | <1% |
massif |
CPU | Heap usage timeline | valgrind | ~20x |
memcheck |
CPU | Memory errors and leaks | valgrind | ~20x |
helgrind |
CPU | Data races and lock-order violations (DRD opt.) | valgrind | ~20x |
offcpu |
CPU | Off-CPU stack profiling | bpftrace + root + tracefs | low |
heaptrack |
CPU | Lower-overhead heap profiler | heaptrack on PATH | ~1.5x |
jemalloc |
CPU | jemalloc prof sampling | libjemalloc on PATH (LD_PRELOAD) | ~5-10% |
nsight |
GPU | Nsight Systems / Compute (auto stats) | CUDA toolkit + nsys/ncu | ~2x |
compute-sanitizer |
GPU | GPU memcheck / racecheck / synccheck / initcheck | CUDA toolkit | ~5-10x |
rocprof |
GPU | AMD ROCm GPU profiler | ROCm + rocprof | ~2x |
CUPTI activity counters (per-launch register count, static + dynamic shared
memory, kernel count) populate the GPU CSV section on every PERF_GPU_*
test without requiring --profile -- spawning external ncu is unnecessary
for those metrics.
Run bench doctor <ptest-binary> (or <ptest-binary> --profile-check) to
see each backend's status on the current host.
# Enable perf (requires root, one-time setup)
sudo sysctl -w kernel.perf_event_paranoid=-1
# Run test with perf profiling
./MyComponent_PTEST --profile perf --csv results.csv
# Analyze results
perf report -i MyComponent.Throughput.perf/perf.data
perf annotate -i MyComponent.Throughput.perf/perf.dataArtifacts: perf writes its capture to perf-<Test>-<timestamp>.data; the
profileTool and profileDir CSV columns record that a profile was taken and
where it landed. Counter detail lives in the perf report, not in extra CSV
columns.
# Run test with RAPL (requires root)
sudo ./MyComponent_PTEST --profile rapl --csv results.csv
# Energy summary is written to <Test>.rapl/energy.txt
cat MyComponent.Throughput.rapl/energy.txtEnergy report fields (in <Test>.rapl/energy.txt):
Energy consumed- Total energy consumed (J)Average power- Average power (W)Energy per operation- Energy per operation (mJ/call)Duration- Measured wall-clock duration (s)
See API_REFERENCE.md for Profiler base class interface to implement custom profilers.
The --quick flag reduces test duration for fast feedback during development:
./MyComponent_PTEST --quick --csv results.csvDefault values:
// Normal mode (production)
cycles = 10000;
repeats = 10;
warmup = 1;
// Quick mode (if not explicitly overridden)
cycles = 5000; // 2x fewer operations
repeats = 5; // 2x fewer samples
warmup = 2; // Extra warmup to stabilizePerformance impact:
- 30-100x faster than full run
- Less statistical confidence (higher CV%)
- May miss outliers/edge cases
- Same CSV format (compatible with analysis tools)
GOOD: Use --quick for:
- Active development (tight iteration loop)
- Debugging test failures
- Sanity checking builds
- Rapid experimentation
- Local development
BAD: Don't use --quick for:
- Production benchmarks
- CI/CD regression testing
- Publishing results
- Comparing implementations
- Performance validation
# Development (fast feedback)
make
./test --quick --csv dev.csv
# Make changes...
make
./test --quick --csv dev2.csv
bench compare dev.csv dev2.csv
# Validation (more samples)
./test --cycles 50000 --repeats 20 --csv validate.csv
# Production (full characterization)
./test --cycles 100000 --repeats 30 --csv production.csvQuick mode may show higher coefficient of variation:
| Mode | Typical CV% | Acceptable? |
|---|---|---|
| Production | <5% | Good |
| Normal | <10% | Good |
| Quick | <15% | Acceptable for dev |
Use EXPECT_STABLE_CV_CPU - it automatically relaxes thresholds in quick mode.
PERF_MAIN() // Replaces int main()Purpose: Provides complete main() function with:
- GoogleTest initialization
- Command-line parsing (
--csv,--profile,--quick, etc.) - PerfConfig singleton setup
- CSV export after all tests
- Profiler lifecycle management
- Proper exit codes for CI/CD
Usage: Place at end of test file:
#include <gtest/gtest.h>
#include "Perf.hpp"
namespace ub = vernier::bench;
PERF_TEST(MyComponent, Test1) {
// ...
}
PERF_TEST(MyComponent, Test2) {
// ...
}
PERF_MAIN() // That's it - no custom main() needed!What it expands to:
int main(int argc, char** argv) {
// 1. Parse performance flags
auto& cfg = vernier::bench::detail::perfConfigSingleton();
vernier::bench::parsePerfFlags(cfg, &argc, argv);
// 2. Register global config for CSV export
vernier::bench::setGlobalPerfConfig(&cfg);
// 3. Install CSV listener
vernier::bench::installPerfEventListener(cfg);
// 4. Initialize GoogleTest
::testing::InitGoogleTest(&argc, argv);
// 5. Run all tests
return RUN_ALL_TESTS();
}When NOT to use:
- Custom main() logic needed
- Embedding tests in larger application
- Multiple test binaries with shared setup
In those cases, manually implement the steps above.
// Get read-only config in tests
const ub::PerfConfig& cfg = ub::detail::getPerfConfig();
// Check flags
if (cfg.quickMode) {
// Adjust test behavior
}
// Use values
std::printf("Running with %d cycles\n", cfg.cycles);Core flags:
--cycles N # Operations per repeat (default: 10000)
--repeats N # Samples collected (default: 10)
--warmup N # Warmup iterations (default: 1, 0=auto)
--threads N # Worker threads (default: 1)
--msg-bytes N # Payload size (default: 64)
--quick # Fast mode (reduced cycles/repeats)Output flags:
--csv PATH # CSV output file
--console # Echo to console (default: false)
--nonblocking # Non-blocking mode
--min-level STR # Minimum log level (default: INFO)Profiling flags:
--profile TOOL # Profiler: perf|gperf|bpftrace|rapl|callgrind
--profile-args ARGS # Profiler-specific arguments
--artifact-root DIR # Output directory (default: .)
--profile-frequency N # Sampling Hz for CPU profilers (default: 10000)
--profile-analyze # Auto-run analysis after profiling
--bpf LIST # BPF script names/paths (comma-separated): fsync_latency,write_latencyGPU flags:
--gpu-device N # CUDA device ID (default: 0)
--gpu-warmup N # GPU warmup iterations (default: 10)
--gpu-memory MODE # Memory strategy: explicit|unified|pinned|mapped
--min-speedup F # Minimum expected speedup vs CPUExample combinations:
# Development
./test --quick --csv dev.csv
# Production CI
./test --cycles 100000 --repeats 30 --csv ci_results.csv
# With profiling
./test --profile perf --gtest_filter="*Encode*"
# Multi-threaded
./test --threads 8 --csv mt_results.csv
# GPU test
./test --gpu-device 0 --gpu-memory unified --csv gpu_results.csvGPU tests follow the same pattern as CPU but use UB_PERF_GPU_GUARD:
class GpuPayloadTest : public ::testing::TestWithParam<int> {
protected:
const ub::PerfConfig& getCfg() {
return ub::detail::getPerfConfig();
}
};
TEST_P(GpuPayloadTest, Kernel) {
const int arraySize = GetParam();
ub::PerfConfig cfg = getCfg();
cfg.msgBytes = arraySize * sizeof(float);
std::string testName = ::testing::UnitTest::GetInstance()
->current_test_info()->test_suite_name() +
std::string(".") +
::testing::UnitTest::GetInstance()
->current_test_info()->name();
// Note: PerfGpuCase uses PerfConfig, not custom config override
ub::PerfGpuCase perf{testName, ub::detail::getPerfConfig()};
// Allocate arrays
std::vector<float> h_data(arraySize);
float* d_data;
cudaMalloc(&d_data, arraySize * sizeof(float));
// Warmup
perf.cudaWarmup([&](cudaStream_t s) {
myKernel<<<grid, block, 0, s>>>(d_data, arraySize);
});
// Measure
auto result = perf.cudaKernel([&](cudaStream_t s) {
myKernel<<<grid, block, 0, s>>>(d_data, arraySize);
}, "kernel")
.withLaunchConfig(grid, block)
.withHostToDevice(h_data.data(), d_data, arraySize * sizeof(float))
.withDeviceToHost(d_data, h_data.data(), arraySize * sizeof(float))
.measure();
cudaFree(d_data);
}
INSTANTIATE_TEST_SUITE_P(
ArraySizes,
GpuPayloadTest,
::testing::Values(1024, 4096, 16384, 65536)
);Control memory management strategy via command-line:
# Explicit malloc/memcpy (default)
./test --gpu-memory explicit --csv results.csv
# Unified memory (automatic migration)
./test --gpu-memory unified --csv results.csv
# Pinned host memory (faster transfers)
./test --gpu-memory pinned --csv results.csv
# Mapped memory (zero-copy)
./test --gpu-memory mapped --csv results.csvStrategy comparison:
| Strategy | Transfer Speed | Use When |
|---|---|---|
explicit |
Fast (PCIe bandwidth) | Default, full control |
unified |
Variable (page faults) | Large datasets, ease of use |
pinned |
Fastest | Frequent H2D/D2H transfers |
mapped |
Slowest (no copy) | Infrequent access, small data |
Test scaling across multiple GPUs:
class MultiGpuTest : public ::testing::TestWithParam<int> {};
TEST_P(MultiGpuTest, Scaling) {
const int deviceCount = GetParam();
UB_PERF_GPU_GUARD(perf);
// Allocate per-device data
std::vector<float*> d_data(deviceCount);
for (int i = 0; i < deviceCount; ++i) {
cudaSetDevice(i);
cudaMalloc(&d_data[i], SIZE * sizeof(float));
}
// Measure multi-GPU performance
auto result = perf.cudaKernelMultiGpu(deviceCount,
[&](int dev, cudaStream_t stream) {
myKernel<<<grid, block, 0, stream>>>(d_data[dev], SIZE);
}, "multi-gpu")
.withLaunchConfig(grid, block)
.withP2PAccess()
.measure();
// Check scaling efficiency
EXPECT_GT(result.aggregatedStats.multiGpu->scalingEfficiency, 0.8)
<< "Poor scaling with " << deviceCount << " GPUs";
// Cleanup
for (int i = 0; i < deviceCount; ++i) {
cudaSetDevice(i);
cudaFree(d_data[i]);
}
}
INSTANTIATE_TEST_SUITE_P(
DeviceCounts,
MultiGpuTest,
::testing::Values(1, 2, 4, 8)
);Complete GPU-specific CSV columns:
| Column | Type | Description | Example |
|---|---|---|---|
gpuModel |
string | GPU device name | "NVIDIA A100-SXM4-40GB" |
computeCapability |
string | CUDA compute capability | "8.0" |
kernelTimeUs |
double | GPU kernel execution time | 123.45 |
transferTimeUs |
double | H2D + D2H transfer time | 45.67 |
h2dBytes |
int64 | Host-to-device bytes | 4194304 |
d2hBytes |
int64 | Device-to-host bytes | 4194304 |
speedupVsCpu |
double | GPU vs CPU speedup | 15.3 |
memBandwidthGBs |
double | Memory bandwidth | 850.2 |
occupancy |
double | Kernel occupancy [0-1] | 0.82 |
smClockMHz |
double | SM clock frequency | 1410.0 |
throttling |
bool | Thermal throttling detected | false |
| Column | Type | Description | Example |
|---|---|---|---|
powerDrawW |
double | Average GPU package power (watts) | 19.79 |
powerLimitW |
double | Configured power-cap limit (watts) | 80.0 |
temperatureC |
int | Peak GPU temperature during the run (C) | 49 |
temperatureDeltaC |
int | Temperature delta from start to end (C) | 2 |
| Column | Type | Description | Example |
|---|---|---|---|
cuptiKernelLaunches |
int | Kernel launches observed in this measure() window | 25000 |
cuptiRegistersMedian |
int | Median registers per thread across launches | 16 |
cuptiRegistersMax |
int | Max registers per thread observed | 16 |
cuptiStaticSmemBytes |
int | Median static __shared__ allocation per launch |
0 |
cuptiDynamicSmemBytes |
int | Median dynamic shared memory passed at launch | 0 |
CUPTI columns populate automatically on every GPU run when libcupti is
linked at build time; no --profile flag required. See
demo/docs/19_CUPTI_KERNEL_METRICS.md.
| Column | Type | Description | Present When |
|---|---|---|---|
deviceId |
int | Primary device ID | Multi-GPU tests |
deviceCount |
int | Number of GPUs used | Multi-GPU tests |
multiGpuEfficiency |
double | Parallel efficiency [0-1] | Multi-GPU tests |
p2pBandwidthGBs |
double | Peer-to-peer bandwidth | P2P enabled |
| Column | Type | Description | Present When |
|---|---|---|---|
umPageFaults |
int64 | GPU page faults | Unified memory |
umH2DMigrations |
int64 | Host->device migrations | Unified memory |
umD2HMigrations |
int64 | Device->host migrations | Unified memory |
umMigrationTimeUs |
double | Total migration overhead | Unified memory |
umThrashing |
bool | Memory thrashing detected | Unified memory |
GPU-specific profilers:
| Profiler | Purpose | Requirements | Output |
|---|---|---|---|
nsight |
Nsight Systems timeline / Compute kernel detail | CUDA toolkit + nsys/ncu | profile.nsys-rep (default), kernel_replay.ncu-rep (--profile-args replay) |
compute-sanitizer |
GPU memcheck / racecheck / synccheck / initcheck | CUDA toolkit | sanitizer.log |
rocprof |
AMD ROCm GPU profiler | ROCm + rocprof | results.{csv,json} |
Using Nsight:
# Profile specific kernel (default Systems mode)
./test --profile nsight --gtest_filter="*MyKernel"
# Generates: MyKernel.MyKernel.nsight/profile.nsys-rep
# Kernel deep-dive (Compute replay)
./test --profile nsight --profile-args replay --gtest_filter="*MyKernel"
# Generates: MyKernel.MyKernel.nsight/kernel_replay.ncu-rep
# Analyze with Nsight UI
ncu-ui MyKernel.MyKernel.nsight/kernel_replay.ncu-rep-
Always warmup GPU kernels - First launch includes JIT compilation
perf.cudaWarmup([&](cudaStream_t s) { myKernel<<<grid, block, 0, s>>>(d_data); }); -
Use appropriate launch configuration - Check occupancy
auto result = perf.cudaKernel([&](cudaStream_t s) { myKernel<<<grid, block, sharedMemBytes, s>>>(d_data); }, "kernel") .withLaunchConfig(grid, block, sharedMemBytes) .measure(); EXPECT_GT(result.stats.occupancy.achievedOccupancy, 0.5) << "Low occupancy - increase threads or reduce resources";
-
Profile with multiple block sizes - Find optimal configuration
for (int blockSize : {128, 256, 512, 1024}) { dim3 block(blockSize); dim3 grid((N + blockSize - 1) / blockSize); auto result = perf.cudaKernel([&](cudaStream_t s) { myKernel<<<grid, block, 0, s>>>(d_data, N); }, "kernel") .withLaunchConfig(grid, block) .measure(); }
-
Measure transfer overhead separately - Identify bottlenecks
// Just transfers (no kernel) auto transfer = perf.cudaKernel([&](cudaStream_t) {}, "transfer-only") .withHostToDevice(h_in, d_in, SIZE) .withDeviceToHost(d_out, h_out, SIZE) .measure(); // Full pipeline auto full = perf.cudaKernel([&](cudaStream_t s) { myKernel<<<grid, block, 0, s>>>(d_in, d_out, N); }, "full") .withHostToDevice(h_in, d_in, SIZE) .withDeviceToHost(d_out, h_out, SIZE) .measure(); double kernelOnlyUs = full.kernelTimeUs - transfer.transferTimeUs;
- Always call
attachProfilerHooks()afterUB_PERF_GUARDorPerfCaseconstructor - Use
MemoryProfilefor memory-bound code analysis - Set
cfg.msgBytesto actual payload size in parameterized tests - Use
--quickduring development, full config for production - Check CV% with
EXPECT_STABLE_CV_CPUfor result stability
- Always warmup GPU kernels - First launch includes JIT compilation
- Check occupancy - Target >50% for compute-bound kernels
- Profile multiple block sizes - Find optimal configuration
- Measure transfers separately - Identify bottlenecks
- Use appropriate memory strategy - Test explicit vs unified vs pinned
- Call
PERF_MAIN()to handle all boilerplate - Check CSV schema to understand available metrics
- Use TEST_P for parameterized tests - PERF_TEST_P doesn't exist!
- API Reference - Complete API documentation
- CPU Guide - CPU benchmarking guide
- GPU Guide - GPU benchmarking guide
- Demos - Interactive demos with step-by-step walkthroughs
- Troubleshooting - Common issues