Benchmarks, static assertions, system.warnings for per CPU - #109283
Conversation
|
Workflow [PR], commit [58b575b] Summary: ✅
AI ReviewSummaryThis PR adds a startup Findings
Final VerdictStatus: Minimum required action: make |
Without libc rseq registration (glibc < 2.35 or the glibc.pthread.rseq tunable disabled) sched_getcpu falls back to a slower path - a real syscall on AArch64 - paid on every per-CPU counter increment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tight micro-benchmark of per-CPU counter increment: - atomic path: `__atomic_add_fetch(&slots[sched_getcpu()].value, 1, RELAXED)`. - rseq path: `rseq_cpu_start()` + `rseq_load_cbne_store__ptr`. Same cache-aligned slot layout in both. Cache lines never bounce (each CPU's counter is touched by at most one CPU at a time), so the comparison isolates the bus-locked `xadd` vs. the rseq compare-and-store on the hot path. Usage: `clickhouse-examples rseq_vs_atomic_benchmark [--threads N] [--ops N]`. Defaults: threads=hardware_concurrency, ops=50M per thread. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
b27d5bc to
f351072
Compare
alexey-milovidov
left a comment
There was a problem hiding this comment.
Everything else LGTM.
The `rseq_vs_atomic_benchmark` example is Linux-only: it calls `sched_getcpu` and uses `rseq`. It was added to `clickhouse-examples` unconditionally, so a non-Linux `clickhouse-examples` build would pick up a source file it cannot compile. Gate both the source-list entry in `src/Examples/CMakeLists.txt` and the registration in `src/Examples/main.cpp` behind `OS_LINUX`, matching the nearby `thread_creation_latency` and `memory_statistics_os_perf` examples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `system.warnings` message shown when `rseq` is not registered was phrased in terms internal to ClickHouse (`sched_getcpu`, "per-CPU profile counters"). Rewrite it to explain, in plain language, what `rseq` is, what ClickHouse uses it for, the performance impact of its absence, and how to enable it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The startup warning emitted by `sanityChecks` when `PerCPU::haveRSeq` returns `false` previously advised only checking the `glibc.pthread.rseq` tunable. But `haveRSeq` also returns `false` when the runtime `glibc` is older than 2.35 (the weak `__rseq_size` symbol is absent), which is a normal, supported configuration for official builds via `GLIBC_COMPATIBILITY` (e.g. Ubuntu 20.04). On such hosts that tunable does not exist, so the advice was non-actionable and misleading. Reword the message to call out both cases: an old runtime `glibc` (the feature is unavailable, upgrade to benefit from it) versus rseq disabled via the `glibc.pthread.rseq` tunable. Addresses the AI review finding on `programs/server/Server.cpp`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The startup `system.warnings` message for an unavailable `rseq` framed the only causes as "glibc older than 2.35" or the `glibc.pthread.rseq` tunable being disabled. But `PerCPU::haveRSeq` only probes `__rseq_size >= 8`, and `__rseq_size` is 0 whenever the runtime C library or the kernel did not register a usable rseq area - which also happens on non-glibc runtimes (e.g. the `amd_musl` build, whose `base/glibc-compatibility/musl/sched_getcpu.c` uses the same weak-symbol check), on kernels without rseq support, and when registration failed at startup. In those cases the old message pointed users at the wrong remediation (a `glibc` tunable that does not exist there). Reword the remediation to describe the condition generically - the runtime C library or the kernel did not register a usable rseq area - and list old `glibc`, non-glibc libraries such as musl, an old kernel, and the `glibc.pthread.rseq` tunable as possible causes rather than the only ones. Addresses the AI review "Request changes" verdict on this PR.
…INUX The `rseq_vs_atomic_benchmark` example includes `<rseq/rseq.h>` and uses the `librseq` API, both of which come from the `contrib/librseq` submodule. It was added to `clickhouse-examples` behind an `OS_LINUX` guard, but `contrib/CMakeLists.txt` lets `add_contrib(librseq-cmake librseq)` no-op when the `contrib/librseq` submodule is empty — which is how minimal-submodule Linux checkouts (for example `FastTest`) build. In that configuration `OS_LINUX` is still true, so the source was still added, yet `ch_contrib::librseq` (and its `<rseq/rseq.h>` include directory) did not exist, so the translation unit failed to compile. The later `if (TARGET ch_contrib::librseq)` link guard was too late to help. Gate the source on `TARGET ch_contrib::librseq` — the actual capability check — and expose the same condition to the code via a new `USE_LIBRSEQ` compile definition (`config.h`), used in both `src/Examples/main.cpp` (registration) and the benchmark itself in place of `OS_LINUX`. When `librseq` is present the generated code is identical to before; when it is absent the example is not compiled or registered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_SC_NPROCESSORS_CONF` is a CPU count, not a dense upper bound for logical CPU ids, so on hosts with sparse CPU numbering `sched_getcpu` / `rseq_cpu_start` can return an id outside the slot array. Previously `atomicBump` and `rseqBump` dropped the increment in that case, making the reported ns/op artificially low and diverging from the production `ProfileEvents::Counters::fetchAdd` behavior, which falls back to row 0. Now both modes fall back to slot 0, so every loop iteration performs exactly one increment. In the rseq mode the fallback is a plain atomic add, since the rseq critical section only commits while running on the CPU it targets. Addresses AI review finding on ClickHouse#109283
The kernel keeps negative sentinels in the rseq area cpu_id field: -1 (UNINITIALIZED) and -2 (REGISTRATION_FAILED). The production sched_getcpu (base/glibc-compatibility/musl/sched_getcpu.c) rejects them before taking the rseq fast path, while BM_sched_getcpu_rseq only checked __rseq_size, so on a thread with a failed registration it would report a fast "rseq" number that does not match the mechanism sched_getcpu actually uses. Mirror the sentinel check in the availability test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mic example `PerCPU::haveRSeq` and the `rseq_supported` probe in `rseq_vs_atomic_benchmark` treated a sufficiently large `__rseq_size` / `rseq_size` as proof of a usable rseq area, but the kernel uses negative sentinels in `cpu_id` (-1 UNINITIALIZED, -2 REGISTRATION_FAILED) and the production `sched_getcpu` (base/glibc-compatibility/musl/sched_getcpu.c) rejects them before taking the rseq fast path. Mirror that check in both places, like `benchmark_sched_getcpu` already does, so the `system.warnings` entry fires whenever the slow fallback is in use and the example never reports an rseq timing that does not measure a usable fast path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # src/Common/PerCPU.cpp
Address review: `_SC_NPROCESSORS_CONF` is a CPU count, not an upper bound for logical CPU ids, so on hosts with sparse CPU numbering part of the supposedly per-CPU workload collapsed into the shared `slots[0]` fallback, mixing cross-core contention into the reported `atomic`/`rseq` numbers. Use `rseq_get_max_nr_cpus` (librseq parses "/sys/devices/system/cpu/possible", falling back to sysconf) so every `sched_getcpu`/`rseq_cpu_start` value maps to its own slot; the slot-0 fallbacks remain only as guards for error sentinels.
|
|
||
| for (int t = 0; t < opts.threads; ++t) | ||
| { | ||
| workers.emplace_back([&, t]() |
There was a problem hiding this comment.
rseq_vs_atomic_benchmark promises that the per-CPU modes keep each slot private to one CPU, but the workers are never pinned. In the atomic path we do sched_getcpu and then __atomic_add_fetch; if the scheduler migrates the thread in that window, one core updates another core's slot and reintroduces the cache-line bouncing this benchmark says it removed. The rseq path rejects that migration and retries, so the reported atomic / rseq delta can partly measure scheduler-migration artifacts rather than just xadd vs rseq_load_cbne_store__ptr.
Please pin each worker to a dedicated logical CPU (or otherwise validate that the increment ran on the same CPU that selected the slot) before using this benchmark as evidence for the speedup quoted in the PR description.
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 10/24 (41.67%) · Uncovered code |
Changelog category (leave one):
Also according to the results, sched_getcpu() overhead is negligible even w/ plain syscall (though this is only the case when there is real contention), but anyway we have rseq and glibc 2.35+ (released in the beginning 2022) for amd64/aarch64
rseq_vs_atomic_benchmark
src/Common/benchmarks/benchmark_sched_getcpu
Follow-up for: #105056
Version info
26.8.1.124(included in26.8and later)