foundationdb/fdbserver/bench/BenchMemoryTracker.cpp

168 lines
6.4 KiB
C++

/*
* BenchMemoryTracker.cpp
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2026 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Microbenchmarks for the per-call-site memory tracker (see
// design/memory-tracker.md, Testing Considerations -> Microbenchmarks).
//
// Two questions, both about "is it cheap enough to leave on?" (R0):
// * off-state cost — the always-compiled hooks with sampling disabled;
// * enabled-state cost — hooks at the envisioned production 1% rate
// and the pessimal every-allocation rate, which includes both the
// sampled-alloc slow path and the per-free lock+probe (the
// dominant enabled-state cost).
//
// Run: bin/fdbserver_bench --benchmark_filter=memtracker
//
// This bench lives under fdbserver/ (not flow/) and its CMake compiles
// fdbserver/GlobalNewDelete.cpp into the executable, so the real global
// operator new/delete override is active here and bench_memtracker_operator_new
// actually exercises the tracker. flow_bench links only flow (no override), so
// the operator-new path could not be measured there.
//
// FLOW_KNOBS points at the process-default (non-simulated) bootstrap knobs in
// fdbserver_bench, so MEMORY_TRACKING_SAMPLE_INVERSE starts at 0 (off); we drive
// it per benchmark via const_cast, exactly like the unit tests do.
#include "benchmark/benchmark.h"
#include "flow/Arena.h"
#include "flow/FastAlloc.h"
#include "flow/Knobs.h"
#include "flow/MemoryTracker.h"
#include <cstdlib>
namespace {
constexpr int kSize = 64;
// Set the sample-inverse knob (0=off, N=1-in-N) and clear tracker state so the
// run starts clean. Returns the previous inverse for restoration.
int setInverseAndReset(int inverse) {
auto* k = const_cast<FlowKnobs*>(FLOW_KNOBS);
int prev = k->MEMORY_TRACKING_SAMPLE_INVERSE;
k->MEMORY_TRACKING_SAMPLE_INVERSE = inverse;
memTrackerResetForTest();
return prev;
}
} // namespace
// Baseline: raw libc malloc/free. std::malloc is NOT hooked (we override
// operator new, not libc malloc), so this is the tracker-free reference the
// operator-new benchmark is compared against.
static void bench_memtracker_malloc_free(benchmark::State& state) {
for (auto _ : state) {
void* p = std::malloc(kSize);
benchmark::DoNotOptimize(p);
std::free(p);
}
state.SetItemsProcessed(state.iterations());
}
// End-to-end cost of a hooked allocation: global operator new[]/delete[] (which
// fire memTrackerOnAlloc/OnFree) at sample inverse Arg(0). 0 = off, 100 = prod
// 1%, 1 = every allocation. Compare Arg(0) against bench_memtracker_malloc_free
// for the disabled-hook cost, and Arg(100)/Arg(1) against Arg(0) for sampling.
static void bench_memtracker_operator_new(benchmark::State& state) {
int prev = setInverseAndReset(state.range(0));
for (auto _ : state) {
char* p = new char[kSize];
benchmark::DoNotOptimize(p);
delete[] p;
}
state.SetItemsProcessed(state.iterations());
setInverseAndReset(prev); // restore so later benchmarks aren't sampled
}
// Isolated tracker-hook cost: call memTrackerOnAlloc/OnFree directly on one
// preallocated buffer, with no real allocation in the loop, so only the
// tracker's own work is measured. At inverse>0 with live-tracking on, every
// OnFree still takes the global lock and probes the live table (the dominant
// enabled-state cost), while ~1/inverse of the OnAlloc calls take the sampling
// slow path (frame walk + table insert).
//
// This is doing a stack unwind against the same stack, and is going
// to hit the same hash table entries each iteration, so this is definitely
// a best-case estimate.
static void bench_memtracker_hooks(benchmark::State& state) {
int prev = setInverseAndReset(state.range(0));
void* p = std::malloc(kSize);
for (auto _ : state) {
memTrackerOnAlloc(p, kSize);
memTrackerOnFree(p);
}
state.SetItemsProcessed(state.iterations());
std::free(p);
setInverseAndReset(prev);
}
BENCHMARK(bench_memtracker_malloc_free);
BENCHMARK(bench_memtracker_operator_new)->Arg(0)->Arg(100)->Arg(1);
BENCHMARK(bench_memtracker_hooks)->Arg(0)->Arg(100)->Arg(1);
// --- Per-path unweighted overhead (for the FDB_MEMORY_TRACKER compile gate) ---
// Plain allocation loops with no knob manipulation: in a default build
// (FDB_MEMORY_TRACKER=1) they run with the tracker present but sampling off; in a
// FDB_MEMORY_TRACKER=0 build the tracker code is absent. Diffing the two builds
// gives each path's always-compiled off-state cost per op, unweighted by how
// often the path is actually taken at runtime.
// operator new[]/delete[] at size Arg(0): exercises GlobalNewDelete.cpp (our
// override when FDB_MEMORY_TRACKER, else libc++'s operator new).
static void bench_path_operator_new(benchmark::State& state) {
size_t n = state.range(0);
for (auto _ : state) {
char* p = new char[n];
benchmark::DoNotOptimize(p);
delete[] p;
}
state.SetItemsProcessed(state.iterations());
}
// FastAllocator<Size> allocate/release: exercises the flow/FastAlloc.cpp hook.
template <int Size>
static void bench_path_fastalloc(benchmark::State& state) {
for (auto _ : state) {
void* p = FastAllocator<Size>::allocate();
benchmark::DoNotOptimize(p);
FastAllocator<Size>::release(p);
}
state.SetItemsProcessed(state.iterations());
}
// Arena block create/destroy: exercises the flow/Arena.cpp hook. Arg(0) is the
// user allocation size, which selects the block class (medium vs huge).
static void bench_path_arena(benchmark::State& state) {
int n = state.range(0);
for (auto _ : state) {
Arena a;
uint8_t* p = new (a) uint8_t[n];
benchmark::DoNotOptimize(p);
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK(bench_path_operator_new)->Arg(64)->Arg(96)->Arg(256)->Arg(100000);
BENCHMARK_TEMPLATE(bench_path_fastalloc, 64);
BENCHMARK_TEMPLATE(bench_path_fastalloc, 96);
BENCHMARK_TEMPLATE(bench_path_fastalloc, 256);
BENCHMARK(bench_path_arena)->Arg(600)->Arg(100000);