Perf/improvements#364
Merged
Merged
Conversation
There was a problem hiding this comment.
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 withResizeArrayaccumulation instead of recursive slicing/appending. - Optimizes
Task.apply,Task.map2, andTask.map3by using directtask { ... }workflows. - Reworks
TaskOption/TaskValueOption/JobOptionsingleton/none paths to use existing immediate constructors, plus updatesList.traverseTaskResultMto 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
traverseValidationAallocates anerrorsResizeArray up-front even when the traversal completes successfully. Since the success path returns onlyoks, consider deferring creation of the error buffer until the firstError(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
traverseAsyncResultAallocates theerrorsResizeArray even when all results areOk, which adds avoidable overhead in the success case. Consider lazily allocating the error buffer only after the firstErroris 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
traverseVOptionMrefer to the input as a “list” and describe the result as an “Option monad”, but the function takes an array and returns avoption. 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 ->
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
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
This PR optimizes several hot-path helpers without changing public API surface or short-circuit semantics.
Changes include:
Array.head,Array.skip, andArray.appendallocations.Task.apply,Task.map2, andTask.map3with direct task workflows.TaskOption,TaskValueOption, andJobOptionnone/some construction paths.List.traverseTaskResultM.Types of changes
Checklist
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:
Task.map268.737 ns->9.065 ns7.58x faster320 B->0 BTask.map3107.769 ns->11.006 ns6.25x faster568 B->0 BArray.traverseResultMall-Ok280,063.5 ns->1,172.0 ns239.0x faster4046.9 KB->7.89 KB513.0x lessArray.traverseResultMearly-error141,281.1 ns->569.9 ns247.9x faster2023.52 KB->3.96 KB511.0x lessArray.traverseResultAall-Ok189,879.6 ns->1,391.6 ns136.4x faster3992.21 KB->7.92 KB504.1x lessArray.traverseOptionMall-Some196,828.6 ns->2,716.8 ns72.4x faster4039.13 KB->31.35 KB128.8x lessArray.traverseVOptionMall-Some192,376.5 ns->1,151.5 ns167.1x faster3992.21 KB->7.89 KB506.0x lessArray.traverseAsyncResultMall-Ok633.57 us->142.59 us4.4x faster4924.51 KB->385.37 KB12.8x lessArray.traverseAsyncOptionMall-Some561.28 us->101.30 us5.5x faster4932.26 KB->393.15 KB12.5x lessTaskOption.applySome/Some70.30 ns->62.13 ns1.1x faster240 B->240 BTaskValueOption.applyValueSome/ValueSome61.09 ns->57.15 ns1.1x faster216 B->216 BJobOption.applySome/Some103.96 ns->92.35 ns1.1x faster352 B->328 B1.1x lessJobOption.bindNone61.89 ns->54.99 ns1.1x faster192 B->168 B1.1x lessList.traverseTaskResultMall-Ok27.357 us->27.306 us144.71 KB->140.8 KB1.03x lessList.traverseTaskResultMearly-error11.861 us->9.339 us1.3x faster58.85 KB->54.95 KB1.1x less