Description
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