perf: cache header order lookups instead of doing them per comparison - #22
Open
burruplambert wants to merge 2 commits into
Open
perf: cache header order lookups instead of doing them per comparison#22burruplambert wants to merge 2 commits into
burruplambert wants to merge 2 commits into
Conversation
headerSorter instances are pooled and shared between SortedKeyValuesBy and SortedKeyValues, but SortedKeyValues never cleared hs.order. A sorter previously used for an ordered sort would keep its order map and apply it to a later orderless sort, producing the wrong header order whenever the new header's lowercased keys collide with entries in the stale order map. Reset hs.order before sorting in SortedKeyValues and add a regression test.
headerSorter.Less did two order map lookups, each preceded by a
strings.ToLower of the header key, on every comparison - O(n log n) map
lookups and lowercasing per sort. In CPU profiles of a header-heavy
client workload this shows up as runtime.mapaccess2_faststr and
aeshashbody dominating the sort.
Decorate-sort-undecorate instead: SortedKeyValuesBy resolves each key's
order lookup once into (orderIdx, orderOK) slices on the pooled sorter,
Swap keeps them aligned with kvs, and Less compares the cached results
with the exact same four-branch logic as before.
Caching the (index, ok) pair rather than mapping absent keys to a
sentinel index keeps the comparison correct for every possible order
map, including maps whose values reach len(order) - reachable through a
Header-Order: list with a repeated entry, e.g. ["c","a","c"] gives
{"c": 2, "a": 1} - as well as duplicate or negative values. Since the
branch logic is unchanged and only the lookup is hoisted, the produced
order is identical in all cases.
The orderless SortedKeyValues path is unchanged; the caches are
populated only when an order map is set, and the existing pooled-state
reset keeps a stale order from ever indexing them.
Adds order-equivalence tests (including the repeated-entry case), a
pool-reuse test cycling one sorter through ordered and orderless sorts
of different sizes, and a seeded differential test that checks Less
against a reference implementation of the per-comparison semantics
across adversarial order maps.
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.
Depends on #18 - please read first
This branch contains #18's commit as well as its own, because it requires that fix. Without the pooled-state reset from #18, a sorter reused by
SortedKeyValuesstill hasorderset while its caches are sized for the previous header, andLessindexes past them:Merge #18 first and this rebases to a single commit, or merge this and #18 becomes redundant. Either is fine, but this must not land without that reset.
Summary
headerSorter.Lessdid two order map lookups, each preceded by astrings.ToLowerof the header key, on every comparison - so a sort costs O(n log n) map lookups and lowercasing. In CPU profiles of a header-heavy client this shows up asruntime.mapaccess2_faststrandaeshashbodydominating the sort.Decorate-sort-undecorate instead:
SortedKeyValuesByresolves each key's lookup once intoorderIdx/orderOKslices on the pooled sorter,Swapkeeps them aligned withkvs, andLesscompares the cached results using the same four-branch logic as before.Why cache (index, ok) rather than a sentinel
The obvious compression is to map absent keys to
len(order)and compare ints. That is wrong for reachable inputs: the order map is built asorder[v] = iover theHeader-Order:values, so a repeated entry such as["c", "a", "c"]gives{"c": 2, "a": 1}- a present key whose index equalslen(order), which would then tie with every absent key and sort differently. Caching the(index, ok)pair keeps the comparison correct for any order map, including duplicate, negative and sparse values.Since only the lookup is hoisted and the branch logic is untouched, the produced order is identical in all cases.
Tests
Lessagainst a reference implementation of the per-comparison semantics over 200 rounds of adversarial order mapsMeasurements
Ordered header encoding -33% on this branch's base.
BenchmarkHeaderWriteSubsetabout -8%. In production the sorter's comparison cost effectively disappeared from the profile.This supersedes the header-sorting half of the closed #17, which only cached the lowercased keys and left the map lookups in place.
Note
TestHeaderWritecase 9 fails on master before and after this change - the order map is built from theHeader-Order:values verbatim while the comparator looks them up lowercased, so mixed-case entries never match. Unrelated, and left alone since fixing it changes wire behavior.