Fsharp: Needless allocations on invocation of array of functions

Created on 28 Sep 2017  路  5Comments  路  Source: dotnet/fsharp

Using an indexed for loop to iterate through an array of functions and invoke them causes a closure allocation for each invocation.

Repro steps

open System

let exampleFunc v = v + 2

let runAll(fArr:(int -> int) array) x =
    let mutable n = 0
    for i = 0 to fArr.Length - 1 do
        n <- n + fArr.[i] x
    n

[<EntryPoint>]
let main argv = 
    let mutable n = 0
    let fArr = Array.create 10000 exampleFunc
    let runAllClosure = runAll fArr
    // snapshot here
    Console.ReadLine() |> ignore
    for i = 1 to 7 do
        n <- n + runAllClosure i
    // snapshot here
    Console.ReadLine() |> ignore
    Console.WriteLine("N is {0}", n)
    printfn "%A" argv
    0

Expected behavior

This shouldn't allocate.

Actual behavior

This allocates a closure for each item in the array, whenever runAllClosure is called!
70,000 objects. (7 calls * 1000 size of array)!

This is probably automatically capturing the index for whatever reason, and thus generating a closure.

Known workarounds

Swapping to an iterator avoids the allocations.

let runAll(fArr:(int -> int) array) x =
    let mutable n = 0
    for f in fArr do
        n <- n + f x
    n

Related information

  • Win10 x64
  • Current release version (not the nightlies)
  • .NET 4.6.2, Release Build, MSIL
  • VS 2017
  • Profiled using dotMemory
Area-Compiler Severity-Medium Tenet-Performance bug

Most helpful comment

I believe I've got this down to

    let arr = [|id|]
    let b = (arr.[1]) 2
    let a = arr.[1] 1

compiling to

        int num = array2[1].Invoke(2);
        int num2 = FSharpFunc<FSharpFunc<int, int>[], int>.InvokeFast(new a@7(), array2, 1, 1);

The TypedTree.Expr after optimisation are: App(App(get, [arr, 1]), [2]) and App(App(get, []), [arr, 1, 1])

Adding parentheses appears to be enough to avoid this?

All 5 comments

what happens if you don't use the index like:

for i = 0 to fArr.Length - 1 do
    n <- n + fArr.[0] x

stil allocating?

@forki - still allocates.

@varon I agree this is a bug. We are getting intermediate code roughly equivalent to InvokeFast2((fun fArr i -> fArr.[i]),fArr,i,x). I don't understand why as yet

THis also avoids the allocations:

let runAll(fArr:(int -> int) array) x =
    let mutable n = 0
    for i = 0 to fArr.Length - 1 do
        let f = fArr.[i]
        n <- n + f x
    n

I believe I've got this down to

    let arr = [|id|]
    let b = (arr.[1]) 2
    let a = arr.[1] 1

compiling to

        int num = array2[1].Invoke(2);
        int num2 = FSharpFunc<FSharpFunc<int, int>[], int>.InvokeFast(new a@7(), array2, 1, 1);

The TypedTree.Expr after optimisation are: App(App(get, [arr, 1]), [2]) and App(App(get, []), [arr, 1, 1])

Adding parentheses appears to be enough to avoid this?

Was this page helpful?
0 / 5 - 0 ratings