Skip to content

Commit 95e7838

Browse files
perf: optimize windowed ring-buffer and toResizeArrayAsync
- windowed: replace integer modulo (count % windowSize) in the hot loop with explicit circular-buffer pointer arithmetic (increment + reset at boundary). Modulo is typically 5-10x slower than a conditional increment on modern CPUs. A separate 'writePos' mutable tracks the next write slot, and also serves directly as the 'start' index for the output blit, removing the second modulo that was needed before. - toResizeArrayAsync: rewrite from 'iter (SimpleAction (fun item -> res.Add item))' to a direct 'while! e.MoveNextAsync() do res.Add e.Current' loop. The old form allocated a closure that captured 'res', wrapped it in a struct-DU case, and dispatched through a pattern match inside 'iter'. The new form has no extra allocation and no indirection. All 5013 existing tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6fd41af commit 95e7838

File tree

2 files changed

+21
-8
lines changed

2 files changed

+21
-8
lines changed

release-notes.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Release notes:
33

44
1.0.0
5+
- perf: optimize TaskSeq.windowed to avoid integer modulo in hot loop, using explicit circular buffer pointer arithmetic
6+
- perf: optimize toResizeArrayAsync (internal) to use a direct loop instead of routing through iter, eliminating one closure allocation per call
57
- adds TaskSeq.distinctUntilChangedWith and TaskSeq.distinctUntilChangedWithAsync, #345
68
- adds TaskSeq.withCancellation, #167
79
- adds TaskSeq.replicateInfinite, replicateInfiniteAsync, replicateUntilNoneAsync, #345

src/FSharp.Control.TaskSeq/TaskSeqInternal.fs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -546,12 +546,16 @@ module internal TaskSeqInternal =
546546
yield result
547547
}
548548

549-
let toResizeArrayAsync source =
549+
let toResizeArrayAsync (source: TaskSeq<_>) =
550550
checkNonNull (nameof source) source
551551

552552
task {
553553
let res = ResizeArray()
554-
do! source |> iter (SimpleAction(fun item -> res.Add item))
554+
use e = source.GetAsyncEnumerator CancellationToken.None
555+
556+
while! e.MoveNextAsync() do
557+
res.Add e.Current
558+
555559
return res
556560
}
557561

@@ -1804,24 +1808,31 @@ module internal TaskSeqInternal =
18041808

18051809
taskSeq {
18061810
// Ring buffer: arr holds elements in circular order.
1807-
// 'count' tracks total elements seen; count % windowSize is the next write position.
1811+
// 'writePos' tracks the next write position (= count % windowSize, but avoiding modulo in the hot loop).
1812+
// 'count' tracks total elements seen.
18081813
let arr = Array.zeroCreate windowSize
18091814
let mutable count = 0
1815+
let mutable writePos = 0
18101816

18111817
for item in source do
1812-
arr.[count % windowSize] <- item
1818+
arr.[writePos] <- item
1819+
writePos <- writePos + 1
1820+
1821+
if writePos = windowSize then
1822+
writePos <- 0
1823+
18131824
count <- count + 1
18141825

18151826
if count >= windowSize then
18161827
// Copy ring buffer in source order into a fresh array.
1828+
// 'writePos' is now the oldest element's index (we just wrapped or are at the start).
18171829
let result = Array.zeroCreate windowSize
1818-
let start = count % windowSize // index of oldest element in the ring
18191830

1820-
if start = 0 then
1831+
if writePos = 0 then
18211832
Array.blit arr 0 result 0 windowSize
18221833
else
1823-
Array.blit arr start result 0 (windowSize - start)
1824-
Array.blit arr 0 result (windowSize - start) start
1834+
Array.blit arr writePos result 0 (windowSize - writePos)
1835+
Array.blit arr 0 result (windowSize - writePos) writePos
18251836

18261837
yield result
18271838
}

0 commit comments

Comments
 (0)