From 48ab97dce292e6c1a4fda4c53a32bf76381b13d7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:14:46 +0000 Subject: [PATCH] perf: direct loop in toResizeArrayAsync; simplify tryItem loop toResizeArrayAsync previously went through the iter function with a SimpleAction DU wrapper, causing a lambda closure allocation and discriminated-union wrapping on every call. The new direct loop eliminates these allocations and removes an extra layer of indirection. This benefits toArrayAsync, toListAsync, toResizeArrayAsync, and toIListAsync which all route through toResizeArrayAsync. tryItem previously used a while-loop with condition idx <= index and an inner if idx = index check on every iteration. The refactored version advances with idx < index and captures the current element after the loop, removing the redundant inner comparison from the hot path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- release-notes.txt | 2 ++ src/FSharp.Control.TaskSeq/TaskSeqInternal.fs | 26 +++++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/release-notes.txt b/release-notes.txt index d07e4fc..cfe2d7d 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -3,6 +3,8 @@ Release notes: 1.0.0 - adds taskSeqDynamic computation expression and TaskSeqDynamic/TaskSeqDynamicInfo types for dynamic (FSI-compatible) resumable code, fixing issue where taskSeq would raise NotImplementedException in F# Interactive, #246 + - perf: toResizeArrayAsync (and therefore toArrayAsync, toListAsync, toResizeArrayAsync, toIListAsync) uses a direct loop instead of going through iter, avoiding a lambda and DU allocation per call + - perf: tryItem uses a simpler loop that skips the redundant inner index check on every iteration - perf: TaskSeq.chunkBy and chunkByAsync reuse the ResizeArray buffer between chunks, reducing allocations on sequences with many chunk boundaries - fixes: TaskSeq.insertAt, insertManyAt, removeAt, removeManyAt, updateAt now raise ArgumentNullException (not NullReferenceException) when given a null source; insertManyAt also validates the values argument - refactor: simplify lengthBy and lengthBeforeMax to use while! and remove the redundant mutable 'go' and initial MoveNextAsync diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 261dbba..b6f77a3 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -532,12 +532,16 @@ module internal TaskSeqInternal = yield result } - let toResizeArrayAsync source = + let toResizeArrayAsync (source: TaskSeq<'T>) = checkNonNull (nameof source) source task { - let res = ResizeArray() - do! source |> iter (SimpleAction(fun item -> res.Add item)) + let res = ResizeArray<'T>() + use e = source.GetAsyncEnumerator CancellationToken.None + + while! e.MoveNextAsync() do + res.Add e.Current + return res } @@ -915,14 +919,14 @@ module internal TaskSeqInternal = let! step = e.MoveNextAsync() go <- step - while go && idx <= index do - if idx = index then - foundItem <- Some e.Current - go <- false - else - let! step = e.MoveNextAsync() - go <- step - idx <- idx + 1 + // advance past the first `index` elements, then capture the current element + while go && idx < index do + let! step = e.MoveNextAsync() + go <- step + idx <- idx + 1 + + if go then + foundItem <- Some e.Current return foundItem }