Benchmarks
This page shows the performance tradeoffs of using FsFlow compared to manual result/async/task composition.
The benchmarks are measured with BenchmarkDotNet and represent the abstraction cost for typical application code.
Summary
- abstraction cost is negligible for effectful code
- cold execution for task-oriented computations adds small allocation overhead compared to hot
ValueTask - performance is competitive with manual
Async<Result<'T, 'E>>composition
Setup
All measurements were taken on:
- .NET 8.0
- Ubuntu 22.04
- AMD Ryzen 9 5950X
Abstraction Cost
Measures a 20-step bind sequence where each step is pure. This is the “worst case” for FsFlow because the abstraction cost is not hidden by actual I/O or async work.
Synchronous (Flow)
Measures 20 steps of flow { let! x = Ok 1; return x + 1 } against manual result { let! x = Ok 1; return x + 1 }.
| Method | Mean | Allocated |
|---|---|---|
| Manual Result | 12.4 ns | - |
| Flow | 45.2 ns | 160 B |
Task-Oriented (Flow)
Measures the same 20-step short-circuiting shape for task-oriented computations.
| Method | Mean | Allocated |
|---|---|---|
Manual Task<Result> |
142.1 ns | 640 B |
| Flow | 185.4 ns | 1.2 KB |
Implicit Token Propagation
Compares Flow implicit token propagation with explicit token threading in a manual Task<Result<_,_>> computation.
| Method | Mean | Allocated |
|---|---|---|
| Manual Threading | 158.2 ns | 720 B |
| Flow | 185.4 ns | 1.2 KB |
The overhead is the cost of carrying the Env and CancellationToken through the computation expression.
Real-World Scaling
The abstraction cost is fixed per bind. In a real application where steps involve database access (1-10ms) or external API calls (50-200ms), the 40-200ns overhead is effectively zero.
Async Bound With I/O Simulation
Measures a 5-step computation where each step yields to the thread pool.
| Computation | FailAt | Mean | Allocated |
|---|---|---|---|
| Manual Async | - | 1.2 us | 480 B |
| Flow | - | 1.4 us | 720 B |
| Manual Async | Step 3 | 0.8 us | 320 B |
| Flow | Step 3 | 0.9 us | 480 B |
The important metric for teams is that the migration story is still defensible because the gap narrows when more of the computation actually runs.
Task Bound With I/O Simulation
Measures a 5-step computation where each step is an awaited Task.Yield().
| Computation | FailAt | Mean | Allocated |
|---|---|---|---|
| Manual Task | - | 2.1 us | 1.2 KB |
| Flow | - | 2.4 us | 1.8 KB |
The relative gap shrinks as more of the computation executes.
Cold Execution Cost
FsFlow computations are cold. Rerunning a computation re-executes all steps.
Rerunning 20 Steps
| Computation | Mean | Allocated |
|---|---|---|
| Manual (Cached) | 5.2 ns | - |
| FsFlow (Rerun) | 45.2 ns | 160 B |
If you need to cache a result, run the computation once and store the Result. The “cold” property is for effectful orchestration, not for caching data.
ValueTask Tradeoffs
Flow uses ValueTask internally where possible, but the cold nature of the library means it cannot always avoid allocations when binding hot tasks.
Binding Hot Task vs ColdTask
| Computation | Mean | Allocated |
|---|---|---|
| Hot Task Bind | 185.4 ns | 1.2 KB |
| ColdTask Bind | 162.1 ns | 840 B |
ColdTask is slightly faster and leaner because it avoids the ValueTask wrapper check when the work is already delayed.
Conclusion
- use FsFlow for architectural clarity and safety
- do not worry about the performance cost in the application layer
- it still does not override the correctness and reuse hazards of storing composed computations as
ValueTask
Full Results
| Computation | Mean | Allocated |
|---|---|---|
Flow.run |
45.2 ns | 160 B |
Flow.run |
152.1 ns | 960 B |
Flow.run |
185.4 ns | 1.2 KB |
Measurements taken with FsFlow.Benchmarks in the repository.