Pool comparisons across processes, since one process is one observation - #113
Merged
Merged
Conversation
A single Report's interval covers the noise within one process. Each process lays its data out differently, and for large or pointer-heavy data that layout moves the difference by several points, far beyond the interval, and differently in the next process (#109). - Combine pools per-process reports into a Pooled result. It gives a Student t interval over the per-process deltas, the scatter between processes against the scatter within one (Inflation), Cochran's Q and I², and warnings when processes resolve with opposite signs. The t quantile is computed in-package, so there is no new dependency. - The multiproc package re-executes the binary as child processes, one at a time, and collects each child's named reports through a file, not stdout. It pools them and applies an adaptive stop rule: at least 5 processes, at most 20, until every pooled interval is within ±2 points or ±10% of |delta|. The rule is only checked after a whole Rotation (default 2), so alternating build orders stay balanced. - PerturbHeap allocates seeded filler in every small size class and one large block, so that each process gets its own heap layout. DPRNG.Shuffle permutes build orders. - Report.Suspended detects a machine that slept during Compare, from the gap between the wall clock and the monotonic clock. - A drift warning for a single candidate now needs the shift to exceed the result's resolution as well as being significant. It used to fire on almost every run. - HOWTO, README, Report and EstimateDifference say what the interval covers and how to pool. Measured with cmd/rtcompare-aa -workload scan (100 consecutive elements of a linked list whose nodes were allocated in random order) on a Ryzen 9 7900, comparing identical fixtures, so the true difference is zero: - 1M nodes, fixed layout, 8 processes: every process put the same sign on the difference, -2.95% and +3.08% in the two roles, 7 of 8 resolved. Whichever list was built second was faster, and PerturbHeap did not change that. - 1M nodes, perturbed heap and build order alternating by process: 14 processes, pooled +0.29% [-1.59, +2.17] and -0.21% [-2.16, +1.75]. The processes scattered 4.5 times as widely as their own intervals (I² 0.96). 25 of 28 single-process results were resolved at ±3-4%, with both signs. - 4K nodes, same setup: 6 processes, pooled within ±0.5%, scatter 1.7 to 1.8 times the single-process interval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- -workload scan walks 100 consecutive elements of a linked list with randomly placed nodes. It is the range scan from #109. - -perturb applies PerturbHeap before the fixtures are built. - -build random picks ab or ba from -seed. - -multi runs the comparison in several processes through multiproc, perturbing each process's heap and alternating the build order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Using multiproc correctly meant remembering to perturb the heap, keep the spacers alive, alternate the build order by process index, return early in a child and restrict a test's children to that test. Each one forgotten silently weakens the result, so the package now takes care of all of them. - Run perturbs the heap from the process seed in every child before the suite runs, and keeps the spacers alive until the suite is done. Process.PerturbHeap is gone. - Pair describes a comparison by two builder functions. Pairs, Main and RunTest build them in an order that alternates with the process index, compare them and record the report. - Main is a whole benchmark program. It prints progress and the pooled results, and exits in a child. - RunTest restricts the children to the calling test, skips the rest of the test in a child and fails the test on errors. - Report.LiveHeap records the live heap from runtime/metrics. Above LargeHeapThreshold (16 MB) Compare warns that one process is not enough and points to multiproc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #109. Stacked on #112 (#111): this PR reuses
Report.resolution(),DriftRatioandcmd/rtcompare-aa. Merge #112 first; this PR's base then switches tomain.Problem
The interval, noise floor and
Resolvedof aReportdescribe only the noise within one process. For large or pointer-heavy data, the memory layout moves the difference by several points. The layout is fixed for the lifetime of a process and differs in the next one.Changes
Combine(reports, level) (Pooled, error)returns:SpreadBetween/SpreadWithin/Inflation, i.e. how many times more the processes scatter than their own intervals imply;QandI2;The t quantile is computed in-package, checked against table values to 1e-5, so there is no new dependency. I chose the t interval over DerSimonian-Laird because DL is known to be too narrow at k ≈ 5.
multiprocpackage (new):Run(Options, suite)re-executes the binary as child processes, one at a time. Children are recognised through environment variables and pass their named reports back as JSON in a file, so user output on stdout does not interfere.Rotation(default 2): the stop rule is only checked after complete rotations, so that alternating build orders stay balanced (see measurements).ProcessoffersIndex,Seed,Record,PerturbHeap()andRand().go testtoo, withArgs: -test.run=^TestX$; the package's own tests do exactly that.PerturbHeap(seed) *Spacers: seeded filler in every small size class, with and without pointers, plus one large block of up to 16 MB.DPRNG.Shuffle: Fisher–Yates.Report.Suspended: sleep detection via wall clock vs. monotonic clock (> 1 s), with a warning.Drift warning for A/B only when |shift| > resolution (max(floor, half-width)). According to the field report it used to fire in 399 of 406 comparisons.
Docs: HOWTO section "One process is one observation", troubleshooting for suspend, glossary, README, and notes on
Report.EstimateandEstimateDifference.cmd/rtcompare-aa:-workload scan(the range scan from the issue),-perturb,-build randomand-multi.Measurements
Ryzen 9 7900, WSL2. A/A comparison of identical fixtures (true difference = 0),
-workload scan, defaults.PerturbHeap+ random build orderPerturbHeap+ alternating build orderPerturbHeap+ alternatingWhat the measurements show:
PerturbHeap. That is the flip from the issue ("A before or after B: −2.2 % vs +2.2 %"). A random order over 8 processes produced 6:2, so the pooled result leaned. Hence the docs recommend alternating byp.Indexfor two fixtures, andRotationkeeps the stop rule balanced.PerturbHeapdoes: on its own it removed no measurable bias in this reproduction. It stays in because it breaks the determinism of the layout, which the process results would otherwise repeat. The HOWTO says honestly that the build order matters more.Not addressed (as agreed)
caffeinateetc.); it is documented, and sleep is detected.Checks
go test ./...andgo test ./... -racepass, andgolangci-lint runreports 0 issues.Combineon scattered and agreeing processes, its warnings and its input errors;Precise, heterogeneity without SE;Shuffle: permutation, determinism, uniformity;PerturbHeap: determinism and bounds;multiprocend to end with real child processes: pooling, seed reproducibility, stop rule, Rotation, child errors, option checks.Addendum: multiproc takes care of the precautions itself (f23243b)
Correct use of multiproc required several things to be remembered:
PerturbHeap+KeepAlive, build order viap.Index%2, returning early onres.Child, and-test.runin tests. Each forgotten step quietly weakens the result. multiproc now does all of it on its own:Runperturbs the heap in every child before the suite runs, and keeps the spacers alive.Process.PerturbHeaphas been removed.Pair{Name, A, B func() rtcompare.Candidate, Options}: the driver calls the builders itself, in alternating order (A first in even processes, B first in odd ones), then compares and records the result.Main(opt, pairs...): a complete benchmark program with progress output and the pooled result. The child exits by itself.RunTest(t, opt, pairs...): children run only the calling test. In the child the rest of the test is skipped, and errors →t.Fatal.Report.LiveHeap+ warning: aboveLargeHeapThreshold(16 MB of live data, viaruntime/metrics),Comparewarns that one process is not enough and points to multiproc.A benchmark program is now just:
The HOWTO section shrinks from "remember X" to "this happens automatically". Tests (
go test ./...,-race) and lint are green. I also merged all three branches locally and tested them together:go test ./...is green there too.🤖 Generated with Claude Code