dotnet / dotnet/fsharp

While loop pattern in Async builder allocates a lot

Open
#8,668 5 comments 20 reactions 0 assignees View on GitHub
Area-Async Feature Request
Dominant language
F#
Stars
4.3k
Forks
876
Avg merge
4d 11h
Merged PRs (30d)
131

Description

**Repro steps**

The problem is in this code pattern:

```
async {
let mutable i = 0
while i < "some length" do
i <- i + 1
return i
}
```

Benchmark code:

```
[]
[]
[]
[]
type Benchs() =

[]
member val Length = 0 with get, set

[]
member x.Run() =
async {
let mutable i = 0
while i < x.Length do
i <- i + 1
return i
} |> Async.StartAsTask
```

Result:

``` ini

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
Intel Core i7-3770K CPU 3.50GHz (Ivy Bridge), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT DEBUG
Job-VRTOUB : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT

Runtime=.NET Core 3.1 Server=True IterationCount=5
LaunchCount=3 WarmupCount=3

```
| Method | Length | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|------- |------- |-----------:|-----------:|-----------:|--------:|------:|------:|----------:|
| **Run** | **100** | **8.443 us** | **1.0161 us** | **0.9504 us** | **0.2136** | **-** | **-** | **7.59 KB** |
| **Run** | **200** | **19.149 us** | **1.5007 us** | **1.4037 us** | **0.3662** | **-** | **-** | **13.94 KB** |
| **Run** | **300** | **21.909 us** | **3.3153 us** | **3.1011 us** | **0.5493** | **-** | **-** | **20.25 KB** |
| **Run** | **400** | **29.473 us** | **0.5453 us** | **0.5101 us** | **0.7324** | **-** | **-** | **26.55 KB** |
| **Run** | **500** | **34.433 us** | **1.2715 us** | **1.1893 us** | **0.9155** | **-** | **-** | **32.86 KB** |
| **Run** | **1000** | **59.594 us** | **2.5140 us** | **2.3516 us** | **1.7090** | **-** | **-** | **64.38 KB** |
| **Run** | **2000** | **104.767 us** | **4.0733 us** | **3.8102 us** | **3.5400** | **-** | **-** | **127.43 KB** |
| **Run** | **3000** | **154.497 us** | **2.4013 us** | **2.2462 us** | **5.1270** | **-** | **-** | **190.48 KB** |
| **Run** | **10000** | **484.288 us** | **19.7594 us** | **18.4830 us** | **17.0898** | **-** | **-** | **631.8 KB** |

Essentially the problem is that the loop internally turns into this:

![image](https://user-images.githubusercontent.com/10695322/76064591-0a8ffe80-5f9b-11ea-87fd-778fd5c23c8e.png)

**Expected behavior**

Allocations shouldn't depend(?) on the number of loops.

**Actual behavior**

Allocations depend on the number of loops.

**Known workarounds**

Using recursion:

```
async {
let mutable i = 0
let rec while' () =
if i = "some length"
then i
else
i <- i + 1
while' ()
return while' ()
}
```

**Related information**

Using `TaskBuilder.fs` helps, but not that much:

https://gist.github.com/grishace/83f540cb299867e94145551931fcbcb1

.NET Core 3.1

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.