Skip to content

Commit c5de988

Browse files
folly/Benchmark: bracket perf around the benchmark, not the whole run (#2682)
Summary: Pull Request resolved: #2682 ## Human comment: AI said that when it ran topdown, the extra noise from baseline set up and such was substantial. That doesn't seem to be true. But - the diff is still making things cleaner and neater I think. ## AI comment: The perf guard was constructed one scope too high. `runBenchmarksWithPrinter` built it immediately before calling `runBenchmarksWithPrinterImpl`, so the counting window covered everything that function does -- including the two global empty-loop baselines, flag validation, the banners, and per-row printing. `selectBenchmarksToRun` peels the baselines out before applying `--bm_regex`, so no flag could exclude them. They run under the full `--bm_max_secs`/`--bm_max_trials` budget: about 2 seconds and 9.8B instructions per run. On a memory-bound benchmark that was 68% of the counted instructions and 66% of the cycles, and it inverted the answer -- AMD topdown reported `frontend_bound` 32% and `backend_bound` 51.9%, because empty tight loops are dispatch-bound. The true figures are ~2% and ~71%. Cache-miss counters were mostly unaffected (0.2%), since empty loops never touch memory, which is why this went unnoticed. Move the guard into the per-benchmark loop, around the measurement dispatch only. The baselines are computed before the loop, so they fall outside for free, and printing stays outside too. Adaptive mode keeps a whole-run guard: it interleaves samples across benchmarks, so there is no contiguous per-benchmark region to bracket. Since perf attaches to the whole process, it can only describe one benchmark, so require the filters to select exactly one. `BENCHMARK_DRAW_TEXT` registers an entry that passes the name filters but is not a measurement, so it is excluded from the count and gets no window of its own. Two related fixes: - Usage errors were invisible. `LOG(ERROR)` emits nothing in these binaries -- the pre-existing `--bm_mode=bogus` path exits 1 silently too. Routed the fatals through one helper that writes to `cerr`. - A perf that cannot start now exits 1 with the reason instead of aborting. Combined with the preceding commit, `--bm_perf_args="stat -e nosuchevent"` goes from exit 134 to a clear message. `--bm_perf_args` without a narrowing `--bm_regex` now fails instead of silently profiling the whole run. Two callers do this today and will need updating: `xplat/superpack/apps/crunch/recon_agents.py` and `fbcode/kernel/fastio_kerneltest/provided_buffer_ring_bench_compare.py`. The former parses last-block-wins, so its numbers are already wrong. Reviewed By: yfeldblum Differential Revision: D116315663 fbshipit-source-id: 9df0bf6cb1d6f9f6e3b84c5f527bbdf17f3cec79
1 parent a4a9631 commit c5de988

2 files changed

Lines changed: 169 additions & 25 deletions

File tree

folly/Benchmark.cpp

Lines changed: 105 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <vector>
3030

3131
#include <folly/FileUtil.h>
32+
#include <folly/Function.h>
3233
#include <folly/MapUtil.h>
3334
#include <folly/Overload.h>
3435
#include <folly/String.h>
@@ -159,8 +160,8 @@ FOLLY_GFLAGS_DEFINE_bool(
159160
FOLLY_GFLAGS_DEFINE_string(
160161
bm_perf_args,
161162
"",
162-
"Attach `perf` during measurement (skips the first iteration "
163-
"for setup). Example: --bm_perf_args=\"record -g\"");
163+
"Attach `perf` to one benchmark's measurement, selected with --bm_regex. "
164+
"Example: --bm_perf_args=\"record -g\"");
164165
#endif
165166

166167
FOLLY_GFLAGS_DEFINE_bool(
@@ -505,6 +506,11 @@ namespace {
505506
constexpr std::string_view kUnitHeaders = "relative time/iter iters/s";
506507
constexpr std::string_view kUnitHeadersPadding = " ";
507508

509+
// BENCHMARK_DRAW_TEXT entries pass the name filters but are not measured.
510+
bool isPseudoBenchmark(const std::string& name) {
511+
return !name.empty() && name[0] == '"';
512+
}
513+
508514
std::string headerContents(std::string_view file, size_t columns) {
509515
const size_t maxFileNameChars =
510516
columns - kUnitHeaders.size() - kUnitHeadersPadding.size();
@@ -570,7 +576,7 @@ class BenchmarkResultsPrinter {
570576
separator('-');
571577
continue;
572578
}
573-
if (s[0] == '"') {
579+
if (isPseudoBenchmark(s)) {
574580
// Strips quote characters from the beginning and end of the name.
575581
line(s.substr(1, s.length() - 2));
576582
continue;
@@ -955,13 +961,70 @@ bool userSetGflag([[maybe_unused]] const char* name) {
955961
#endif
956962
}
957963

964+
// Report a user-facing error and exit without a stack trace.
965+
[[noreturn]] void fatalUsage(const std::string& msg) {
966+
std::cerr << detail::kANSIBoldRed << msg << detail::kANSIReset << std::endl;
967+
exit(1);
968+
}
969+
970+
void validatePerfUsage(const BenchmarksToRun& toRun) {
971+
#if FOLLY_PERF_IS_SUPPORTED
972+
const bool perfRequested = !FLAGS_bm_perf_args.empty();
973+
#else
974+
constexpr bool perfRequested = false;
975+
#endif
976+
if (!perfRequested) {
977+
return;
978+
}
979+
980+
if (FLAGS_bm_mode == "adaptive") {
981+
fatalUsage(
982+
"--bm_perf_args is not supported in --bm_mode=adaptive, which "
983+
"interleaves benchmark and baseline samples so that there is no "
984+
"region for perf to bracket. Use --bm_mode=best-of.");
985+
}
986+
987+
std::vector<std::string> selected;
988+
selected.reserve(toRun.benchmarks.size());
989+
for (const auto* bm : toRun.benchmarks) {
990+
if (!isPseudoBenchmark(bm->name)) {
991+
selected.push_back(bm->name);
992+
}
993+
}
994+
995+
if (selected.size() == 1) {
996+
return;
997+
}
998+
999+
if (selected.empty()) {
1000+
fatalUsage(
1001+
"--bm_perf_args is set, but the current filters select no benchmarks, "
1002+
"so perf would profile only the harness. --bm_list prints what is "
1003+
"selectable. Note that --bm_regex is a regex, so parentheses in "
1004+
"parameterized names such as 'gather(2MB)' have to be escaped.");
1005+
}
1006+
1007+
constexpr std::size_t kNamesToShow = 3;
1008+
const auto shownCount = std::min(kNamesToShow, selected.size());
1009+
auto shown = join(", ", selected.begin(), selected.begin() + shownCount);
1010+
if (selected.size() > shownCount) {
1011+
shown += fmt::format(", and {} more", selected.size() - shownCount);
1012+
}
1013+
1014+
fatalUsage(
1015+
fmt::format(
1016+
"--bm_perf_args profiles one benchmark at a time, but the current "
1017+
"filters select {} of them ({}). perf attaches to the whole process, "
1018+
"so the counters would be a single unattributable sum. Narrow the "
1019+
"selection with --bm_regex; --bm_list prints what the current "
1020+
"filters select.",
1021+
selected.size(),
1022+
shown));
1023+
}
1024+
9581025
// Check that no mode-incompatible flags were explicitly set.
9591026
void validateFlagCombinations() {
960-
// Log a user-facing error and exit without a stack trace.
961-
auto fatal = [](const std::string& msg) {
962-
LOG(ERROR) << detail::kANSIBoldRed << msg << detail::kANSIReset;
963-
exit(1);
964-
};
1027+
auto fatal = [](const std::string& msg) { fatalUsage(msg); };
9651028

9661029
if (FLAGS_bm_mode != "best-of" && FLAGS_bm_mode != "adaptive") {
9671030
fatal(
@@ -987,9 +1050,7 @@ void validateFlagCombinations() {
9871050
"(--bm_target_percentile).");
9881051
}
9891052
if (userSetGflag("bm_profile")) {
990-
fatal(
991-
"--bm_profile is not supported in adaptive mode. "
992-
"Use --bm_perf_args to attach perf in any mode.");
1053+
fatal("--bm_profile is not supported in adaptive mode.");
9931054
}
9941055
} else {
9951056
// Best-of mode
@@ -1038,7 +1099,8 @@ int64_t resolveSliceUsec() {
10381099
std::pair<std::set<std::string>, std::vector<detail::BenchmarkResult>>
10391100
runBenchmarksWithPrinterImpl(
10401101
BenchmarkResultsPrinter* FOLLY_NULLABLE printer,
1041-
const BenchmarksToRun& toRun) {
1102+
const BenchmarksToRun& toRun,
1103+
FunctionRef<detail::PerfScoped()> setUpPerf) {
10421104
vector<detail::BenchmarkResult> results;
10431105
results.reserve(toRun.benchmarks.size());
10441106

@@ -1110,13 +1172,19 @@ runBenchmarksWithPrinterImpl(
11101172
const detail::BenchmarkRegistration& bm = *toRun.benchmarks[i];
11111173
bool shouldDrawLineAfter = shouldDrawLineTracker();
11121174

1113-
if (FLAGS_bm_profile) {
1114-
elapsed = runProfilingGetNSPerIteration(bm.func, globalBaseline.first);
1115-
} else {
1116-
elapsed = FLAGS_bm_estimate_time
1117-
? runBenchmarkGetNSPerIterationEstimate(bm.func, globalBaseline.first)
1118-
: runBenchmarkGetNSPerIteration(
1119-
bm.func, globalBaseline.first, sliceUsec);
1175+
{
1176+
detail::PerfScoped perf =
1177+
isPseudoBenchmark(bm.name) ? detail::PerfScoped{} : setUpPerf();
1178+
1179+
if (FLAGS_bm_profile) {
1180+
elapsed = runProfilingGetNSPerIteration(bm.func, globalBaseline.first);
1181+
} else {
1182+
elapsed = FLAGS_bm_estimate_time
1183+
? runBenchmarkGetNSPerIterationEstimate(
1184+
bm.func, globalBaseline.first)
1185+
: runBenchmarkGetNSPerIteration(
1186+
bm.func, globalBaseline.first, sliceUsec);
1187+
}
11201188
}
11211189

11221190
// if customized user counters is used, it cannot print the result in real
@@ -1224,14 +1292,24 @@ PerfScoped BenchmarkingStateBase::doSetUpPerfScoped(
12241292
}
12251293

12261294
PerfScoped BenchmarkingStateBase::setUpPerfScoped() const {
1227-
std::vector<std::string> perfArgs;
12281295
#if FOLLY_PERF_IS_SUPPORTED
1296+
std::vector<std::string> perfArgs;
12291297
folly::split(' ', FLAGS_bm_perf_args, perfArgs, true);
1230-
#endif
12311298
if (perfArgs.empty()) {
12321299
return PerfScoped{};
12331300
}
1234-
return doSetUpPerfScoped(perfArgs);
1301+
try {
1302+
return doSetUpPerfScoped(perfArgs);
1303+
} catch (const std::exception& e) {
1304+
fatalUsage(
1305+
fmt::format(
1306+
"--bm_perf_args=\"{}\" could not be started: {}",
1307+
FLAGS_bm_perf_args,
1308+
e.what()));
1309+
}
1310+
#else
1311+
return PerfScoped{};
1312+
#endif
12351313
}
12361314

12371315
template <typename Printer>
@@ -1242,10 +1320,12 @@ BenchmarkingStateBase::runBenchmarksWithPrinter(Printer* printer) const {
12421320
}
12431321
std::lock_guard guard(mutex_);
12441322
BenchmarksToRun toRun = selectBenchmarksToRun(benchmarks_);
1323+
validatePerfUsage(toRun);
12451324
maybeRunWarmUpIteration(toRun);
12461325

1247-
detail::PerfScoped perf = setUpPerfScoped();
1248-
return runBenchmarksWithPrinterImpl(printer, toRun);
1326+
return runBenchmarksWithPrinterImpl(printer, toRun, [this] {
1327+
return setUpPerfScoped();
1328+
});
12491329
}
12501330

12511331
std::vector<BenchmarkResult> BenchmarkingStateBase::runBenchmarksWithResults()
@@ -1291,7 +1371,7 @@ void runBenchmarks() {
12911371

12921372
if (FLAGS_bm_list) {
12931373
auto bmNames = state.getBenchmarkList();
1294-
for (auto testName : bmNames) {
1374+
for (const auto& testName : bmNames) {
12951375
std::cout << testName << std::endl;
12961376
}
12971377
return;

folly/test/BenchmarkTest.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ TEST_F(BenchmarkingStateTest, PerfBasic) {
200200
int setUpPerfCalled = 0;
201201
std::vector<std::string> expectedArgs;
202202

203+
state.addBenchmark(__FILE__, "a", [&] {
204+
doBaseline();
205+
TestClock::advance(std::chrono::nanoseconds(1));
206+
return 1;
207+
});
208+
203209
state.perfSetup = [&](const std::vector<std::string>& args) {
204210
++setUpPerfCalled;
205211
EXPECT_EQ(expectedArgs, args);
@@ -219,9 +225,67 @@ TEST_F(BenchmarkingStateTest, PerfBasic) {
219225
setUpPerfCalled = 0;
220226
expectedArgs = {"stat", "-e", "cache-misses,cache-references"};
221227
(void)state.runBenchmarksWithResults();
228+
EXPECT_EQ(1, setUpPerfCalled);
222229
}
223230
}
224231

232+
TEST_F(BenchmarkingStateTest, PerfRejectsMultipleBenchmarks) {
233+
state.addBenchmark(__FILE__, "a", [&] {
234+
doBaseline();
235+
return 1;
236+
});
237+
state.addBenchmark(__FILE__, "b", [&] {
238+
doBaseline();
239+
return 1;
240+
});
241+
242+
folly::gflags::FlagSaver _;
243+
folly::gflags::SetCommandLineOption("bm_perf_args", "stat");
244+
245+
EXPECT_EXIT(
246+
(void)state.runBenchmarksWithResults(),
247+
::testing::ExitedWithCode(1),
248+
"profiles one benchmark at a time");
249+
}
250+
251+
TEST_F(BenchmarkingStateTest, PerfRejectsEmptySelection) {
252+
state.addBenchmark(__FILE__, "a", [&] {
253+
doBaseline();
254+
return 1;
255+
});
256+
257+
folly::gflags::FlagSaver _;
258+
folly::gflags::SetCommandLineOption("bm_perf_args", "stat");
259+
folly::gflags::SetCommandLineOption("bm_regex", "matches-nothing");
260+
261+
EXPECT_EXIT(
262+
(void)state.runBenchmarksWithResults(),
263+
::testing::ExitedWithCode(1),
264+
"select no benchmarks");
265+
}
266+
267+
TEST_F(BenchmarkingStateTest, PerfIgnoresTextEntries) {
268+
int setUpPerfCalled = 0;
269+
270+
state.addBenchmark(__FILE__, "\"some text\"", [&] { return 0; });
271+
state.addBenchmark(__FILE__, "a", [&] {
272+
doBaseline();
273+
TestClock::advance(std::chrono::nanoseconds(1));
274+
return 1;
275+
});
276+
277+
state.perfSetup = [&](const std::vector<std::string>&) {
278+
++setUpPerfCalled;
279+
return PerfScoped{};
280+
};
281+
282+
folly::gflags::FlagSaver _;
283+
folly::gflags::SetCommandLineOption("bm_perf_args", "stat");
284+
(void)state.runBenchmarksWithResults();
285+
286+
EXPECT_EQ(1, setUpPerfCalled);
287+
}
288+
225289
TEST_F(BenchmarkingStateTest, PerfSkipsAnIteration) {
226290
bool firstTimeSetUpDone = false;
227291
bool perfIsCalled = false;

0 commit comments

Comments
 (0)