WIP: DO NOT REVIEW - QueryFeature duplicate-key promotion allocation (perf experiment)#67769
Draft
artl93 wants to merge 3 commits into
Draft
WIP: DO NOT REVIEW - QueryFeature duplicate-key promotion allocation (perf experiment)#67769artl93 wants to merge 3 commits into
artl93 wants to merge 3 commits into
Conversation
When a duplicate query key is encountered, KvpAccumulator promoted the values to a List<string> with default capacity and then added the existing value(s) plus the new one, forcing an immediate growth/copy on the second value. Initialize the list with the known capacity (values.Count + 1) and add the indexed values directly. Ordering and semantics are preserved exactly. Also adds a DuplicateKeys benchmark case to QueryCollectionBenchmarks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d2560db8-c6d9-4e9d-aaa7-8d205d12d86e
Adds QueryDuplicateKeyScalingBenchmarks to characterize how QueryFeature parsing allocations scale with the number of repeated query keys, plus unique-key, multi-duplicate-key, percent-encoded, and long-value controls. Benchmark-only change; exercises the KvpAccumulator promotion path affected by the duplicate-key list pre-size optimization. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d2560db8-c6d9-4e9d-aaa7-8d205d12d86e
Replace the pre-sized-list variant with a default-capacity List<string> populated by an indexed copy loop. AddRange(values) on the original code boxed the StringValues struct (passed as IEnumerable<string>); indexed adds avoid that box while letting the list grow to its default capacity in a single step. Empirical allocation (BenchmarkDotNet, deterministic, crossover A/B vs parent f297235): - No duplicate (key appears once): 0 B change (promotion path not hit). - Every duplicate promotion: -24 B (removes the StringValues box), at every cardinality (key repeated 2/3/4/8/16/32x): uniform -24 B with no regrow penalty. This supersedes the earlier pre-size-to-(Count+1) approach (dd93cf0), which saved 40 B at exactly-2 occurrences but regressed +16 B at 3+. Ordering and semantics are preserved; KvpAccumulator is internal. Also correct the scaling benchmark XML doc (the un-optimized path performs a single promotion, not Occurrences-1 growth events). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d2560db8-c6d9-4e9d-aaa7-8d205d12d86e
Member
Author
|
Still WIP — do not review or merge. Posting the canonical experiment write-up after a skeptical implementation/evidence review. Key outcome of reviewThe original premise ("pre-size the list to avoid growth/copy on the second value") was only partly right. The real cost on the parent path is the boxing of Lineage
Allocation (deterministic bytes/op, interleaved crossover A/B)Scaling, one key repeated N times:
Canonical scenario ( Bottom line
Provenance / artifacts
This PR stays a draft indefinitely. |
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.
I am running an experiment - full analysis to follow. Contact @artl93
What this changes
QueryFeature.KvpAccumulator(internal) promotes a repeated query key into aList<string>. The original code did:AddRange(values)boxes theStringValuesvalue type when it is passed asIEnumerable<string>. This PR replaces it with an indexed copy loop, which avoids the box while letting the list grow to its default capacity in a single step:Ordering and semantics are preserved exactly (
KvpAccumulatoris internal;values[i]!is a compile-time-only null-forgiving;values.Countis effectively 1 at promotion).Lineage (for auditability)
f297235c48c2cffc8fb61ff41803d709b30ddab2Count+1):dd93cf022e456292a23da958582dab199b6333c4e121cb9ec296b0a6c4af21c3868a45c732b586b1v1 was a tradeoff (see table) and is intentionally superseded by the strictly-better Variant-C after a skeptical review.
Measured allocation (BenchmarkDotNet
[MemoryDiagnoser], deterministic bytes/op)Interleaved crossover A/B: whole
QueryFeature.csswapped + rebuilt per arm,Http.dllhashed each arm. Server GC, ShortRun + default throughput job. Apple M5 Pro, .NET SDK 11.0.100-preview.6 (Arm64).Canonical scenario (
ParseNewDuplicateKeys:key1×3 + key2×2):Scaling — one key repeated N times (
RepeatedSingleKey):Interpretation & honest limitations
StringValues.string[2], forcing a 2→4 regrow). Variant-C removes that regression.Correctness / invariants checked
Value ordering through promotion, empty values, percent-encoded keys/values,
OrdinalIgnoreCasecomparer, threefold keys — all covered by existingQueryFeatureTests. Local run: 28/28 Query tests pass.Reproduce
Benchmarks are committed:
src/Http/Http/perf/Microbenchmarks/QueryCollectionBenchmarks.cs(ParseNewDuplicateKeys)src/Http/Http/perf/Microbenchmarks/QueryDuplicateKeyScalingBenchmarks.cs(RepeatedSingleKey, controls)Raw BDN Markdown/CSV/JSON/logs, per-arm binary hashes, source hashes, environment, and a
SHA256SUMSmanifest are retained as experiment artifacts.