Skip to content

Perf/improvements#364

Merged
TheAngryByrd merged 8 commits into
masterfrom
perf/improvements
May 17, 2026
Merged

Perf/improvements#364
TheAngryByrd merged 8 commits into
masterfrom
perf/improvements

Conversation

@TheAngryByrd

@TheAngryByrd TheAngryByrd commented May 17, 2026

Copy link
Copy Markdown
Collaborator

Proposed Changes

This PR optimizes several hot-path helpers without changing public API surface or short-circuit semantics.

Changes include:

  • Reworks array traversal helpers to avoid repeated Array.head, Array.skip, and Array.append allocations.
  • Optimizes Task.apply, Task.map2, and Task.map3 with direct task workflows.
  • Uses existing singleton fast paths for TaskOption, TaskValueOption, and JobOption none/some construction paths.
  • Avoids converting lists to arrays in List.traverseTaskResultM.
  • Adds BenchmarkDotNet coverage for the optimized paths.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist

  • Build and tests pass locally
  • I have added tests that prove my fix is effective or that my feature works (if appropriate)
  • I have added necessary documentation (if appropriate)

Further comments

This is a performance-only change. The array traversal changes replace recursive slicing/appending with single-pass accumulation,
which removes the main allocation source and substantially improves traversal throughput.

Benchmark highlights:

Benchmark highlights:

Benchmark Time Time Ratio Allocation Allocation Ratio
Task.map2 68.737 ns -> 9.065 ns 7.58x faster 320 B -> 0 B eliminated
Task.map3 107.769 ns -> 11.006 ns 6.25x faster 568 B -> 0 B eliminated
Array.traverseResultM all-Ok 280,063.5 ns -> 1,172.0 ns 239.0x faster 4046.9 KB -> 7.89 KB 513.0x less
Array.traverseResultM early-error 141,281.1 ns -> 569.9 ns 247.9x faster 2023.52 KB -> 3.96 KB 511.0x less
Array.traverseResultA all-Ok 189,879.6 ns -> 1,391.6 ns 136.4x faster 3992.21 KB -> 7.92 KB 504.1x less
Array.traverseOptionM all-Some 196,828.6 ns -> 2,716.8 ns 72.4x faster 4039.13 KB -> 31.35 KB 128.8x less
Array.traverseVOptionM all-Some 192,376.5 ns -> 1,151.5 ns 167.1x faster 3992.21 KB -> 7.89 KB 506.0x less
Array.traverseAsyncResultM all-Ok 633.57 us -> 142.59 us 4.4x faster 4924.51 KB -> 385.37 KB 12.8x less
Array.traverseAsyncOptionM all-Some 561.28 us -> 101.30 us 5.5x faster 4932.26 KB -> 393.15 KB 12.5x less
TaskOption.apply Some/Some 70.30 ns -> 62.13 ns 1.1x faster 240 B -> 240 B unchanged
TaskValueOption.apply ValueSome/ValueSome 61.09 ns -> 57.15 ns 1.1x faster 216 B -> 216 B unchanged
JobOption.apply Some/Some 103.96 ns -> 92.35 ns 1.1x faster 352 B -> 328 B 1.1x less
JobOption.bind None 61.89 ns -> 54.99 ns 1.1x faster 192 B -> 168 B 1.1x less
List.traverseTaskResultM all-Ok 27.357 us -> 27.306 us effectively unchanged 144.71 KB -> 140.8 KB 1.03x less
List.traverseTaskResultM early-error 11.861 us -> 9.339 us 1.3x faster 58.85 KB -> 54.95 KB 1.1x less

@TheAngryByrd TheAngryByrd requested a review from Copilot May 17, 2026 15:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes several core traversal and task helper functions in FsToolkit.ErrorHandling to reduce allocations and improve throughput on hot paths, while also adding benchmarks to measure the gains.

Changes:

  • Reimplements several Array.traverse* helpers using single-pass loops with ResizeArray accumulation instead of recursive slicing/appending.
  • Optimizes Task.apply, Task.map2, and Task.map3 by using direct task { ... } workflows.
  • Reworks TaskOption / TaskValueOption / JobOption singleton/none paths to use existing immediate constructors, plus updates List.traverseTaskResultM to avoid list-to-array conversion.
  • Adds BenchmarkDotNet coverage for the optimized paths.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/FsToolkit.ErrorHandling/TaskValueOption.fs Uses Task.singleton fast paths for ValueNone / ValueSome construction.
src/FsToolkit.ErrorHandling/TaskOption.fs Uses Task.singleton fast paths for None / Some construction.
src/FsToolkit.ErrorHandling/Task.fs Rewrites apply/map2/map3 to direct task workflows to reduce overhead.
src/FsToolkit.ErrorHandling/List.fs Updates traverseTaskResultM to traverse lists without converting to arrays.
src/FsToolkit.ErrorHandling/Array.fs Replaces recursive traversal helpers with iterative implementations to reduce allocations and recursion overhead.
src/FsToolkit.ErrorHandling.JobResult/JobOption.fs Uses Job.result for None/Some construction fast paths.
benchmarks/Benchmarks.fs Adds/extends benchmarks for the updated traversal/task helpers.
Comments suppressed due to low confidence (3)

src/FsToolkit.ErrorHandling/Array.fs:83

  • traverseValidationA allocates an errors ResizeArray up-front even when the traversal completes successfully. Since the success path returns only oks, consider deferring creation of the error buffer until the first Error (or pre-sizing appropriately) to avoid an extra allocation in the all-Ok hot path.
        for x in xs do
            match f x with
            | Ok value when ok -> oks.Add value
            | Ok _ -> ()

src/FsToolkit.ErrorHandling/Array.fs:101

  • traverseAsyncResultA allocates the errors ResizeArray even when all results are Ok, which adds avoidable overhead in the success case. Consider lazily allocating the error buffer only after the first Error is observed (or pre-sizing it) to keep the hot path lean.
            for x in xs do
                let! result = f x

                match result with

src/FsToolkit.ErrorHandling/Array.fs:189

  • The XML docs for traverseVOptionM refer to the input as a “list” and describe the result as an “Option monad”, but the function takes an array and returns a voption. Please adjust the doc text to match the actual array-based API and return type.
        let results = ResizeArray<'okOutput>(xs.Length)
        let mutable index = 0
        let mutable ok = true

        while ok
              && index < xs.Length do
            match f xs[index] with
            | ValueSome value ->

Comment thread src/FsToolkit.ErrorHandling/Array.fs
Comment thread src/FsToolkit.ErrorHandling/Array.fs
Comment thread src/FsToolkit.ErrorHandling/List.fs
@TheAngryByrd TheAngryByrd merged commit 50fa073 into master May 17, 2026
26 checks passed
TheAngryByrd added a commit that referenced this pull request Jun 2, 2026
- BREAKING: [Move `ValueTaskValueOption` to the IcedTasks package](#363) Credits @TheAngryByrd
- This was misplaced originally as it depends on IcedTasks since it's using valueTasks. If you were using this before, add
FsToolkit.ErrorHandling.IcedTasks as a dependency.
- [Add `partitionResults` with single-pass implementations for Array, List, and Seq](#359) Credits @bpe-incom
- [Fix exception handling in task dynamic invocation](#360) Credits Eugene Auduchinok
- [Improve performance of task applicative helpers, array traversal helpers, option workflows, and list task traversal](#364) Credits @TheAngryByrd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants