Repository navigation
perf: a linear ofList, interface walks for reference types and a pooled buffer in choose - #49
Open
xperiandri wants to merge 3 commits into
Open
xperiandri wants to merge 3 commits into
xperiandri wants to merge 3 commits into
Conversation
…ore they build the tree An F# list implements `IReadOnlyList`, and its indexer walks the list from its head. `ImmutableList.CreateRange` builds the tree by indexing a list that implements `IReadOnlyList`, so the build took time that grows with the square of the length: 0.43 ms for 1000 elements, 41 ms for 10 000 and 5.9 s for 100 000. The copy in an array takes 8 µs, 90 µs and 1.4 ms, and the tree is built from the array. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…face `ImmutableList<T>.GetEnumerator` returns a struct, which is the fastest way to walk a list of a value type. The code of a generic function is shared by all reference types, and each call of a member of a generic struct in it needs a lookup of the exact type at run time: walking the list through `IEnumerable<T>` took 0.56 to 0.70 times as long for strings. The internal module `Walk` has the loops `iter`, `iteri`, `fold` and `tryPick`; `typeof<'T>.IsValueType` picks the enumerator, and the body of a loop is a lambda that the compiler inlines into both loops. `iter`, `iteri`, `fold`, `mapi`, `scan`, `collect`, `choose` and `pick`/`tryPick` use them. For strings of 100 to 100 000 elements `iter` and `fold` took 0.6 to 0.7 times as long, `tryPick` 0.6, `mapi` and `scan` 0.7 to 0.9; nothing changed for integers. `mapFold` keeps its loop: compiled into the library, the loop of `Walk` took 1.1 to 1.4 times as long there for strings, with machine code that differs from the faster loop only in the order of a copy of a struct. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the pool for a long list The elements that `IndexedList.choose` and `IIndexedList.choose` choose waited in a `ResizeArray` that doubles its array as it fills. For a million elements it allocated 18 % (integers) to 35 % (strings) more than the tree needs, and the arrays of its growth in the large object heap made the collections of the second generation more frequent. The result is at most as long as the list, so the elements now wait in an array of that length: a new one below 1024 elements, where the array is cheap, and one rented from `ArrayPool<T>.Shared` above, which allocates nothing in the steady state. The rented array is at most 1 048 576 elements long, so that a sparse selection from a huge list does not rent an array of the length of the list; a longer result moves into a list. A rented array is cleared as far as it was written when it holds references, and is not returned when the chooser throws. A million integers allocate 46 875 KB, the size of the tree, where 55 067 KB were allocated before, and a million strings 46 875 KB where 63 259 KB were. Measured with one variant in a process, in a loop that calls the function again and again, the list caused up to 11 collections of the second generation per 1000 calls for 100 000 elements chosen out of 100 000, and the array none. The time did not change for integers of up to 10 000 elements and for a million, and was up to 10 % less for 100 000; with the enumerator of the previous commit strings took 0.65 to 0.9 times as long. A buffer of chunks joined to the result as subtrees was not taken: it took no less time than the array, and the indexer of its tree was 6 to 10 % slower (the average of two runs, 100 to 100 000 elements). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
3 of 6 tasks
This branch has not been deployed
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.
Proposed Changes
Builds on #31. Fixes
IndexedList.ofListandofSeqof an F# list, which built the tree in O(n²), and makes the walking functions andchooseofIndexedList(andIIndexedList.choose) faster for reference types and lighter for long lists. No public signature changes.This is the lower pull request of a stack: #45 (
ofArrayfrom a span, and the pooled temporary arrays) is on top of it. The measurements are in the comments of #45, because they were made for the whole stack: the first gives the method and which numbers to trust, the secondofListand theWalkloops, the thirdchoose, the fifth the other collections (1, 2, 3, 5). Every number in the commit messages and below comes from them.4d98858fix:ofListandofSeqof an F# list copy the list before they build the treeImmutableList.CreateRangeindexes an F# list, whose indexer is O(i)849d6b6perf:IndexedListwalks a list of reference types through the interfaceWalkpicks the enumerator withtypeof<'T>.IsValueType, because the struct enumerator is slow in the code shared by reference typesiter0.6,fold0.7,tryPick0.6,mapiandscan0.7 to 0.9 of the time; integers unchanged.mapFoldkeeps its loop (theWalkloop took 1.1 to 1.4 times as long)d381e3fperf:choosecollects the elements in an array that is rented from the pool for a long listBuffers.ChosenBuffer: a new array below 1024 elements,ArrayPoolabove, capped at 1 048 576 elements (a longer result moves into a list)Ratios are the time relative to the code before; below 1 is faster. A difference inside the noise (about ±10 %, and ±34 % for calls of 100 elements) is not claimed.
Types of changes
Checklist
ofList,ofSeqandchoose(100, 50 and 1 % chosen); the walking functions on strings; the buffer around the length where the pool starts, its spill and the clearing of the rented arrayofListandofSeqin the signature file, and the doc comments of the helpers (they carry the measured numbers, as the comments ofCollectorsdo)Further comments
Why the pool, and not something simpler. The elements of
chooseare not counted in advance, and the tree is built once from them. Four buffers were measured for it, with capacity, with a span, with an exact array and with a rented one, and chunks joined as subtrees and the builder as the extremes (third comment of #45). The rented array wins on allocation (the tree needs 46 875 KB for a million elements; theResizeArrayallocated 17 % more for integers and 35 % more for strings, in arrays that go to the large object heap and cause collections of the second generation) and is not slower beyond the noise; chunks give a tree whose indexer is slower, an array of the length of the source allocates 7 to 11 times as much as the old code when 1 % is chosen, and the builder is 1.2 to 3.3 times slower.What is not here.
ofArrayandreplicate, and the pool for the temporary arrays ofinit,sub,rev,mapiand the others, are in #45. The other collections were measured for the same interface walk (fifth comment of #45); onlySortedSetandSortedMapgain, in #46 and #47.exists2,forall2andcompareWithofIndexedListstill walk two struct enumerators in step.unfoldis unchanged.🤖 Generated with Claude Code