Repository navigation
perf: ofArray builds the tree from a span, and the temporary arrays of long lists come from the pool - #45
perf: ofArray builds the tree from a span, and the temporary arrays of long lists come from the pool#45xperiandri wants to merge 2 commits into
ofArray builds the tree from a span, and the temporary arrays of long lists come from the pool#45Conversation
…s array with `Array.Fill` `ImmutableList.CreateRange` reads an array through `IReadOnlyList` and checks the range at every node, where `ImmutableList.Create(ReadOnlySpan)` indexes the array directly. Compiled into the library, the build from a span took 14 to 21 % less time for arrays of 100 to 100 000 strings and 10 to 12 % less for a million, and the same time for integers, within the noise of 10 %. Every function that builds its result from a fresh array goes through `ofFreshArray`, so `init`, `sub`, `rev`, `mapi` and the others get it too: for strings of 100 to 10 000 elements they took 0.78 to 0.99 times as long, for integers the same time. The functions of the `Tuple` namespace call `IndexedList.ofArray` now instead of `CreateRange`. `Array.Fill` is vectorized, where `Array.create` stores the elements one by one: `replicate` took 0.75 to 0.84 times as long for strings of 100 to 10 000 elements and 0.92 to 0.96 for 100 000 and a million, and about the same time for integers. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…i`, `scan` and `mapFold` come from the pool for long lists A new array of 1024 elements or more, which the tree copies and nobody reads again, is garbage of the large object heap from about 20 000 integers or 10 000 references. `Buffers.ofFilled` and `ofFilledWith` give such a function an array that is new below 1024 elements and rented from the pool above, cleared as far as it was written when it holds references. Against the previous commit, compiled into the library and measured with one build in a process (the median of three processes), the functions took 0.85 to 1.08 times as long for 1000 to 10 000 elements, inside the scatter of the processes, and a list of 100 strings pays for the call of the helper and its closure, up to 8 % of a call of 2 to 4 µs. For 100 000 and a million elements: - `replicate` took 0.71 and 0.79 times as long for integers and 0.67 and 0.72 for strings, `init` of strings 0.68 and 0.78; - `rev` of strings took 0.92 and 0.94 times as long, `mapi` of strings 0.95 and 1.01; - `sub`, and `init`, `mapi` and `rev` of integers, took the same time within the noise (`init` and `mapi` of a million integers read 1.14 and 1.10 against a scatter of ±12 and ±4 %); - the collections of the second generation that the new arrays caused, 4 to 46 per 1000 calls, are gone. `scan` and `mapFold` use the same helper and were not measured separately. The helper is a function, not an inline one: copied into every caller, with the lambda in both of its arrays, it made `sub` and `mapi` of integers of 100 to 1000 elements take 1.1 to 1.3 times as long. The commit is separate from the previous one so that it can be dropped: the gain is for large lists only, and the cost is a call of the pool and a closure for each call of these functions. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Measurements 1 of 5: how they were made, and which numbers to trustEvery number in the commit messages and in the descriptions of this pull request and of #49 comes from the tables in the five comments here. The measurements were made for the whole stack (#49 below this pull request, this one on top), so the comments stay on this pull request and #49 links to them. The raw CSV files and the scripts are not part of the pull requests. Setup
Protocols
Corrections to earlier numbers
Legend of the variants of
|
| buffer | tree | |
|---|---|---|
| A | ResizeArray() (the code before this branch) |
CreateRange |
| A2 | A again, the noise of the method | |
| B | ResizeArray(n) with the capacity of the source |
CreateRange |
| C0 | ResizeArray() |
Create(ReadOnlySpan) |
| C | ResizeArray(n) |
Create(ReadOnlySpan) |
| E | a new array of the length of the source | Create(ReadOnlySpan) |
| D | an array of that length rented from ArrayPool<T>.Shared and returned |
Create(ReadOnlySpan) |
| S1, S2 | chunks of 1024 or 8192 elements, built as subtrees and joined | Concat of the subtrees |
| F | ImmutableList.Builder, Add per element |
ToImmutable |
| L | IndexedList.choose of the build in question |
🤖 Generated with Claude Code
Measurements 2 of 4:
|
| n | CreateRange of the list, us |
library ofList after, us |
library ofSeq of a list after, us |
copy to an array and build from a span, us |
|---|---|---|---|---|
int |
||||
| 100 | 7.3 | 1.3 | 1.2 | 1.3 |
| 1000 | 684.5 | 11.7 | 11.7 | 11.8 |
| 10000 | 60067.8 | 143.6 | 146.1 | 145.3 |
| 100000 | 6584392.6 | 1586.2 | 1590.1 | 2071.0 |
string |
||||
| 100 | 7.7 | 1.7 | 1.7 | 1.7 |
| 1000 | 664.8 | 16.0 | 15.0 | 15.8 |
| 10000 | 64165.9 | 177.5 | 172.8 | 169.7 |
| 100000 | 6426022.2 | 1935.2 | 5029.6 | 2480.1 |
Walking an ImmutableList<T>: the struct enumerator against the interface (protocol 1)
ImmutableList<T>.GetEnumerator returns a struct. For a value type that is the fastest walk. 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. A walk through IEnumerable<T> (a boxed enumerator, calls through the interface) took 0.56 to 0.7 times as long for strings and 1.0 to 1.5 times as long for integers. Time relative to the loop for item in list do (the "conservative" one of two runs; struct2 is the same loop again, the noise):
int
| n | interface | foreach | copy | struct2 |
|---|---|---|---|---|
| 10 | 1.54 | 1.27 | 1.02 | 1.00 |
| 100 | 1.06 | 1.00 | 1.00 | 1.00 |
| 1 000 | 1.00 | 1.00 | 1.00 | 1.00 |
| 10 000 | 1.12 | 1.00 | 1.00 | 1.00 |
| 100 000 | 1.00 | 1.00 | 1.00 | 0.97 |
| 1 000 000 | 1.05 | 1.02 | 1.02 | 1.00 |
string
| n | interface | foreach | copy | struct2 |
|---|---|---|---|---|
| 10 | 0.87 | 1.15 | 1.26 | 1.00 |
| 100 | 0.62 | 1.00 | 1.09 | 0.99 |
| 1 000 | 0.59 | 1.00 | 1.17 | 1.00 |
| 10 000 | 0.56 | 1.00 | 1.29 | 1.00 |
| 100 000 | 0.60 | 0.98 | 1.30 | 1.00 |
| 1 000 000 | 0.90 | 1.01 | 1.00 | 0.94 |
foreach is ImmutableList<T>.ForEach with a delegate, copy is CopyTo into an array and a loop over it.
The Walk loops in the library (protocol 1)
Walk picks the enumerator with typeof<'T>.IsValueType, which is a constant for the compiler of the run time, and keeps the body of the loop as an inlined lambda. Library function of this branch against a copy of its old loop in the same process, in brackets the same ratio for the build of the base branch (it must be 1.00: the check of the script). Strings and integers, 100 to 100 000 elements:
| function | type | 100 | 1 000 | 10 000 | 100 000 |
|---|---|---|---|---|---|
iter |
string |
0.62 (0.99) | 0.61 (0.99) | 0.59 (0.98) | 0.63 (1.00) |
iter |
int |
0.90 (0.98) | 0.91 (1.00) | 0.90 (1.00) | 0.96 (1.01) |
fold |
string |
0.66 (1.00) | 0.66 (1.01) | 0.67 (0.98) | 0.70 (0.97) |
fold |
int |
0.99 (1.00) | 0.99 (1.00) | 0.99 (1.00) | 0.99 (1.01) |
mapi |
string |
0.80 (1.00) | 0.79 (1.00) | 0.78 (1.01) | 0.89 (0.99) |
mapi |
int |
1.01 (0.98) | 1.00 (1.01) | 0.98 (1.00) | 1.25 (1.02) |
scan |
string |
0.78 (0.99) | 0.79 (1.01) | 0.80 (0.99) | 0.81 (1.07) |
scan |
int |
0.97 (1.00) | 0.98 (1.00) | 1.00 (1.00) | 0.95 (0.97) |
mapFold |
string |
0.98 (0.98) | 0.99 (0.98) | 0.98 (0.98) | 0.99 (0.85) |
mapFold |
int |
0.97 (0.98) | 1.00 (1.00) | 0.99 (1.00) | 0.88 (1.01) |
Two cells deserve a remark. The 100 000-element cells share the noise of the second-generation collections (protocol 1 is unreliable there, see the first comment); mapi of integers at 100 000 elements is checked again below with one build per process. mapFold is not a Walk loop: when it was, it took longer in the library (next table), so it keeps the loop it had, and the row above reads 1.00.
tryPick and collect have no copy of the old loop to compare with, so the library of this branch is compared with the library of the base branch (4 pairs of runs): tryPick of strings took 0.60 to 0.69 times as long, of integers 0.87 to 0.97; collect of strings 0.87 to 1.11 and of integers 0.89 to 1.07, that is the same time.
The same functions at 100 000 and a million elements, one build per process (protocol 2)
The library before this branch (before) against the library of this branch, three replicate processes each, in the order before, branch, branch, before, before, branch; the time of before is the median of its three replicates, the ratio is of the medians, and the last column lists the three replicate means of each build.
The replicates scatter by 10 to 40 % here, even for functions that allocate nothing: mapFold of integers, whose code this branch did not change, reads 1.26 (±23 %) and 0.93. So only the large effects can be read from this table: iter and tryPick of strings at 100 000 elements (0.62 and 0.64, as in the table before), and at a million elements the effect shrinks (0.72 and 0.97), because a pass over 48 MB of nodes waits for memory more than for the enumerator. For the functions that allocate nothing (iter, fold, tryPick) the in-process table above is the better evidence than this one, since two variants in one process do not suffer the differences between processes; integers read 0.85 to 1.26 here and 0.90 to 1.01 there, apart from mapi at 100 000 elements (1.25 there, which allocates, so it suffers from the uneven collections described in the first comment).
| function | type | n | before this branch | this branch / before | replicates (before, branch) |
|---|---|---|---|---|---|
iter |
string |
100 000 | 2.81 ms (±10%) | 0.62 (±30%) | 3.09 ms 2.81 ms 2.52 ms ; 2.71 ms 1.74 ms 1.52 ms |
iter |
string |
1 000 000 | 57.29 ms (±16%) | 0.72 (±6%) | 57.29 ms 64.34 ms 46.63 ms ; 45.23 ms 40.18 ms 41.52 ms |
iter |
int |
100 000 | 1.04 ms (±11%) | 1.06 (±13%) | 1.23 ms 986 us 1.04 ms ; 1.10 ms 1.37 ms 1.07 ms |
iter |
int |
1 000 000 | 27.31 ms (±14%) | 1.09 (±14%) | 32.83 ms 27.31 ms 24.81 ms ; 33.69 ms 29.70 ms 25.28 ms |
fold |
string |
100 000 | 3.10 ms (±13%) | 0.95 (±36%) | 3.74 ms 3.10 ms 2.88 ms ; 4.41 ms 2.93 ms 2.16 ms |
fold |
string |
1 000 000 | 48.90 ms (±13%) | 0.98 (±9%) | 61.89 ms 48.10 ms 48.90 ms ; 47.01 ms 55.67 ms 47.68 ms |
fold |
int |
100 000 | 1.54 ms (±10%) | 1.18 (±12%) | 1.82 ms 1.49 ms 1.54 ms ; 1.99 ms 1.82 ms 1.55 ms |
fold |
int |
1 000 000 | 29.35 ms (±13%) | 1.17 (±12%) | 37.22 ms 29.35 ms 29.22 ms ; 34.31 ms 38.67 ms 30.15 ms |
mapi |
string |
100 000 | 5.81 ms (±4%) | 1.19 (±39%) | 6.19 ms 5.81 ms 5.70 ms ; 6.92 ms 10.84 ms 4.90 ms |
mapi |
string |
1 000 000 | 82.26 ms (±2%) | 0.86 (±6%) | 82.26 ms 82.44 ms 79.77 ms ; 70.98 ms 79.22 ms 71.09 ms |
mapi |
int |
100 000 | 3.88 ms (±8%) | 1.04 (±13%) | 4.42 ms 3.88 ms 3.75 ms ; 4.58 ms 4.02 ms 3.54 ms |
mapi |
int |
1 000 000 | 52.86 ms (±6%) | 1.00 (±12%) | 57.79 ms 51.75 ms 52.86 ms ; 64.73 ms 50.84 ms 53.12 ms |
scan |
string |
100 000 | 5.04 ms (±9%) | 1.13 (±9%) | 5.04 ms 4.80 ms 5.73 ms ; 5.69 ms 6.27 ms 5.28 ms |
scan |
string |
1 000 000 | 68.82 ms (±10%) | 0.93 (±6%) | 81.35 ms 66.86 ms 68.82 ms ; 70.41 ms 62.97 ms 63.78 ms |
scan |
int |
100 000 | 3.90 ms (±14%) | 1.25 (±14%) | 4.97 ms 3.90 ms 3.75 ms ; 4.86 ms 4.88 ms 3.61 ms |
scan |
int |
1 000 000 | 55.10 ms (±8%) | 1.04 (±8%) | 62.08 ms 55.10 ms 52.93 ms ; 66.29 ms 57.36 ms 57.19 ms |
mapFold |
string |
100 000 | 5.79 ms (±10%) | 1.17 (±3%) | 6.93 ms 5.79 ms 5.71 ms ; 6.77 ms 6.55 ms 6.95 ms |
mapFold |
string |
1 000 000 | 77.02 ms (±11%) | 1.05 (±4%) | 92.70 ms 74.60 ms 77.02 ms ; 86.16 ms 80.00 ms 81.12 ms |
mapFold |
int |
100 000 | 3.91 ms (±28%) | 1.26 (±23%) | 5.70 ms 3.91 ms 3.31 ms ; 4.92 ms 5.40 ms 3.31 ms |
mapFold |
int |
1 000 000 | 59.54 ms (±10%) | 0.93 (±10%) | 60.71 ms 59.54 ms 49.30 ms ; 61.25 ms 55.24 ms 49.80 ms |
collect |
string |
100 000 | 46.90 ms (±3%) | 1.02 (±2%) | 48.64 ms 46.90 ms 45.80 ms ; 49.30 ms 47.98 ms 47.44 ms |
collect |
string |
1 000 000 | 390.31 ms (±5%) | 0.92 (±11%) | 397.43 ms 360.49 ms 390.31 ms ; 441.93 ms 357.31 ms 356.78 ms |
collect |
int |
100 000 | 22.44 ms (±10%) | 1.19 (±6%) | 25.80 ms 21.21 ms 22.44 ms ; 27.97 ms 26.72 ms 24.88 ms |
collect |
int |
1 000 000 | 234.05 ms (±18%) | 1.20 (±9%) | 309.51 ms 217.61 ms 234.05 ms ; 281.01 ms 296.64 ms 247.35 ms |
tryPick |
string |
100 000 | 2.42 ms (±7%) | 0.64 (±7%) | 2.45 ms 2.42 ms 2.12 ms ; 1.55 ms 1.79 ms 1.56 ms |
tryPick |
string |
1 000 000 | 44.29 ms (±15%) | 0.97 (±11%) | 56.76 ms 42.35 ms 44.29 ms ; 45.40 ms 35.95 ms 43.03 ms |
tryPick |
int |
100 000 | 963 us (±9%) | 1.18 (±13%) | 1.10 ms 919 us 963 us ; 1.15 ms 1.13 ms 876 us |
tryPick |
int |
1 000 000 | 32.18 ms (±16%) | 0.85 (±27%) | 32.18 ms 32.97 ms 23.48 ms ; 27.47 ms 40.31 ms 23.87 ms |
Why mapFold stays as it was (protocol 1, strings and integers)
Library mapFold written with the Walk loop, against a copy of the old loop in the same process, in four runs:
| run | type | 10 | 100 | 1 000 | 10 000 | 100 000 | 1 000 000 |
|---|---|---|---|---|---|---|---|
| 1 | string |
1.23 | 1.20 | 1.16 | 1.15 | 1.19 | 1.05 |
| 2 | string |
1.21 | 1.23 | 1.19 | 1.19 | 1.12 | 1.07 |
| 3 | string |
- | 1.23 | 1.13 | 1.18 | 1.30 | 1.10 |
| 4 | string |
- | 1.19 | 1.19 | 1.30 | 1.37 | 1.04 |
| 1 | int |
0.99 | 1.00 | 1.01 | 0.93 | 0.90 | 1.04 |
| 2 | int |
0.99 | 0.98 | 0.99 | 1.06 | 0.92 | 1.02 |
| 3 | int |
- | 1.04 | 1.02 | 0.99 | 1.01 | 1.03 |
| 4 | int |
- | 0.99 | 1.02 | 0.98 | 1.06 | 1.00 |
For strings the loop of Walk took 1.1 to 1.4 times as long, although the machine code of the two loops differs only in the order of a copy of the struct that the mapping returns, and the same source in a script ran 0.8 times as long. The reason was not found; the old loop stays.
🤖 Generated with Claude Code
Measurements 3 of 4:
|
| n | B | C0 | C | E | D | S1 | S2 | F | A2 |
|---|---|---|---|---|---|---|---|---|---|
| 10 | 1.00 | 1.00 | 1.00 | 0.93 | 1.00 | 0.91 | 0.85 | 1.18 | 1.00 |
| 100 | 0.83 | 0.94 | 1.00 | 1.00 | 0.99 | 1.00 | 0.97 | 1.91 | 1.00 |
| 1 000 | 0.98 | 1.00 | 1.00 | 1.00 | 0.98 | 0.91 | 0.91 | 2.85 | 0.99 |
| 10 000 | 0.89 | 1.00 | 0.93 | 0.83 | 0.78 | 0.84 | 0.90 | 3.32 | 0.89 |
int, 50 % of the elements chosen
| n | B | C0 | C | E | D | S1 | S2 | F | A2 |
|---|---|---|---|---|---|---|---|---|---|
| 10 | 0.93 | 0.98 | 1.00 | 1.00 | 0.93 | 1.00 | 0.98 | 1.21 | 1.00 |
| 100 | 0.98 | 0.97 | 0.96 | 0.98 | 0.97 | 0.97 | 0.98 | 1.53 | 0.97 |
| 1 000 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | 1.01 | 2.30 | 1.00 |
| 10 000 | 1.00 | 1.00 | 1.00 | 0.96 | 0.96 | 0.95 | 1.00 | 2.77 | 1.01 |
int, 1 % of the elements chosen
| n | B | C0 | C | E | D | S1 | S2 | F | A2 |
|---|---|---|---|---|---|---|---|---|---|
| 10 | 1.00 | 1.01 | 1.01 | 1.00 | 1.00 | 1.09 | 1.10 | 1.12 | 1.07 |
| 100 | 1.00 | 1.00 | 0.99 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | 0.96 |
| 1 000 | 1.03 | 1.00 | 1.06 | 1.01 | 1.00 | 1.02 | 1.03 | 1.01 | 1.00 |
| 10 000 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 |
string, 100 % of the elements chosen
| n | B | C0 | C | E | D | S1 | S2 | F | A2 |
|---|---|---|---|---|---|---|---|---|---|
| 10 | 0.85 | 0.99 | 0.81 | 0.86 | 0.83 | 0.80 | 0.80 | 0.99 | 0.97 |
| 100 | 0.98 | 1.04 | 0.94 | 0.87 | 0.93 | 0.92 | 0.98 | 1.52 | 1.00 |
| 1 000 | 0.94 | 1.00 | 1.00 | 0.99 | 1.00 | 1.00 | 0.97 | 2.01 | 0.98 |
| 10 000 | 0.91 | 1.00 | 0.91 | 0.90 | 0.87 | 0.91 | 0.92 | 2.28 | 1.00 |
string, 50 % of the elements chosen
| n | B | C0 | C | E | D | S1 | S2 | F | A2 |
|---|---|---|---|---|---|---|---|---|---|
| 10 | 0.85 | 0.96 | 0.90 | 0.81 | 0.89 | 0.95 | 0.90 | 1.00 | 0.99 |
| 100 | 0.99 | 0.97 | 0.91 | 0.94 | 0.95 | 1.00 | 0.95 | 1.19 | 0.97 |
| 1 000 | 1.00 | 0.99 | 1.00 | 0.98 | 0.98 | 0.96 | 1.00 | 1.54 | 1.00 |
| 10 000 | 1.05 | 1.01 | 0.99 | 1.00 | 1.00 | 1.00 | 1.01 | 1.91 | 1.00 |
string, 1 % of the elements chosen
| n | B | C0 | C | E | D | S1 | S2 | F | A2 |
|---|---|---|---|---|---|---|---|---|---|
| 10 | 1.00 | 0.91 | 0.90 | 0.92 | 0.89 | 0.88 | 0.91 | 0.89 | 0.96 |
| 100 | 1.00 | 0.99 | 1.01 | 1.03 | 1.00 | 1.00 | 1.03 | 1.00 | 1.00 |
| 1 000 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | 0.98 |
| 10 000 | 1.02 | 1.00 | 0.99 | 1.00 | 0.98 | 0.97 | 1.00 | 1.01 | 1.00 |
Reading: the variants B, C0, C, E, D, S1 and S2 are within 0.78 to 1.10 of A, which is about the size of the noise (A2 reads 0.89 to 1.07), and the buffer is a minority of the time anyway, because the tree allocates one node of 48 bytes per element and builds it once. The lowest cells are E and D at 10 000 elements when half or all of the elements are chosen (0.78 to 0.96). The builder (F) is 1.2 to 3.3 times slower when half or all of the elements are chosen, and as fast when 1 % are.
Allocation of one call for a million elements (exact)
| variant | int 100 % |
int 50 % |
int 1 % |
string 100 % |
string 50 % |
string 1 % |
|---|---|---|---|---|---|---|
| A | 55067.5 KB | 27533.9 KB | 597.2 KB | 63259.5 KB | 31629.9 KB | 725.2 KB |
| B | 50781.3 KB | 27343.7 KB | 4375.2 KB | 54687.6 KB | 31250.0 KB | 8281.4 KB |
| C0 | 55067.5 KB | 27533.9 KB | 597.2 KB | 63259.5 KB | 31629.9 KB | 725.2 KB |
| C | 50781.3 KB | 27343.7 KB | 4375.2 KB | 54687.6 KB | 31250.0 KB | 8281.4 KB |
| E | 50781.3 KB | 27343.7 KB | 4375.1 KB | 54687.5 KB | 31250.0 KB | 8281.4 KB |
| D | 46875.0 KB | 23437.4 KB | 468.9 KB | 46875.0 KB | 23437.4 KB | 468.9 KB |
| S1 | 48295.0 KB | 24126.5 KB | 483.0 KB | 48299.0 KB | 24130.5 KB | 487.0 KB |
| S2 | 47106.6 KB | 23566.2 KB | 502.0 KB | 47138.6 KB | 23598.2 KB | 534.0 KB |
| F | 46875.0 KB | 23437.4 KB | 468.9 KB | 46875.0 KB | 23437.4 KB | 468.9 KB |
The tree needs 46 875 KB for a million elements. A allocates 17 % (integers) to 35 % (strings) more than the tree when half or all of the elements are chosen (the arrays of its growth). An array of the length of the source that is allocated anew (B, C, E) allocates 7 to 11 times as much as A (9 to 18 times as much as the tree needs) when 1 % of the elements are chosen, and a buffer of that length would be an array of a hundred million elements for a list of that size; a rented array (D) does not allocate it, and the branch caps the rented length at 1 048 576 elements and spills a longer result into a list.
Where the pool starts to pay (protocol 1, 100 % chosen)
An array of its own (E) and a rented one (D) took about the same time as the list (A) from 4 to 2 048 elements, 0.80 to 1.10 of its time and mostly 0.9 to 1.0; the first call that rents an array of a length also allocates it. The branch uses an array of its own below 1024 elements and the pool from 1024.
| n | int 100 %: A, us |
exact array / A | pooled array / A | first call, us | string 100 %: A, us |
exact / A | pooled / A | first call, us |
|---|---|---|---|---|---|---|---|---|
| 4 | 0.089 | 0.91 | 0.89 | 5.9 | 0.156 | 0.85 | 0.88 | 11.2 |
| 8 | 0.155 | 0.86 | 0.88 | 6.4 | 0.253 | 0.88 | 0.88 | 11.6 |
| 16 | 0.309 | 0.93 | 0.80 | 6.0 | 0.445 | 0.92 | 0.92 | 11.8 |
| 32 | 0.430 | 0.99 | 1.04 | 6.3 | 1.137 | 0.85 | 0.83 | 11.6 |
| 64 | 0.890 | 0.97 | 0.93 | 9.9 | 1.657 | 0.97 | 0.94 | 13.8 |
| 128 | 1.719 | 0.93 | 0.95 | 10.0 | 3.399 | 0.90 | 0.91 | 20.0 |
| 256 | 3.826 | 0.99 | 0.91 | 13.3 | 6.458 | 1.01 | 1.00 | 21.2 |
| 512 | 7.328 | 1.00 | 1.10 | 21.9 | 13.188 | 0.92 | 0.90 | 39.6 |
| 1024 | 14.476 | 0.97 | 0.97 | 34.5 | 25.756 | 0.98 | 0.96 | 81.4 |
| 2048 | 27.656 | 0.97 | 0.95 | 44.3 | 51.187 | 0.92 | 0.96 | 72.9 |
First call of the rented variant against the warm calls, microseconds (the first call allocates the array of its bucket):
| type | n | first call of D | bytes of the first call | D, warm | E, warm | A |
|---|---|---|---|---|---|---|
| int | 10 | 7.30 | 592 B | 0.24 | 0.23 | 0.26 |
| int | 100 | 18.00 | 5.2 KB | 2.75 | 3.06 | 2.90 |
| int | 1000 | 84.20 | 50.9 KB | 16.85 | 18.07 | 18.54 |
| int | 10000 | 233 | 468.8 KB | 172 | 180 | 219 |
| int | 100000 | 5248 | 5199.5 KB | 2413 | 2596 | 3009 |
| int | 1000000 | 40.3 ms | 50971.0 KB | 43.3 ms | 47.4 ms | 45.3 ms |
| string | 10 | 15.80 | 656 B | 0.36 | 0.37 | 0.45 |
| string | 100 | 20.50 | 5.7 KB | 2.95 | 2.75 | 3.19 |
| string | 1000 | 81.35 | 54.9 KB | 31.59 | 29.35 | 31.77 |
| string | 10000 | 1026 | 596.8 KB | 307 | 327 | 407 |
| string | 100000 | 8293 | 5711.5 KB | 4079 | 4169 | 4951 |
| string | 1000000 | 72.2 ms | 55067.0 KB | 62.5 ms | 59.0 ms | 69.2 ms |
The tree built from joined chunks (the shape experiment, protocol 1, int)
Relative time of a pass over a tree built from chunks of 8192 elements and joined, against a tree built in one piece; the average of two runs, which disagree at some sizes (single runs read 0.77 to 1.17 for the indexer). The indexer is the cost that decided against the chunks.
| type | n | indexer: joined / built | enumeration: joined / built | Add: joined / built |
|---|---|---|---|---|
int |
10 | 0.93 | 0.99 | 0.93 |
int |
100 | 1.06 | 1.08 | 0.84 |
int |
1000 | 1.09 | 1.12 | 0.96 |
int |
10000 | 1.09 | 1.04 | 1.07 |
int |
100000 | 1.10 | 0.87 | 1.00 |
int |
1000000 | 0.93 | 1.04 | 1.03 |
100 000 and a million elements, one variant per process (protocol 2)
Mean time per call of the old code (A and A2, six processes together) with the scatter of the replicates; D is the array from the pool with the old loop; L is IndexedList.choose of this branch (the pool and the Walk loop). The ratios are to the old code. Allocation, collections of the second generation per 1000 calls and the GC pause per call are given as old -> new.
| elements | n | old code (A and A2, 6 processes) | D: pool, old loop | L: this branch | allocation old -> new | gen2 per 1000 calls old -> new | GC pause per call old -> new |
|---|---|---|---|---|---|---|---|
int, 100 % |
100 000 | 3.8 ms (±12%) | 0.79 (±1%) | 0.94 (±22%) | 5712 -> 4688 KB | 6.7 -> 0.0 | 1.0 ms -> 350 us |
int, 100 % |
1 000 000 | 52.9 ms (±8%) | 0.99 (±4%) | 0.98 (±4%) | 55067 -> 46875 KB | 14.8 -> 0.0 | 10.4 ms -> 8.3 ms |
int, 50 % |
100 000 | 2.5 ms (±11%) | 0.85 (±7%) | 0.90 (±5%) | 2856 -> 2344 KB | 3.1 -> 0.0 | 454 us -> 137 us |
int, 50 % |
1 000 000 | 39.3 ms (±12%) | 0.98 (±4%) | 1.03 (±11%) | 27534 -> 23437 KB | 0.0 -> 0.0 | 2.4 ms -> 2.6 ms |
int, 1 % |
100 000 | 1.2 ms (±8%) | 1.06 (±4%) | 1.11 (±19%) | 55 -> 47 KB | 0.0 -> 0.0 | 3 us -> 2 us |
int, 1 % |
1 000 000 | 28.2 ms (±4%) | 1.00 (±6%) | 1.02 (±4%) | 597 -> 469 KB | 0.0 -> 0.0 | 0 us -> 0 us |
string, 100 % |
100 000 | 7.4 ms (±10%) | 0.75 (±25%) | 0.65 (±15%) | 6736 -> 4688 KB | 9.8 -> 0.0 | 1.7 ms -> 366 us |
string, 100 % |
1 000 000 | 78.1 ms (±6%) | 0.94 (±6%) | 0.84 (±7%) | 63259 -> 46875 KB | 40.0 -> 0.0 | 15.7 ms -> 9.2 ms |
string, 50 % |
100 000 | 4.7 ms (±11%) | 0.95 (±24%) | 0.67 (±8%) | 3368 -> 2344 KB | 2.5 -> 0.0 | 507 us -> 138 us |
string, 50 % |
1 000 000 | 58.8 ms (±6%) | 0.98 (±1%) | 0.89 (±10%) | 31630 -> 23438 KB | 0.0 -> 0.0 | 2.6 ms -> 2.8 ms |
string, 1 % |
100 000 | 2.7 ms (±17%) | 1.01 (±15%) | 0.73 (±6%) | 63 -> 47 KB | 0.0 -> 0.0 | 0 us -> 1 us |
string, 1 % |
1 000 000 | 44.1 ms (±9%) | 1.00 (±1%) | 0.85 (±8%) | 725 -> 469 KB | 0.0 -> 0.0 | 0 us -> 0 us |
Reading:
- Integers: the time is inside the noise (0.90 to 1.03); the one cell above it, 1 % of 100 000, comes from one replicate (1675 us, the others 1150 to 1290).
- Strings: 0.65 to 0.89. Most of it is the
Walkloop, not the pool:Dalone is about 1.0 except where the old code's collections are large (100 % chosen). - Allocation: 15 to 21 % less for integers and 25 to 35 % less for strings.
- Collections of the second generation: with the pool there were none in any cell. The old code had up to 11 per 1000 calls at 100 000 elements (6.7 and 10.9 in single processes when everything is chosen), and between 0 and 77 at a million depending on the process, so its pause is very variable there.
- GC pause per call at 100 000 elements when everything is chosen: 1.0 ms -> 0.35 ms for integers, 1.7 ms -> 0.37 ms for strings.
The same runs per variant: mean time, scatter, allocation, collections, pause
int, 100 % of the elements chosen
| n | A: mean us (± spread %), KB, gen2/1000 calls, pause us/call | A2: mean us (± spread %), KB, gen2/1000 calls, pause us/call | D: mean us (± spread %), KB, gen2/1000 calls, pause us/call | L: mean us (± spread %), KB, gen2/1000 calls, pause us/call |
|---|---|---|---|---|
| 100000 | 3914.1 (±11.1) 5712 6.7 1037.4 | 3621.4 (±7.5) 5712 6.7 1017.0 | 2986.8 (±0.7) 4688 0.0 338.9 | 3524.1 (±21.9) 4688 0.0 350.1 |
| 1000000 | 53190.7 (±7.8) 55067 1.9 8988.4 | 52543.2 (±8.3) 55067 27.8 11817.1 | 52374.4 (±4.4) 46875 0.0 8980.8 | 51623.8 (±3.9) 46875 0.0 8303.7 |
Ratio to A, mean / median of the rounds (A2 is A again):
| n | A2 | D | L |
|---|---|---|---|
| 100000 | 0.93 / 0.95 | 0.76 / 0.80 | 0.90 / 0.90 |
| 1000000 | 0.99 / 1.01 | 0.98 / 1.01 | 0.97 / 1.00 |
int, 50 % of the elements chosen
| n | A: mean us (± spread %), KB, gen2/1000 calls, pause us/call | A2: mean us (± spread %), KB, gen2/1000 calls, pause us/call | D: mean us (± spread %), KB, gen2/1000 calls, pause us/call | L: mean us (± spread %), KB, gen2/1000 calls, pause us/call |
|---|---|---|---|---|
| 100000 | 2708.9 (±3.7) 2856 3.5 503.9 | 2372.4 (±6.8) 2856 2.7 394.5 | 2170.8 (±6.8) 2344 0.0 138.5 | 2278.7 (±5.2) 2344 0.0 137.0 |
| 1000000 | 39826.2 (±2.3) 27534 0.0 2752.2 | 38758.0 (±12.0) 27534 0.0 1971.5 | 38402.7 (±4.2) 23437 0.0 2838.9 | 40461.6 (±10.9) 23437 0.0 2567.2 |
Ratio to A, mean / median of the rounds (A2 is A again):
| n | A2 | D | L |
|---|---|---|---|
| 100000 | 0.88 / 0.90 | 0.80 / 0.80 | 0.84 / 0.84 |
| 1000000 | 0.97 / 0.93 | 0.96 / 0.96 | 1.02 / 0.99 |
int, 1 % of the elements chosen
| n | A: mean us (± spread %), KB, gen2/1000 calls, pause us/call | A2: mean us (± spread %), KB, gen2/1000 calls, pause us/call | D: mean us (± spread %), KB, gen2/1000 calls, pause us/call | L: mean us (± spread %), KB, gen2/1000 calls, pause us/call |
|---|---|---|---|---|
| 100000 | 1279.2 (±4.0) 55 0.0 2.7 | 1198.7 (±7.7) 55 0.0 2.6 | 1308.4 (±4.2) 47 0.0 1.1 | 1371.6 (±19.1) 47 0.0 2.0 |
| 1000000 | 28749.2 (±2.2) 597 0.0 0.0 | 27710.3 (±2.8) 597 0.0 0.0 | 28335.1 (±6.3) 469 0.0 0.0 | 28829.3 (±4.3) 469 0.0 0.0 |
Ratio to A, mean / median of the rounds (A2 is A again):
| n | A2 | D | L |
|---|---|---|---|
| 100000 | 0.94 / 0.94 | 1.02 / 1.01 | 1.07 / 1.05 |
| 1000000 | 0.96 / 0.95 | 0.99 / 0.97 | 1.00 / 0.98 |
string, 100 % of the elements chosen
| n | A: mean us (± spread %), KB, gen2/1000 calls, pause us/call | A2: mean us (± spread %), KB, gen2/1000 calls, pause us/call | D: mean us (± spread %), KB, gen2/1000 calls, pause us/call | L: mean us (± spread %), KB, gen2/1000 calls, pause us/call |
|---|---|---|---|---|
| 100000 | 7705.0 (±1.7) 6736 10.9 1871.5 | 7157.6 (±10.0) 6736 8.9 1631.5 | 5589.9 (±24.7) 4688 0.0 364.1 | 4823.5 (±15.1) 4688 0.0 366.3 |
| 1000000 | 75590.5 (±2.4) 63259 0.0 10329.8 | 80612.3 (±4.5) 63259 76.9 20576.4 | 73050.0 (±6.2) 46875 0.0 9666.0 | 65890.9 (±6.7) 46875 0.0 9153.6 |
Ratio to A, mean / median of the rounds (A2 is A again):
| n | A2 | D | L |
|---|---|---|---|
| 100000 | 0.93 / 0.89 | 0.73 / 0.68 | 0.63 / 0.60 |
| 1000000 | 1.07 / 1.08 | 0.97 / 0.96 | 0.87 / 0.89 |
string, 50 % of the elements chosen
| n | A: mean us (± spread %), KB, gen2/1000 calls, pause us/call | A2: mean us (± spread %), KB, gen2/1000 calls, pause us/call | D: mean us (± spread %), KB, gen2/1000 calls, pause us/call | L: mean us (± spread %), KB, gen2/1000 calls, pause us/call |
|---|---|---|---|---|
| 100000 | 4586.2 (±2.6) 3368 0.0 252.2 | 4898.8 (±11.0) 3368 4.3 688.1 | 4483.0 (±24.2) 2344 0.0 146.0 | 3197.2 (±7.5) 2344 0.0 138.1 |
| 1000000 | 57963.5 (±0.9) 31630 0.0 2211.7 | 59567.4 (±6.0) 31630 0.0 2943.6 | 57518.1 (±0.9) 23437 0.0 2581.1 | 52375.6 (±9.9) 23438 0.0 2799.2 |
Ratio to A, mean / median of the rounds (A2 is A again):
| n | A2 | D | L |
|---|---|---|---|
| 100000 | 1.07 / 1.03 | 0.98 / 0.92 | 0.70 / 0.69 |
| 1000000 | 1.03 / 1.01 | 0.99 / 1.00 | 0.90 / 0.90 |
string, 1 % of the elements chosen
| n | A: mean us (± spread %), KB, gen2/1000 calls, pause us/call | A2: mean us (± spread %), KB, gen2/1000 calls, pause us/call | D: mean us (± spread %), KB, gen2/1000 calls, pause us/call | L: mean us (± spread %), KB, gen2/1000 calls, pause us/call |
|---|---|---|---|---|
| 100000 | 2607.8 (±5.2) 63 0.0 0.0 | 2694.3 (±17.1) 63 0.0 0.0 | 2680.1 (±14.5) 47 0.0 0.0 | 1926.5 (±6.4) 47 0.0 1.1 |
| 1000000 | 44408.1 (±2.8) 725 0.0 0.0 | 43784.0 (±8.8) 725 0.0 0.0 | 44169.3 (±1.4) 469 0.0 0.0 | 37688.7 (±8.2) 469 0.0 0.0 |
Ratio to A, mean / median of the rounds (A2 is A again):
| n | A2 | D | L |
|---|---|---|---|
| 100000 | 1.03 / 0.96 | 1.03 / 1.05 | 0.74 / 0.77 |
| 1000000 | 0.99 / 1.00 | 0.99 / 1.03 | 0.85 / 0.84 |
🤖 Generated with Claude Code
Measurements 4 of 4:
|
| n | base (us) | A / base | B / base | B / A | gen2 per 1000 calls: base / A / B |
|---|---|---|---|---|---|
| 100 | 1.24 us (±34%) | 1.58 | 1.76 | 1.11 | 0.0 / 0.0 / 0.0 |
| 1000 | 10.10 us (±8%) | 1.08 | 1.06 | 0.98 | 0.0 / 0.0 / 0.0 |
| 10000 | 126.06 us (±1%) | 1.08 | 1.03 | 0.95 | 0.0 / 0.0 / 0.0 |
| 100000 | 1.64 ms (±7%) | 0.93 | 0.96 | 1.03 | 0.0 / 0.0 / 0.0 |
| 1000000 | 18.68 ms (±4%) | 0.96 | 1.01 | 1.05 | 0.0 / 0.0 / 0.0 |
fresh, string
| n | base (us) | A / base | B / base | B / A | gen2 per 1000 calls: base / A / B |
|---|---|---|---|---|---|
| 100 | 1.70 us (±2%) | 0.86 | 0.82 | 0.95 | 0.0 / 0.0 / 0.0 |
| 1000 | 16.23 us (±0%) | 0.85 | 0.81 | 0.95 | 0.0 / 0.0 / 0.0 |
| 10000 | 198.29 us (±4%) | 0.79 | 0.77 | 0.98 | 0.0 / 0.0 / 0.0 |
| 100000 | 2.02 ms (±5%) | 0.85 | 0.83 | 0.98 | 0.0 / 0.0 / 0.0 |
| 1000000 | 24.75 ms (±6%) | 0.90 | 0.88 | 0.98 | 0.0 / 0.0 / 0.0 |
temp-sub, int
| n | base (us) | A / base | B / base | B / A | gen2 per 1000 calls: base / A / B |
|---|---|---|---|---|---|
| 100 | 2.34 us (±15%) | 0.86 | 0.81 | 0.95 | 0.0 / 0.0 / 0.0 |
| 1000 | 16.82 us (±2%) | 1.01 | 1.04 | 1.02 | 0.0 / 0.0 / 0.0 |
| 10000 | 199.18 us (±16%) | 1.08 | 0.98 | 0.91 | 0.0 / 0.0 / 0.0 |
| 100000 | 2.75 ms (±38%) | 1.07 | 0.93 | 0.87 | 0.0 / 0.0 / 0.0 |
| 1000000 | 50.07 ms (±21%) | 0.90 | 0.89 | 0.99 | 0.0 / 0.0 / 0.0 |
temp-sub, string
| n | base (us) | A / base | B / base | B / A | gen2 per 1000 calls: base / A / B |
|---|---|---|---|---|---|
| 100 | 3.90 us (±4%) | 0.86 | 0.89 | 1.03 | 0.0 / 0.0 / 0.0 |
| 1000 | 37.86 us (±7%) | 0.86 | 0.85 | 0.99 | 0.0 / 0.0 / 0.0 |
| 10000 | 417.90 us (±4%) | 0.99 | 0.96 | 0.98 | 0.0 / 0.0 / 0.0 |
| 100000 | 5.77 ms (±5%) | 1.16 | 1.05 | 0.90 | 0.3 / 5.9 / 0.0 |
| 1000000 | 74.56 ms (±5%) | 0.95 | 0.90 | 0.94 | 0.0 / 0.0 / 0.0 |
temp-rev, int
| n | base (us) | A / base | B / base | B / A | gen2 per 1000 calls: base / A / B |
|---|---|---|---|---|---|
| 100 | 1.84 us (±4%) | 1.00 | 1.03 | 1.03 | 0.0 / 0.0 / 0.0 |
| 1000 | 17.81 us (±7%) | 1.01 | 0.96 | 0.95 | 0.0 / 0.0 / 0.0 |
| 10000 | 203.49 us (±11%) | 1.00 | 0.99 | 0.99 | 0.0 / 0.0 / 0.0 |
| 100000 | 2.57 ms (±3%) | 1.01 | 1.06 | 1.05 | 0.0 / 0.0 / 0.0 |
| 1000000 | 43.12 ms (±4%) | 1.01 | 0.99 | 0.98 | 0.0 / 0.0 / 0.0 |
temp-rev, string
| n | base (us) | A / base | B / base | B / A | gen2 per 1000 calls: base / A / B |
|---|---|---|---|---|---|
| 100 | 4.27 us (±2%) | 0.93 | 0.86 | 0.93 | 0.0 / 0.0 / 0.0 |
| 1000 | 39.91 us (±5%) | 0.84 | 0.90 | 1.08 | 0.0 / 0.0 / 0.0 |
| 10000 | 430.28 us (±0%) | 0.98 | 0.96 | 0.98 | 0.0 / 0.0 / 0.0 |
| 100000 | 6.39 ms (±8%) | 0.93 | 0.86 | 0.92 | 6.2 / 4.1 / 0.0 |
| 1000000 | 78.18 ms (±4%) | 0.96 | 0.91 | 0.94 | 0.0 / 0.0 / 0.0 |
temp-mapi, int
| n | base (us) | A / base | B / base | B / A | gen2 per 1000 calls: base / A / B |
|---|---|---|---|---|---|
| 100 | 2.30 us (±1%) | 1.00 | 0.97 | 0.96 | 0.0 / 0.0 / 0.0 |
| 1000 | 20.97 us (±10%) | 1.03 | 0.97 | 0.94 | 0.0 / 0.0 / 0.0 |
| 10000 | 238.53 us (±2%) | 1.04 | 1.07 | 1.02 | 0.0 / 0.0 / 0.0 |
| 100000 | 3.29 ms (±23%) | 1.03 | 0.98 | 0.96 | 0.0 / 0.0 / 0.0 |
| 1000000 | 48.05 ms (±4%) | 0.95 | 1.05 | 1.10 | 0.0 / 0.0 / 0.0 |
temp-mapi, string
| n | base (us) | A / base | B / base | B / A | gen2 per 1000 calls: base / A / B |
|---|---|---|---|---|---|
| 100 | 3.46 us (±7%) | 0.87 | 0.90 | 1.03 | 0.0 / 0.0 / 0.0 |
| 1000 | 32.31 us (±5%) | 0.95 | 0.89 | 0.94 | 0.0 / 0.0 / 0.0 |
| 10000 | 368.99 us (±5%) | 0.90 | 0.93 | 1.02 | 0.0 / 0.0 / 0.0 |
| 100000 | 6.02 ms (±5%) | 0.86 | 0.82 | 0.95 | 6.3 / 7.7 / 0.0 |
| 1000000 | 68.69 ms (±9%) | 0.94 | 0.94 | 1.01 | 0.0 / 0.0 / 0.0 |
temp-init, int
| n | base (us) | A / base | B / base | B / A | gen2 per 1000 calls: base / A / B |
|---|---|---|---|---|---|
| 100 | 2.95 us (±3%) | 1.03 | 1.00 | 0.98 | 0.0 / 0.0 / 0.0 |
| 1000 | 28.55 us (±7%) | 1.23 | 1.04 | 0.85 | 0.0 / 0.0 / 0.0 |
| 10000 | 323.75 us (±9%) | 0.97 | 0.98 | 1.01 | 0.0 / 0.0 / 0.0 |
| 100000 | 3.74 ms (±12%) | 0.99 | 0.90 | 0.91 | 0.0 / 0.0 / 0.0 |
| 1000000 | 37.59 ms (±3%) | 1.00 | 1.14 | 1.14 | 0.0 / 0.0 / 0.0 |
temp-init, string
| n | base (us) | A / base | B / base | B / A | gen2 per 1000 calls: base / A / B |
|---|---|---|---|---|---|
| 100 | 2.00 us (±6%) | 0.84 | 0.90 | 1.08 | 0.0 / 0.0 / 0.0 |
| 1000 | 20.17 us (±8%) | 0.78 | 0.80 | 1.03 | 0.0 / 0.0 / 0.0 |
| 10000 | 212.96 us (±4%) | 0.91 | 0.89 | 0.98 | 0.0 / 0.0 / 0.0 |
| 100000 | 3.49 ms (±4%) | 0.90 | 0.61 | 0.68 | 10.5 / 9.9 / 0.0 |
| 1000000 | 36.73 ms (±12%) | 0.92 | 0.72 | 0.78 | 32.3 / 34.9 / 0.0 |
temp-replicate, int
| n | base (us) | A / base | B / base | B / A | gen2 per 1000 calls: base / A / B |
|---|---|---|---|---|---|
| 100 | 1.12 us (±2%) | 1.08 | 1.20 | 1.10 | 0.0 / 0.0 / 0.0 |
| 1000 | 10.32 us (±19%) | 0.96 | 0.94 | 0.98 | 0.0 / 0.0 / 0.0 |
| 10000 | 126.40 us (±7%) | 1.01 | 0.97 | 0.96 | 0.0 / 0.0 / 0.0 |
| 100000 | 1.93 ms (±12%) | 1.06 | 0.75 | 0.71 | 5.8 / 5.3 / 0.0 |
| 1000000 | 23.42 ms (±4%) | 1.01 | 0.79 | 0.79 | 32.5 / 38.2 / 0.0 |
temp-replicate, string
| n | base (us) | A / base | B / base | B / A | gen2 per 1000 calls: base / A / B |
|---|---|---|---|---|---|
| 100 | 1.95 us (±1%) | 0.79 | 0.82 | 1.03 | 0.0 / 0.0 / 0.0 |
| 1000 | 20.21 us (±5%) | 0.75 | 0.75 | 1.00 | 0.0 / 0.0 / 0.0 |
| 10000 | 203.63 us (±18%) | 0.84 | 0.88 | 1.05 | 0.0 / 0.0 / 0.0 |
| 100000 | 3.25 ms (±1%) | 0.92 | 0.61 | 0.67 | 9.8 / 11.0 / 0.0 |
| 1000000 | 35.38 ms (±5%) | 0.96 | 0.70 | 0.72 | 33.7 / 45.9 / 0.0 |
Reading:
ofArray(thefreshtable): strings take 14 to 21 % less for 100 to 100 000 elements and 10 to 12 % less for a million; integers take the same time within the noise.- The first commit (
A) also helpsinit,sub,revandmapiof strings of up to 10 000 elements (0.78 to 0.99 of the time) andreplicateof strings withArray.Fill(0.75 to 0.84 for 100 to 10 000 elements, 0.92 to 0.96 above). Integers: no change.subof strings at 100 000 elements reads 1.16 forA; thereAhad 5.9 collections of the second generation per 1000 calls against 0.3 ofbasefor the same allocation, which is the uneven fall of the collections described in the first comment, and the neighbouring sizes read 0.99 and 0.95. - The last commit (
B, the pool) pays inreplicate(0.71 and 0.79 of the time ofAfor integers, 0.67 and 0.72 for strings of 100 000 and a million elements) and ininitof strings (0.68 and 0.78), where it also removes the collections of the second generation (5 to 46 per 1000 calls). Forrevandmapiof strings the gain is small (0.92 to 1.01), and forsuband the integerinit,mapiandrevit is inside the noise;initandmapiof a million integers read 1.14 and 1.10 against a scatter of ±12 and ±3 %, so the pool may cost a little there. A list of 100 strings pays for the call of the helper and its closure, up to 8 % of a call of 2 to 4 microseconds.scanandmapFolduse the same helper and were not measured separately. - Below 1024 elements
Ballocates an array of its own as before; from 1024 it rents. Between 1000 and 10 000 elementsB / Aranges from 0.85 to 1.08, inside the scatter of the replicates.
An exact array against a rented one, against the old code (protocol 1, 10 to 10 000 elements)
Copies of the algorithm as a script, relative to a copy of the old code in the same process: an array of the exact length allocated anew (exact), and an array rented from the pool (pool). This is what the choice between them rested on. Above 10 000 elements the table before is the evidence, because a new array above the 85 000-byte limit goes to the large object heap and its garbage causes the collections that the pool avoids.
Tables
sub, int
| n | exact | pool |
|---|---|---|
| 10 | 1.00 | 0.93 |
| 100 | 1.00 | 1.00 |
| 1 000 | 0.85 | 0.92 |
| 10 000 | 0.96 | 1.00 |
rev, int
| n | exact | pool |
|---|---|---|
| 10 | 0.81 | 0.84 |
| 100 | 0.90 | 0.95 |
| 1 000 | 0.83 | 0.85 |
| 10 000 | 0.93 | 0.85 |
mapi, int
| n | exact | pool |
|---|---|---|
| 10 | 0.91 | 0.89 |
| 100 | 0.94 | 0.93 |
| 1 000 | 0.89 | 0.85 |
| 10 000 | 0.98 | 1.00 |
init, int
| n | exact | pool |
|---|---|---|
| 10 | 1.00 | 0.86 |
| 100 | 1.00 | 0.96 |
| 1 000 | 0.82 | 0.81 |
| 10 000 | 1.00 | 1.00 |
replicate, int
| n | exact | pool |
|---|---|---|
| 10 | 0.74 | 0.68 |
| 100 | 0.73 | 0.85 |
| 1 000 | 0.74 | 0.78 |
| 10 000 | 0.88 | 0.91 |
sub, string
| n | exact | pool |
|---|---|---|
| 10 | 0.92 | 0.93 |
| 100 | 0.95 | 0.94 |
| 1 000 | 0.90 | 0.93 |
| 10 000 | 0.96 | 1.00 |
rev, string
| n | exact | pool |
|---|---|---|
| 10 | 0.90 | 0.93 |
| 100 | 0.92 | 0.95 |
| 1 000 | 0.86 | 0.91 |
| 10 000 | 1.00 | 1.00 |
mapi, string
| n | exact | pool |
|---|---|---|
| 10 | 0.90 | 0.91 |
| 100 | 1.00 | 1.02 |
| 1 000 | 0.92 | 1.00 |
| 10 000 | 0.93 | 0.92 |
init, string
| n | exact | pool |
|---|---|---|
| 10 | 0.80 | 0.85 |
| 100 | 0.85 | 0.82 |
| 1 000 | 0.82 | 0.88 |
| 10 000 | 0.83 | 0.90 |
replicate, string
| n | exact | pool |
|---|---|---|
| 10 | 0.74 | 0.79 |
| 100 | 0.76 | 0.78 |
| 1 000 | 0.75 | 0.85 |
| 10 000 | 0.80 | 0.82 |
unfold (protocol 1, 10 to 10 000 elements)
Variants of the buffer of unfold, where the number of elements is neither known nor bounded: C0 the ResizeArray with the span build, S1 and S2 chunks of 1024 and 8192 elements joined as subtrees, A2 the old code again. The chunks read 0.65 to 1.00 of the time in this range (A2 reads 0.88 to 1.02) and C0 reads 0.91 to 1.10. The chunks were not taken: the tree built from joined chunks has a slower indexer (see the shape table in the third comment), and the other variants gain nothing. unfold is unchanged.
int
| n | C0 | S1 | S2 | A2 |
|---|---|---|---|---|
| 10 | 1.00 | 0.92 | 0.86 | 1.01 |
| 100 | 1.00 | 1.00 | 0.90 | 1.00 |
| 1 000 | 0.91 | 0.87 | 0.82 | 1.02 |
| 10 000 | 1.06 | 0.98 | 1.00 | 1.00 |
string
| n | C0 | S1 | S2 | A2 |
|---|---|---|---|---|
| 10 | 1.00 | 0.77 | 0.81 | 1.00 |
| 100 | 1.00 | 1.00 | 1.00 | 1.00 |
| 1 000 | 1.10 | 0.98 | 0.93 | 1.00 |
| 10 000 | 1.00 | 0.65 | 0.75 | 0.88 |
🤖 Generated with Claude Code
Measurements 5 of 5: the other collectionsThe The struct enumerator against the interface (protocol 1, two runs of 15 rounds, light cells)Time of a walk through
Per element at 100 000 elements:
Reading:
|
| collection | elements | base build | changed build, run 1 | changed build, run 2 |
|---|---|---|---|---|
SortedSet |
string, 100 to 100 000 |
0.94 to 1.08 | 0.47 to 0.57 | 0.47 to 0.56 |
SortedSet |
int, 100 to 100 000 |
1.00 to 1.07 | 0.94 to 1.03 | 0.93 to 1.05 |
SortedMap |
string, string |
0.90 to 1.10 | 0.56 to 0.68 | 0.55 to 0.84 (one cell 1.11) |
SortedMap |
string, int |
0.94 to 1.06 | 0.62 to 0.66 | 0.61 to 0.66 |
SortedMap |
int, string |
0.98 to 1.10 | 0.60 to 0.69 | 0.62 to 0.66 |
SortedMap |
int, int |
0.95 to 1.07 | 0.90 to 1.13 | 0.96 to 1.09 |
The cost of the interface is the boxed enumerator: 64 bytes per call for the set, 56 for the map. The largest map (100 000 bindings) is the noisiest cell of the second run (0.82 and 0.84).
Every cell of the changed builds, run 1
SortedSet
| function, type, n | old loop | library / old [10th-90th] | old again / old |
|---|---|---|---|
exists, string, 100 |
1.74 us | 0.51 [0.45-0.63] | 0.93 [0.71-1.10] |
forall, string, 100 |
1.81 us | 0.53 [0.46-0.62] | 1.03 [0.91-1.20] |
exists, string, 1000 |
17.38 us | 0.51 [0.39-0.66] | 0.95 [0.71-1.14] |
forall, string, 1000 |
18.11 us | 0.49 [0.46-0.64] | 0.98 [0.91-1.24] |
exists, string, 10000 |
197.04 us | 0.49 [0.42-0.60] | 1.00 [0.78-1.08] |
forall, string, 10000 |
202.97 us | 0.47 [0.37-0.60] | 0.99 [0.77-1.35] |
exists, string, 100000 |
2.57 ms | 0.57 [0.45-0.81] | 1.03 [0.80-1.37] |
forall, string, 100000 |
2.39 ms | 0.57 [0.44-0.68] | 0.95 [0.83-1.24] |
exists, int, 100 |
0.51 us | 0.94 [0.82-1.21] | 1.00 [0.83-1.21] |
forall, int, 100 |
0.51 us | 1.03 [0.74-1.14] | 1.01 [0.78-1.31] |
exists, int, 1000 |
5.78 us | 0.97 [0.55-1.16] | 0.95 [0.79-1.10] |
forall, int, 1000 |
5.22 us | 1.03 [0.92-1.12] | 0.95 [0.85-1.23] |
exists, int, 10000 |
65.74 us | 0.96 [0.77-1.17] | 0.93 [0.79-1.21] |
forall, int, 10000 |
58.64 us | 1.01 [0.90-1.18] | 0.97 [0.84-1.20] |
exists, int, 100000 |
772.86 us | 1.02 [0.86-1.22] | 1.05 [0.88-1.28] |
forall, int, 100000 |
811.97 us | 0.98 [0.87-1.21] | 1.01 [0.89-1.22] |
SortedMap
| function, key and value, n | old loop | library / old [10th-90th] | old again / old |
|---|---|---|---|
exists, string, string, 100 |
2.43 us | 0.56 [0.50-0.74] | 0.91 [0.66-1.10] |
forall, string, string, 100 |
2.37 us | 0.58 [0.44-0.67] | 0.98 [0.70-1.11] |
exists, string, string, 1000 |
23.50 us | 0.62 [0.44-0.70] | 1.09 [0.79-1.25] |
forall, string, string, 1000 |
29.86 us | 0.56 [0.44-0.60] | 0.96 [0.85-1.14] |
exists, string, string, 10000 |
332.78 us | 0.63 [0.48-0.81] | 1.08 [0.77-1.22] |
forall, string, string, 10000 |
265.38 us | 0.63 [0.47-0.73] | 0.99 [0.85-1.13] |
exists, string, string, 100000 |
2.79 ms | 0.68 [0.49-0.82] | 1.17 [0.86-1.47] |
forall, string, string, 100000 |
3.21 ms | 0.68 [0.51-0.87] | 1.06 [0.69-1.31] |
exists, int, int, 100 |
1.02 us | 0.90 [0.77-0.96] | 0.99 [0.87-1.16] |
forall, int, int, 100 |
0.95 us | 0.96 [0.84-1.13] | 1.00 [0.88-1.12] |
exists, int, int, 1000 |
10.18 us | 0.96 [0.83-1.10] | 1.03 [0.80-1.25] |
forall, int, int, 1000 |
9.75 us | 1.01 [0.93-1.08] | 1.05 [0.87-1.26] |
exists, int, int, 10000 |
100.84 us | 1.03 [0.83-1.42] | 0.99 [0.86-1.14] |
forall, int, int, 10000 |
107.00 us | 1.04 [0.87-1.42] | 1.18 [0.82-1.32] |
exists, int, int, 100000 |
1.31 ms | 0.96 [0.85-1.08] | 0.93 [0.82-1.06] |
forall, int, int, 100000 |
2.22 ms | 1.13 [0.90-1.34] | 1.09 [0.98-1.18] |
exists, string, int, 100 |
3.93 us | 0.62 [0.56-0.75] | 0.99 [0.88-1.17] |
forall, string, int, 100 |
3.86 us | 0.62 [0.52-0.77] | 1.01 [0.87-1.14] |
exists, string, int, 1000 |
28.25 us | 0.65 [0.55-0.79] | 1.02 [0.86-1.24] |
forall, string, int, 1000 |
29.97 us | 0.65 [0.47-0.73] | 0.94 [0.80-1.11] |
exists, string, int, 10000 |
231.23 us | 0.64 [0.50-0.75] | 1.04 [0.92-1.16] |
forall, string, int, 10000 |
224.15 us | 0.63 [0.56-0.72] | 1.06 [0.90-1.17] |
exists, string, int, 100000 |
2.78 ms | 0.62 [0.49-0.71] | 0.99 [0.70-1.22] |
forall, string, int, 100000 |
2.88 ms | 0.66 [0.59-0.76] | 0.94 [0.72-1.16] |
exists, int, string, 100 |
2.19 us | 0.62 [0.54-0.72] | 1.02 [0.85-1.32] |
forall, int, string, 100 |
2.13 us | 0.63 [0.56-0.78] | 1.01 [0.90-1.15] |
exists, int, string, 1000 |
22.23 us | 0.60 [0.51-0.65] | 0.98 [0.81-1.20] |
forall, int, string, 1000 |
21.09 us | 0.67 [0.59-0.78] | 0.99 [0.86-1.14] |
exists, int, string, 10000 |
225.21 us | 0.63 [0.49-0.71] | 0.98 [0.86-1.27] |
forall, int, string, 10000 |
222.56 us | 0.61 [0.52-0.70] | 1.02 [0.67-1.13] |
exists, int, string, 100000 |
2.40 ms | 0.69 [0.55-0.81] | 1.03 [0.85-1.19] |
forall, int, string, 100000 |
2.25 ms | 0.68 [0.59-0.79] | 1.05 [0.77-1.09] |
🤖 Generated with Claude Code
IndexedList for large lists and reference types, and a linear ofListofArray builds the tree from a span, and the temporary arrays of long lists come from the pool
Proposed Changes
Builds on #49 (
perf/indexed-list: a linearofList, interface walks, a pooled buffer inchoose), which is below this pull request in the stack and builds on #31. HereofArrayand every function that builds its result from a fresh array useImmutableList.Create(ReadOnlySpan),replicatefills its array withArray.Fill, andinit,replicate,sub,rev,mapi,scanandmapFoldtake their temporary array from the pool for long lists. No public signature changes.The measurements are in the comments below. The fourth is about this pull request; the first (method, and which numbers to trust), the second, the third and the fifth were made for the whole stack and are linked from #49. Every number in the commit messages and in this description comes from them.
4d00ab3perf:ofArraybuilds the tree from a span, andreplicatefills its array withArray.FillofFreshArrayandofArrayuseImmutableList.Create(ReadOnlySpan); the functions of theTuplenamespace callIndexedList.ofArrayreplicateof strings 0.75 to 0.84; integers unchanged1d985f9perf: the temporary arrays ofinit,replicate,sub,rev,mapi,scanandmapFoldcome from the pool for long listsBuffers.ofFilledandofFilledWithreplicate0.71 to 0.79 (integers) and 0.67 to 0.72 (strings),initof strings 0.68 to 0.78, the rest inside the noise. Separate so that it can be dropped on its ownRatios 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
A performance change that keeps every result and every signature.
Checklist
ofArrayandreplicate; the helper around the length where the pool startsCollectorsdoFurther comments
The pool for the temporary arrays is the part to judge. It helps
replicate(integers and strings) andinitof strings, where the new array is garbage of the large object heap and causes collections of the second generation (4 to 46 per 1000 calls, gone with the pool). Forrevandmapiof strings the gain is small, and forsuband the integerinit,mapiandrevit is inside the noise;initandmapiof a million integers read 1.14 and 1.10, so the pool may cost a little there. A list of 100 strings pays for the helper and its closure, up to 8 % of a call of 2 to 4 µs. The commit is separate so that it can be dropped without touching the span build.What is not here.
scanandmapFoldtake their temporary array from the pool in the last commit, but were not measured on their own. The benchmark scripts and the raw tables are not part of the pull request.🤖 Generated with Claude Code