Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unnecessary allocation when calling functions stored in arrays. #5634

Closed
TobyShaw opened this issue Sep 13, 2018 · 2 comments
Closed

Unnecessary allocation when calling functions stored in arrays. #5634

TobyShaw opened this issue Sep 13, 2018 · 2 comments

Comments

@TobyShaw
Copy link

We noticed an unnecessary allocation which occurs when calling a (unit -> unit) function obtained from an array. The allocation appears to go away when the action of getting from the array is abstracted into a helper function. The allocation does not occur when invoking a System.Action.

Repro steps

Compile the following code on version 4.1 of the F# compiler. (Would be worth testing on a later version, we can try this later).

let f (arr : (unit -> unit) array) =
	for i = 0 to arr.Length - 1 do
		arr.[i] ()

Observe the IL using ILSpy or run a profiler on this function:

.method public static 
	void f (
		class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>[] arr
	) cil managed 
{
	// Method begins at RVA 0x2050
	// Code size 29 (0x1d)
	.maxstack 6
	.locals init (
		[0] int32
	)

	IL_0000: ldc.i4.0
	IL_0001: stloc.0
	IL_0002: br.s IL_0016
	// loop start (head: IL_0016)
		IL_0004: newobj instance void X.Y/f@9::.ctor()
		IL_0009: ldarg.0
		IL_000a: ldloc.0
		IL_000b: ldnull
		IL_000c: call !!1 class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>[], int32>::InvokeFast<class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!0, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!1, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!0, !!1>>>, !0, !1, !!0)
		IL_0011: pop
		IL_0012: ldloc.0
		IL_0013: ldc.i4.1
		IL_0014: add
		IL_0015: stloc.0

		IL_0016: ldloc.0
		IL_0017: ldarg.0
		IL_0018: ldlen
		IL_0019: conv.i4
		IL_001a: blt.s IL_0004
	// end loop

	IL_001c: ret
} // end of method Y::f

The point of interest is IL_0004, which is an unnecesary newobj call.

By refactoring the code very slightly, the allocation is avoided:

let g (arr : (unit -> unit) array) =
    let get i = arr.[i]
    for i = 0 to arr.Length - 1 do
        get i ()

Which produces the following IL:

.method assembly static 
	class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit> get@12 (
		class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>[] arr,
		int32 i
	) cil managed 
{
	// Method begins at RVA 0x207c
	// Code size 8 (0x8)
	.maxstack 8

	IL_0000: ldarg.0
	IL_0001: ldarg.1
	IL_0002: ldelem class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>
	IL_0007: ret
} // end of method Y::get@12

.method public static 
	void g (
		class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>[] arr
	) cil managed 
{
	// Method begins at RVA 0x2088
	// Code size 29 (0x1d)
	.maxstack 4
	.locals init (
		[0] int32
	)

	IL_0000: ldc.i4.0
	IL_0001: stloc.0
	IL_0002: br.s IL_0016
	// loop start (head: IL_0016)
		IL_0004: ldarg.0
		IL_0005: ldloc.0
		IL_0006: call class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit> X.Y::get@12(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>[], int32)
		IL_000b: ldnull
		IL_000c: callvirt instance !1 class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>::Invoke(!0)
		IL_0011: pop
		IL_0012: ldloc.0
		IL_0013: ldc.i4.1
		IL_0014: add
		IL_0015: stloc.0

		IL_0016: ldloc.0
		IL_0017: ldarg.0
		IL_0018: ldlen
		IL_0019: conv.i4
		IL_001a: blt.s IL_0004
	// end loop

	IL_001c: ret
} // end of method Y::g

Expected behavior

The function f does not allocate at all.

Actual behavior

The function f allocates.

Known workarounds

Using g

Related information

  • Windows 10
  • .NET Framework 4.6.1
  • F# Compiler version 10.1.0
@TobyShaw
Copy link
Author

Looks like a duplicate of #3660, might close this

@dsyme
Copy link
Contributor

dsyme commented Apr 1, 2022

Yup a duplicate, thanks

@dsyme dsyme closed this as completed Apr 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants