Skip to content

While loop pattern in Async builder allocates a lot #8668

Open
@Liminiens

Description

@Liminiens

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:

[<SimpleJob(runtimeMoniker = RuntimeMoniker.NetCoreApp31, launchCount = 3, warmupCount = 3, targetCount = 5)>]
[<GcServer(true)>]
[<MemoryDiagnoser>]
[<MarkdownExporterAttribute.GitHub>]
type Benchs() =

  [<Params(100, 200, 300, 400, 500, 1000, 2000, 3000, 10000)>]
  member val Length = 0 with get, set

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

Result:

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

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    New

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions