Fix O(n) walk in Access.slice/1 for :get_and_update past range.last#15336
Merged
josevalim merged 1 commit intoelixir-lang:mainfrom May 2, 2026
Merged
Conversation
Member
|
💚 💙 💜 💛 ❤️ |
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.
Change
get_and_update_slice/6walks the full list even afterindex > range.lastand just adds every element after the range to the aggregator without updating it, then reverses the complete list. I added a guard clause that short-circuits once we're pastrange.last, reverses only the updated slice and appends the untouched tail via:lists.reverse(updates, rest).This drops the complexity from O(n) to O(last + 1) for prefix/short slices over long lists.
Benchmark
Benchmark (Benchee, 1s warmup, 3s measurement; M2 Pro, OTP 28.4)
slice :get_and_updatestart0..5of 10kslice :get_and_updatemid100..120of 10kslice :get_and_updatefull0..-1of 1kOther improvements considered but discarded
I also looked at 4 other potential improvements, but they were all slower in the benchmarks. I'm adding them here for documentation purposes:
Considered and discarded
Enum.slice |> Enum.mapinslice(:get, …)with a single recursive walk. Hypothesis: avoid the intermediate list. Reality:Enum.slice/2on lists hits a BIF-backed fast path; the hand-rolled walk withindex in range(which decodes aRangestruct each iteration) was 2–9× slower across all sizes.values(:get_and_update, %{}, …)withdatainstead of%{}and useMap.replace!vsMap.put/2Hypothesis: avoid rebuilding the map from%{}. Reality:Map.replace!(lookup + path-copy) is more expensive per call thanMap.putbuilding from empty for 1k-entry hash maps; no-pop case regressed 25%, half-pop 2×.values(:get_and_update, accumulate{key, update}pairs and finalize withMap.new/1(:maps.from_list). Hypothesis: one BIF beats NMap.putcalls. Reality: marginal at best — no-pop ~6% faster (within original ±84% deviation), half-pop ~13% slower due to extra list-build overhead.Enum.reverse/1calls and theEnum.reduce+Enum.reversepair invalues_keyword(:get_and_update, …). Predicted impact was marginal; left for a future pass since none of the surrounding changes landed.