Skip to content

Commit 04ce212

Browse files
committed
Add synthetic benchmark corpus + cold/warm cache numbers (M12)
1 parent 44aa9e2 commit 04ce212

7 files changed

Lines changed: 194 additions & 4 deletions

File tree

README.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,10 +266,40 @@ project on a memory-constrained machine. All job counts produced
266266
identical findings (verified via `diff`), including on the demo project
267267
run directly on this VM.
268268

269-
This is still closer to "two honest data points" than a real benchmark
270-
suite -- see [`docs/spec.md`](docs/spec.md) §14 for what a full suite
271-
(synthetic corpus, cold/warm, scaling *and* memory as first-class
272-
dimensions, regression gating) would still need to add.
269+
### Synthetic corpus: cold vs. warm cache
270+
271+
The spdlog and scaling numbers above are real projects, but both are
272+
external dependencies -- reproducing them means cloning something else
273+
first. [`benchmarks/synthetic-corpus/`](benchmarks/synthetic-corpus/) is
274+
a self-contained alternative generated entirely from templates at CMake
275+
configure time: 30 independent unit pairs (60 TUs), each reproducing the
276+
same seven-rule bug pattern as `tests/projects/multi-rule`, for an exact,
277+
predictable 210 total findings (30 per rule) -- a built-in correctness
278+
check on the run, not just a timing number.
279+
280+
Measured on the same 8-core Apple M3, `--jobs 1` (isolating the cache
281+
effect from parallelism), average of 3 runs each, fresh `--cache-dir` per
282+
cold run vs. one warm cache reused across all three warm runs:
283+
284+
| | Wall clock | Peak RSS |
285+
|---|---|---|
286+
| Cold (no cache) | 12.68 s | ~171 MiB |
287+
| Warm (full cache hit, 60/60 TUs) | 0.87 s | ~34 MiB |
288+
289+
~14.6x speedup. All six runs (3 cold + 3 warm) produced byte-for-byte
290+
identical findings (verified via `diff`), and every run reported exactly
291+
210 findings, confirming the corpus's own self-check. Cold-run peak RSS
292+
varied more than expected across repetitions (128-213 MiB) -- likely
293+
ordinary background load on a shared dev laptop rather than anything
294+
corpus-specific, included here rather than smoothed over.
295+
296+
This is now three real, reproducible data points (one external project,
297+
one scaling study, one fully self-contained corpus) rather than "two
298+
honest data points" -- what's still missing from a complete benchmark
299+
suite is CI-enforced regression gating (fail a run if wall-clock or peak
300+
RSS regresses past some threshold), which folds naturally into the CI
301+
matrix work in [`docs/spec.md`](docs/spec.md) §14's next item rather than
302+
being its own benchmark-only milestone.
273303

274304
## Known limitations
275305

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
cmake_minimum_required(VERSION 3.20)
2+
project(cpp-sentinel-synthetic-corpus LANGUAGES CXX)
3+
4+
set(CMAKE_CXX_STANDARD 20)
5+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
6+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
7+
8+
# Number of independent unit pairs (2 TUs each) to generate. Every unit
9+
# reproduces the same seven-rule bug pattern as tests/projects/multi-rule
10+
# (mechanically duplicated via configure_file(), not committed N times),
11+
# so the corpus's total finding count is always exactly
12+
# SENTINEL_BENCHMARK_CORPUS_SIZE * 7 -- a built-in sanity check for any
13+
# benchmark number derived from this corpus.
14+
set(SENTINEL_BENCHMARK_CORPUS_SIZE 30 CACHE STRING
15+
"Number of unit pairs (2 TUs each) to generate for the synthetic corpus")
16+
17+
set(GENERATED_SOURCES "")
18+
foreach(UNIT RANGE 1 ${SENTINEL_BENCHMARK_CORPUS_SIZE})
19+
configure_file(templates/geometry.h.in generated/geometry${UNIT}.h @ONLY)
20+
configure_file(templates/shape.cpp.in generated/shape${UNIT}.cpp @ONLY)
21+
configure_file(templates/logger.cpp.in generated/logger${UNIT}.cpp @ONLY)
22+
list(APPEND GENERATED_SOURCES
23+
${CMAKE_CURRENT_BINARY_DIR}/generated/shape${UNIT}.cpp
24+
${CMAKE_CURRENT_BINARY_DIR}/generated/logger${UNIT}.cpp)
25+
endforeach()
26+
27+
# OBJECT, not an executable: units are never linked together -- nothing
28+
# here needs a working binary, only a valid compile_commands.json entry
29+
# per TU -- so there's no need to give each unit's globals unique names
30+
# beyond what's already needed to avoid within-pair redefinition.
31+
add_library(synthetic-corpus OBJECT ${GENERATED_SOURCES})
32+
target_include_directories(synthetic-corpus PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/generated)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Synthetic benchmark corpus
2+
3+
The [spdlog benchmark](../../README.md#benchmark) is a real project, but
4+
it's an external dependency at a pinned commit -- reproducing it means
5+
cloning spdlog. This corpus is the alternative: a deliberately-sized,
6+
fully self-contained C++ project generated entirely from templates in
7+
this directory, so `--jobs`/`--cache-dir` benchmarks are reproducible
8+
from a clean clone of this repo alone, with a known, exact finding count
9+
to sanity-check the run against.
10+
11+
## Design
12+
13+
Every unit is an independently-compiled pair of translation units
14+
reproducing the same seven-rule bug pattern as
15+
[`tests/projects/multi-rule`](../../tests/projects/multi-rule) (missing
16+
override, unchecked null dereference, redundant move, use-after-move, an
17+
unbalanced lock, an expensive by-value copy, and a lock-order-inversion
18+
cycle across the pair). `SENTINEL_BENCHMARK_CORPUS_SIZE` (default 30)
19+
units are generated by `configure_file()` at CMake-configure time from
20+
the three templates in `templates/`, substituting `@UNIT@` for a unique
21+
integer per unit -- nothing beyond the templates themselves is committed.
22+
Units never link against each other (each is its own `OBJECT` library
23+
source, never an executable), so there's no cross-unit ODR concern and no
24+
reason to give each unit's globals more unique naming than "don't collide
25+
within the same pair."
26+
27+
This gives an exact, predictable finding count: `SENTINEL_BENCHMARK_CORPUS_SIZE * 7`
28+
findings total, 30 for each of the seven rules at the default size --
29+
useful as a built-in correctness check on any benchmark run over this
30+
corpus, not just a timing number.
31+
32+
## Usage
33+
34+
```bash
35+
cmake -S benchmarks/synthetic-corpus -B benchmarks/synthetic-corpus/build \
36+
-DSENTINEL_BENCHMARK_CORPUS_SIZE=30
37+
cmake --build benchmarks/synthetic-corpus/build -j"$(sysctl -n hw.ncpu)"
38+
39+
./build/tools/cpp-sentinel/cpp-sentinel analyze \
40+
--compile-commands benchmarks/synthetic-corpus/build/compile_commands.json \
41+
--header-filter '.*'
42+
```
43+
44+
See the top-level [README's Benchmark section](../../README.md#benchmark)
45+
for the cold/warm cache numbers measured against this corpus.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#pragma once
2+
#include <mutex>
3+
#include <string>
4+
5+
// Unit @UNIT@ of the synthetic benchmark corpus (see
6+
// benchmarks/synthetic-corpus/README.md): an independently-compiled copy
7+
// of the same seven-bug pattern tests/projects/multi-rule uses,
8+
// mechanically duplicated by CMake's configure_file() so the corpus size
9+
// is a single cache variable, not N committed files.
10+
extern std::mutex shapeMutex@UNIT@;
11+
extern std::mutex logMutex@UNIT@;
12+
13+
struct Shape@UNIT@ {
14+
virtual std::string describe();
15+
virtual ~Shape@UNIT@() = default;
16+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#include "geometry@UNIT@.h"
2+
#include <string>
3+
4+
void logEvent@UNIT@(const std::string &message) {
5+
std::lock_guard<std::mutex> logLock(logMutex@UNIT@);
6+
std::lock_guard<std::mutex> shapeLock(shapeMutex@UNIT@);
7+
}
8+
9+
std::string buildGreeting@UNIT@() {
10+
std::string name = "world";
11+
return std::move(name);
12+
}
13+
14+
void logTwice@UNIT@(std::string message) {
15+
std::string archived = std::move(message);
16+
// Bug: message was just moved-from above -- triggers use-after-move.
17+
logEvent@UNIT@(message);
18+
}
19+
20+
std::mutex auditMutex@UNIT@;
21+
22+
void auditEvent@UNIT@() {
23+
// Bug: locks auditMutex but never unlocks it -- triggers unbalanced-lock.
24+
auditMutex@UNIT@.lock();
25+
}
26+
27+
void summarize@UNIT@(std::string report) {
28+
// Bug: report is passed by value but only read -- triggers expensive-copy.
29+
logEvent@UNIT@(report);
30+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#include "geometry@UNIT@.h"
2+
3+
std::mutex shapeMutex@UNIT@;
4+
std::mutex logMutex@UNIT@;
5+
6+
struct Circle@UNIT@ : Shape@UNIT@ {
7+
// Missing 'override': triggers missing-override.
8+
std::string describe() {
9+
std::lock_guard<std::mutex> shapeLock(shapeMutex@UNIT@);
10+
std::lock_guard<std::mutex> logLock(logMutex@UNIT@);
11+
return "circle";
12+
}
13+
};
14+
15+
std::string describeAsCircle@UNIT@(Shape@UNIT@ *shape) {
16+
// Bug: dynamic_cast<Circle*> can return nullptr; dereferenced with no
17+
// null check -- triggers unchecked-null-result.
18+
return dynamic_cast<Circle@UNIT@ *>(shape)->describe();
19+
}

docs/architecture.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,24 @@ across independent runs, not session-contaminated -- the number is real,
558558
even without a fully traced explanation of mull's internal coverage
559559
bookkeeping.
560560

561+
## Synthetic benchmark corpus
562+
563+
`benchmarks/synthetic-corpus/` exists because the spdlog benchmark, while
564+
real, is an external dependency at a pinned commit -- reproducing it
565+
means cloning something else first. Nothing about the corpus itself is
566+
committed: `CMakeLists.txt` generates `SENTINEL_BENCHMARK_CORPUS_SIZE`
567+
(default 30) unit pairs via `configure_file()` from three templates,
568+
substituting `@UNIT@` for a unique integer per unit. Each unit reproduces
569+
the same seven-rule bug pattern as `tests/projects/multi-rule`; units are
570+
never linked together (each is only ever an `OBJECT` library source, no
571+
executable), so there's no cross-unit ODR concern and no reason to make
572+
each unit's globals more unique than "don't collide within the same
573+
pair." This makes the corpus's total finding count exactly predictable
574+
(`SENTINEL_BENCHMARK_CORPUS_SIZE * 7`) -- a correctness check on any
575+
benchmark run over it, not just a timing number. See the top-level
576+
README's Benchmark section for the cold/warm cache numbers measured
577+
against it.
578+
561579
## Build
562580

563581
CMake links against a single Homebrew/apt-installed LLVM/Clang (the

0 commit comments

Comments
 (0)