Skip to content

Performance: Not possible to prevent inlining the IL by the F# compiler without the MethodImplOptions.NoInlining flags #5178

Closed
@zpodlovics

Description

@zpodlovics

I am trying to do some performance optimalizations using the ThrowHelper like construct as of #5019 (comment) :

The main method does its checks (say argument validation) and then conditionally calls throw helper with any needed args. Type the helper so no implicit boxing or parms array allocation is needed.

The throw helper does all formatting and exception object creation, then unconditionally throws. It is deliberately not marked with [MethodImpl(MethodImplOptions.NoInlining)].

However it seems the F# compiler still inlines all the small functions IL without the MethodImplOptions.NoInlining flag, and also inlines the small static method calls IL without this attribute. It should be possible express non IL inlining for F# functions and static and non-static methods without marking it MethodImplOptions.NoInlining.

Please provide a succinct description of the issue.

Repro steps

open System
open System.Runtime.InteropServices
open System.Runtime.CompilerServices

type [<RequireQualifiedAccess>] EnumType =
  | F = 0uy
  | T = 1uy 

type ThrowHelper() =
  static member Call() =
    invalidArg "value" "invalid value"

module EnumType = begin
  [<MethodImpl(MethodImplOptions.NoInlining)>]
  let noInliningThrowHelper() =
    invalidArg "value" "invalid value"

  let throwHelper() =
    invalidArg "value" "invalid value"

  let ofValue1 (value: uint8) : EnumType = 
    match value with
    | v when v = 0uy -> EnumType.F
    | v when v = 1uy -> EnumType.T
    | _ -> throwHelper()

  let ofValue2 (value: uint8) : EnumType = 
    match value with
    | v when v = 0uy -> EnumType.F
    | v when v = 1uy -> EnumType.T
    | _ -> ThrowHelper.Call()

  let ofValue3 (value: uint8) : EnumType = 
    match value with
    | v when v = 0uy -> EnumType.F
    | v when v = 1uy -> EnumType.T
    | _ -> noInliningThrowHelper()

end

[<EntryPoint>]
let main argv =
    let v1 = EnumType.ofValue1 1uy
    printfn "Enum: %s" (v1.ToString())
    let v2 = EnumType.ofValue2 1uy
    printfn "Enum: %s" (v2.ToString())
    let v3 = EnumType.ofValue2 1uy
    printfn "Enum: %s" (v3.ToString())
    0 // return an integer exit code

Provide the steps required to reproduce the problem

The compiled IL code will looks like this:

  .class auto ansi serializable nested public ThrowHelper
         extends [System.Runtime]System.Object
  {
    .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 03 00 00 00 00 00 ) 
    .method public specialname rtspecialname 
            instance void  .ctor() cil managed
    {
      // Code size       9 (0x9)
      .maxstack  8
      IL_0000:  ldarg.0
      IL_0001:  callvirt   instance void [System.Runtime]System.Object::.ctor()
      IL_0006:  ldarg.0
      IL_0007:  pop
      IL_0008:  ret
    } // end of method ThrowHelper::.ctor

    .method public static !!a  Call<a>() cil managed
    {
      // Code size       16 (0x10)
      .maxstack  8
      IL_0000:  ldstr      "invalid value"
      IL_0005:  ldstr      "value"
      IL_000a:  newobj     instance void [System.Runtime]System.ArgumentException::.ctor(string,
                                                                                         string)
      IL_000f:  throw
    } // end of method ThrowHelper::Call

  } // end of class ThrowHelper

  .class abstract auto ansi sealed nested public EnumTypeModule
         extends [System.Runtime]System.Object
  {
    .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = ( 01 00 07 00 00 00 00 00 ) 
    .method public static !!a  noInliningThrowHelper<a>() cil managed noinlining
    {
      // Code size       16 (0x10)
      .maxstack  8
      IL_0000:  ldstr      "invalid value"
      IL_0005:  ldstr      "value"
      IL_000a:  newobj     instance void [System.Runtime]System.ArgumentException::.ctor(string,
                                                                                         string)
      IL_000f:  throw
    } // end of method EnumTypeModule::noInliningThrowHelper

    .method public static !!a  throwHelper<a>() cil managed
    {
      // Code size       16 (0x10)
      .maxstack  8
      IL_0000:  ldstr      "invalid value"
      IL_0005:  ldstr      "value"
      IL_000a:  newobj     instance void [System.Runtime]System.ArgumentException::.ctor(string,
                                                                                         string)
      IL_000f:  throw
    } // end of method EnumTypeModule::throwHelper

    .method public static valuetype Program/EnumType 
            ofValue1(uint8 'value') cil managed
    {
      // Code size       27 (0x1b)
      .maxstack  8
      IL_0000:  ldarg.0
      IL_0001:  brtrue.s   IL_0005

      IL_0003:  ldc.i4.0
      IL_0004:  ret

      IL_0005:  ldarg.0
      IL_0006:  ldc.i4.1
      IL_0007:  bne.un.s   IL_000b

      IL_0009:  ldc.i4.1
      IL_000a:  ret

      IL_000b:  ldstr      "invalid value"
      IL_0010:  ldstr      "value"
      IL_0015:  newobj     instance void [System.Runtime]System.ArgumentException::.ctor(string,
                                                                                         string)
      IL_001a:  throw
    } // end of method EnumTypeModule::ofValue1

    .method public static valuetype Program/EnumType 
            ofValue2(uint8 'value') cil managed
    {
      // Code size       27 (0x1b)
      .maxstack  8
      IL_0000:  ldarg.0
      IL_0001:  brtrue.s   IL_0005

      IL_0003:  ldc.i4.0
      IL_0004:  ret

      IL_0005:  ldarg.0
      IL_0006:  ldc.i4.1
      IL_0007:  bne.un.s   IL_000b

      IL_0009:  ldc.i4.1
      IL_000a:  ret

      IL_000b:  ldstr      "invalid value"
      IL_0010:  ldstr      "value"
      IL_0015:  newobj     instance void [System.Runtime]System.ArgumentException::.ctor(string,
                                                                                         string)
      IL_001a:  throw
    } // end of method EnumTypeModule::ofValue2

    .method public static valuetype Program/EnumType 
            ofValue3(uint8 'value') cil managed
    {
      // Code size       17 (0x11)
      .maxstack  8
      IL_0000:  ldarg.0
      IL_0001:  brtrue.s   IL_0005

      IL_0003:  ldc.i4.0
      IL_0004:  ret

      IL_0005:  ldarg.0
      IL_0006:  ldc.i4.1
      IL_0007:  bne.un.s   IL_000b

      IL_0009:  ldc.i4.1
      IL_000a:  ret

      IL_000b:  call       !!0 Program/EnumTypeModule::noInliningThrowHelper<valuetype Program/EnumType>()
      IL_0010:  ret
    } // end of method EnumTypeModule::ofValue3

  } // end of class EnumTypeModule

Expected behavior

Should be possible to express non IL inlining small functions and static and non static method without marking it with MethodImplOptions.NoInlining.

Actual behavior

F# compiler IL inlining small functions and static and non static method without marking it with MethodImplOptions.NoInlining.

Possible/Known workarounds

Implement the helper in C# (not yet tested)

The best future solution probably would be using the noinline keyword as inline already used for functions and static methods/methods (but this will complicate the language a little bit more). However it would be a perfecly fine future solution to have a [<NoInline>] flag in F# for functions/static methods/methods.

Related information

  • Operating system: Ubuntu 16.04 x86_64
  • .NET Runtime, CoreCLR or Mono Version:
ii  dotnet-host                                                 2.1.0-1                                                     amd64        Microsoft .NET Core Host - 2.1.0
ii  dotnet-hostfxr-2.0.7                                        2.0.7-1                                                     amd64        Microsoft .NET Core Host FX Resolver - 2.0.7 2.0.7
ii  dotnet-hostfxr-2.1                                          2.1.0-1                                                     amd64        Microsoft .NET Core Host FX Resolver - 2.1.0 2.1.0
ii  dotnet-runtime-2.0.7                                        2.0.7-1                                                     amd64        Microsoft .NET Core Runtime - 2.0.7 Microsoft.NETCore.App 2.0.7
ii  dotnet-runtime-2.1                                          2.1.0-1                                                     amd64        Microsoft .NET Core Runtime - 2.1.0 Microsoft.NETCore.App 2.1.0
ii  dotnet-runtime-deps-2.1                                     2.1.0-1                                                     amd64        dotnet-runtime-deps-2.1 2.1.0
ii  dotnet-sdk-2.1                                              2.1.300-1                                                   amd64        Microsoft .NET Core SDK 2.1.300

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions