Skip to content

Inline Option module - #14927

Merged
vzarytovskii merged 6 commits into
dotnet:mainfrom
kerams:inline
Mar 25, 2023
Merged

Inline Option module#14927
vzarytovskii merged 6 commits into
dotnet:mainfrom
kerams:inline

Conversation

@kerams

@keramskerams commented Mar 18, 2023

Copy link
Copy Markdown
Contributor

Implements a part of https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1115-InlineIfLambda-in-FSharp-Core.md.

Benchmark

I've decided to benchmark map and defaultWith, as other functions are structurally equivalent for the most part (with map2 and map3 being ever so slightly more complex) and should have nearly identical performance profiles.

Code

openBenchmarkDotNet.AttributesopenBenchmarkDotNet.ConfigsmoduleInline =let inlinedefaultWith defThunk option =match option with| None -> defThunk ()| Some v -> v
let inlinemap mapping option =match option with| None -> None
| Some x -> Some (mapping x)moduleInlineAndLambda =let inlinedefaultWith([<InlineIfLambda>]defThunk)option =match option with| None -> defThunk ()| Some v -> v
let inlinemap([<InlineIfLambda>]mapping)option =match option with| None -> None
| Some x -> Some (mapping x)// Some function with a bunch of instructions that isn't going to cause lambda inlining without InlineIfLambdalet inliney()=if1/1=1then100/100else2/3[<MemoryDiagnoser>]typeCurrent()=lets= Some System.DateTime.Now.Day
letn= Option<int>.None
[<NoCompilerInlining>]letf=10[<Benchmark>]member_.DefaultWithSingletonNone()=
Option.defaultWith (fun()->41+ y ()) n
[<Benchmark>]member_.DefaultWithNone()=
Option.defaultWith (fun()->42+ f + y ()) n
[<Benchmark>]member_.MapSingletonSome()=
Option.map (fun x -> x +43+ y ()) s
[<Benchmark>]member_.MapSingletonNone()=
Option.map (fun x -> x +44+ y ()) n
[<Benchmark>]member_.MapSome()=
Option.map (fun x -> x + f + y ()) s
[<Benchmark>]member_.MapNone()=
Option.map (fun x -> x + f + y ()) n
[<MemoryDiagnoser>]typeInline()=lets= Some System.DateTime.Now.Day
letn= Option<int>.None
[<NoCompilerInlining>]letf=10[<Benchmark>]member_.DefaultWithSingletonNone()=
Inline.defaultWith (fun()->41+ y ()) n
[<Benchmark>]member_.DefaultWithNone()=
Inline.defaultWith (fun()->42+ f + y ()) n
[<Benchmark>]member_.MapSingletonSome()=
Inline.map (fun x -> x +43+ y ()) s
[<Benchmark>]member_.MapSingletonNone()=
Inline.map (fun x -> x +44+ y ()) n
[<Benchmark>]member_.MapSome()=
Inline.map (fun x -> x + f + y ()) s
[<Benchmark>]member_.MapNone()=
Inline.map (fun x -> x + f + y ()) n
[<MemoryDiagnoser>]typeInlineAndLambda()=lets= Some System.DateTime.Now.Day
letn= Option<int>.None
[<NoCompilerInlining>]letf=10[<Benchmark>]member_.DefaultWithSingletonNone()=
InlineAndLambda.defaultWith (fun()->41+ y ()) n
[<Benchmark>]member_.DefaultWithNone()=
InlineAndLambda.defaultWith (fun()->42+ f + y ()) n
[<Benchmark>]member_.MapSingletonSome()=
InlineAndLambda.map (fun x -> x +43+ y ()) s
[<Benchmark>]member_.MapSingletonNone()=
InlineAndLambda.map (fun x -> x +44+ y ()) n
[<Benchmark>]member_.MapSome()=
InlineAndLambda.map (fun x -> x + f + y ()) s
[<Benchmark>]member_.MapNone()=
InlineAndLambda.map (fun x -> x + f + y ()) n
BenchmarkDotNet.Running.BenchmarkRunner.Run (
typeof<Current>.Assembly,
DefaultConfig.Instance.WithOption (ConfigOptions.JoinSummary,true))|> ignore

Decompiled

usingSystem;usingSystem.Diagnostics;usingSystem.Runtime.CompilerServices;using<StartupCode$Bench>;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Reports;usingMicrosoft.FSharp.Core;// Token: 0x02000002 RID: 2[CompilationMapping(SourceConstructFlags.Module)]publicstaticclassProgram{// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250publicstaticinty(){if(1/1==1){return100/100;}return2/3;}// Token: 0x06000002 RID: 2 RVA: 0x00002064 File Offset: 0x00000264[CompilerGenerated]internalstaticintdefThunk@5(UnitunitVar0){return41+((1/1!=1)?(2/3):(100/100));}// Token: 0x06000003 RID: 3 RVA: 0x0000207C File Offset: 0x0000027C[CompilerGenerated]internalstaticintdefThunk@5-1(Program.Inline_,UnitunitVar0){return42+_.f+((1/1!=1)?(2/3):(100/100));}// Token: 0x06000004 RID: 4 RVA: 0x0000209C File Offset: 0x0000029C[CompilerGenerated]internalstaticintmapping@10(intx){returnx+43+((1/1!=1)?(2/3):(100/100));}// Token: 0x06000005 RID: 5 RVA: 0x000020B8 File Offset: 0x000002B8[CompilerGenerated]internalstaticintmapping@10-1(intx){returnx+44+((1/1!=1)?(2/3):(100/100));}// Token: 0x06000006 RID: 6 RVA: 0x000020D4 File Offset: 0x000002D4[CompilerGenerated]internalstaticintmapping@10-2(Program.Inline_,intx){returnx+_.f+((1/1!=1)?(2/3):(100/100));}// Token: 0x06000007 RID: 7 RVA: 0x000020F4 File Offset: 0x000002F4[CompilerGenerated]internalstaticintmapping@10-3(Program.Inline_,intx){returnx+_.f+((1/1!=1)?(2/3):(100/100));}// Token: 0x17000001 RID: 1// (get) Token: 0x06000008 RID: 8 RVA: 0x00002114 File Offset: 0x00000314[CompilationMapping(SourceConstructFlags.Value)]internalstaticSummary[]arg@1{get{return $Program.arg@1;}}// Token: 0x02000003 RID: 3[MemoryDiagnoser(true)][CompilationMapping(SourceConstructFlags.ObjectType)][Serializable]publicclassCurrent{// Token: 0x06000009 RID: 9 RVA: 0x0000211C File Offset: 0x0000031CpublicCurrent():this(){this.s=FSharpOption<int>.Some(DateTime.Now.Day);this.n=null;this.f=10;}// Token: 0x0600000A RID: 10 RVA: 0x00002158 File Offset: 0x00000358[Benchmark(43,"")]publicintDefaultWithSingletonNone(){returnOptionModule.DefaultWith<int>(Program.DefaultWithSingletonNone@45.@_instance,this.n);}// Token: 0x0600000B RID: 11 RVA: 0x0000216C File Offset: 0x0000036C[Benchmark(47,"")]publicintDefaultWithNone(){returnOptionModule.DefaultWith<int>(newProgram.DefaultWithNone@49(this),this.n);}// Token: 0x0600000C RID: 12 RVA: 0x00002184 File Offset: 0x00000384[Benchmark(51,"")]publicFSharpOption<int>MapSingletonSome(){returnOptionModule.Map<int,int>(Program.MapSingletonSome@53.@_instance,this.s);}// Token: 0x0600000D RID: 13 RVA: 0x00002198 File Offset: 0x00000398[Benchmark(55,"")]publicFSharpOption<int>MapSingletonNone(){returnOptionModule.Map<int,int>(Program.MapSingletonNone@57.@_instance,this.n);}// Token: 0x0600000E RID: 14 RVA: 0x000021AC File Offset: 0x000003AC[Benchmark(59,"")]publicFSharpOption<int>MapSome(){returnOptionModule.Map<int,int>(newProgram.MapSome@61(this),this.s);}// Token: 0x0600000F RID: 15 RVA: 0x000021C4 File Offset: 0x000003C4[Benchmark(63,"")]publicFSharpOption<int>MapNone(){returnOptionModule.Map<int,int>(newProgram.MapNone@65(this),this.n);}// Token: 0x04000001 RID: 1internalFSharpOption<int>s;// Token: 0x04000002 RID: 2internalFSharpOption<int>n;// Token: 0x04000003 RID: 3[NoCompilerInlining]internalintf;}// Token: 0x02000004 RID: 4[Serializable]internalsealedclassDefaultWithSingletonNone@45:FSharpFunc<Unit,int>{// Token: 0x06000010 RID: 16 RVA: 0x000021DC File Offset: 0x000003DC[CompilerGenerated][DebuggerNonUserCode]internalDefaultWithSingletonNone@45(){}// Token: 0x06000011 RID: 17 RVA: 0x000021E4 File Offset: 0x000003E4publicoverrideintInvoke(UnitunitVar0){return41+((1/1!=1)?(2/3):(100/100));}// Token: 0x06000012 RID: 18 RVA: 0x000021FC File Offset: 0x000003FC// Note: this type is marked as 'beforefieldinit'.staticDefaultWithSingletonNone@45(){}// Token: 0x04000004 RID: 4internalstaticreadonlyProgram.DefaultWithSingletonNone@45@_instance=newProgram.DefaultWithSingletonNone@45();}// Token: 0x02000005 RID: 5[Serializable]internalsealedclassDefaultWithNone@49:FSharpFunc<Unit,int>{// Token: 0x06000013 RID: 19 RVA: 0x00002214 File Offset: 0x00000414[CompilerGenerated][DebuggerNonUserCode]internalDefaultWithNone@49(Program.Current_){this._=_;}// Token: 0x06000014 RID: 20 RVA: 0x00002224 File Offset: 0x00000424publicoverrideintInvoke(UnitunitVar0){return42+this._.f+((1/1!=1)?(2/3):(100/100));}// Token: 0x04000005 RID: 5publicProgram.Current_;}// Token: 0x02000006 RID: 6[Serializable]internalsealedclassMapSingletonSome@53:FSharpFunc<int,int>{// Token: 0x06000015 RID: 21 RVA: 0x00002248 File Offset: 0x00000448[CompilerGenerated][DebuggerNonUserCode]internalMapSingletonSome@53(){}// Token: 0x06000016 RID: 22 RVA: 0x00002250 File Offset: 0x00000450publicoverrideintInvoke(intx){returnx+43+((1/1!=1)?(2/3):(100/100));}// Token: 0x06000017 RID: 23 RVA: 0x0000226C File Offset: 0x0000046C// Note: this type is marked as 'beforefieldinit'.staticMapSingletonSome@53(){}// Token: 0x04000006 RID: 6internalstaticreadonlyProgram.MapSingletonSome@53@_instance=newProgram.MapSingletonSome@53();}// Token: 0x02000007 RID: 7[Serializable]internalsealedclassMapSingletonNone@57:FSharpFunc<int,int>{// Token: 0x06000018 RID: 24 RVA: 0x00002284 File Offset: 0x00000484[CompilerGenerated][DebuggerNonUserCode]internalMapSingletonNone@57(){}// Token: 0x06000019 RID: 25 RVA: 0x0000228C File Offset: 0x0000048CpublicoverrideintInvoke(intx){returnx+44+((1/1!=1)?(2/3):(100/100));}// Token: 0x0600001A RID: 26 RVA: 0x000022A8 File Offset: 0x000004A8// Note: this type is marked as 'beforefieldinit'.staticMapSingletonNone@57(){}// Token: 0x04000007 RID: 7internalstaticreadonlyProgram.MapSingletonNone@57@_instance=newProgram.MapSingletonNone@57();}// Token: 0x02000008 RID: 8[Serializable]internalsealedclassMapSome@61:FSharpFunc<int,int>{// Token: 0x0600001B RID: 27 RVA: 0x000022C0 File Offset: 0x000004C0[CompilerGenerated][DebuggerNonUserCode]internalMapSome@61(Program.Current_){this._=_;}// Token: 0x0600001C RID: 28 RVA: 0x000022D0 File Offset: 0x000004D0publicoverrideintInvoke(intx){returnx+this._.f+((1/1!=1)?(2/3):(100/100));}// Token: 0x04000008 RID: 8publicProgram.Current_;}// Token: 0x02000009 RID: 9[Serializable]internalsealedclassMapNone@65:FSharpFunc<int,int>{// Token: 0x0600001D RID: 29 RVA: 0x000022F4 File Offset: 0x000004F4[CompilerGenerated][DebuggerNonUserCode]internalMapNone@65(Program.Current_){this._=_;}// Token: 0x0600001E RID: 30 RVA: 0x00002304 File Offset: 0x00000504publicoverrideintInvoke(intx){returnx+this._.f+((1/1!=1)?(2/3):(100/100));}// Token: 0x04000009 RID: 9publicProgram.Current_;}// Token: 0x0200000A RID: 10[MemoryDiagnoser(true)][CompilationMapping(SourceConstructFlags.ObjectType)][Serializable]publicclassInline{// Token: 0x0600001F RID: 31 RVA: 0x00002328 File Offset: 0x00000528publicInline():this(){this.s=FSharpOption<int>.Some(DateTime.Now.Day);this.n=null;this.f=10;}// Token: 0x06000020 RID: 32 RVA: 0x00002364 File Offset: 0x00000564[Benchmark(77,"")]publicintDefaultWithSingletonNone(){FSharpOption<int>fsharpOption=this.n;if(fsharpOption!=null){returnfsharpOption.Value;}returnProgram.defThunk@5(null);}// Token: 0x06000021 RID: 33 RVA: 0x0000238C File Offset: 0x0000058C[Benchmark(81,"")]publicintDefaultWithNone(){FSharpOption<int>fsharpOption=this.n;if(fsharpOption!=null){returnfsharpOption.Value;}returnProgram.defThunk@5-1(this,null);}// Token: 0x06000022 RID: 34 RVA: 0x000023B4 File Offset: 0x000005B4[Benchmark(85,"")]publicFSharpOption<int>MapSingletonSome(){FSharpOption<int>fsharpOption=this.s;if(fsharpOption!=null){FSharpOption<int>fsharpOption2=fsharpOption;intvalue=fsharpOption2.Value;returnFSharpOption<int>.Some(Program.mapping@10(value));}returnnull;}// Token: 0x06000023 RID: 35 RVA: 0x000023E4 File Offset: 0x000005E4[Benchmark(89,"")]publicFSharpOption<int>MapSingletonNone(){FSharpOption<int>fsharpOption=this.n;if(fsharpOption!=null){FSharpOption<int>fsharpOption2=fsharpOption;intvalue=fsharpOption2.Value;returnFSharpOption<int>.Some(Program.mapping@10-1(value));}returnnull;}// Token: 0x06000024 RID: 36 RVA: 0x00002414 File Offset: 0x00000614[Benchmark(93,"")]publicFSharpOption<int>MapSome(){FSharpOption<int>fsharpOption=this.s;if(fsharpOption!=null){FSharpOption<int>fsharpOption2=fsharpOption;intvalue=fsharpOption2.Value;returnFSharpOption<int>.Some(Program.mapping@10-2(this,value));}returnnull;}// Token: 0x06000025 RID: 37 RVA: 0x00002444 File Offset: 0x00000644[Benchmark(97,"")]publicFSharpOption<int>MapNone(){FSharpOption<int>fsharpOption=this.n;if(fsharpOption!=null){FSharpOption<int>fsharpOption2=fsharpOption;intvalue=fsharpOption2.Value;returnFSharpOption<int>.Some(Program.mapping@10-3(this,value));}returnnull;}// Token: 0x0400000A RID: 10internalFSharpOption<int>s;// Token: 0x0400000B RID: 11internalFSharpOption<int>n;// Token: 0x0400000C RID: 12[NoCompilerInlining]internalintf;}// Token: 0x0200000B RID: 11[MemoryDiagnoser(true)][CompilationMapping(SourceConstructFlags.ObjectType)][Serializable]publicclassInlineAndLambda{// Token: 0x06000026 RID: 38 RVA: 0x00002474 File Offset: 0x00000674publicInlineAndLambda():this(){this.s=FSharpOption<int>.Some(DateTime.Now.Day);this.n=null;this.f=10;}// Token: 0x06000027 RID: 39 RVA: 0x000024B0 File Offset: 0x000006B0[Benchmark(111,"")]publicintDefaultWithSingletonNone(){FSharpOption<int>fsharpOption=this.n;if(fsharpOption!=null){returnfsharpOption.Value;}return41+((1/1!=1)?(2/3):(100/100));}// Token: 0x06000028 RID: 40 RVA: 0x000024E8 File Offset: 0x000006E8[Benchmark(115,"")]publicintDefaultWithNone(){FSharpOption<int>fsharpOption=this.n;if(fsharpOption!=null){returnfsharpOption.Value;}return42+this.f+((1/1!=1)?(2/3):(100/100));}// Token: 0x06000029 RID: 41 RVA: 0x00002524 File Offset: 0x00000724[Benchmark(119,"")]publicFSharpOption<int>MapSingletonSome(){FSharpOption<int>fsharpOption=this.s;if(fsharpOption!=null){FSharpOption<int>fsharpOption2=fsharpOption;intx=fsharpOption2.Value;returnFSharpOption<int>.Some(x+43+((1/1!=1)?(2/3):(100/100)));}returnnull;}// Token: 0x0600002A RID: 42 RVA: 0x00002564 File Offset: 0x00000764[Benchmark(123,"")]publicFSharpOption<int>MapSingletonNone(){FSharpOption<int>fsharpOption=this.n;if(fsharpOption!=null){FSharpOption<int>fsharpOption2=fsharpOption;intx=fsharpOption2.Value;returnFSharpOption<int>.Some(x+44+((1/1!=1)?(2/3):(100/100)));}returnnull;}// Token: 0x0600002B RID: 43 RVA: 0x000025A4 File Offset: 0x000007A4[Benchmark(127,"")]publicFSharpOption<int>MapSome(){FSharpOption<int>fsharpOption=this.s;if(fsharpOption!=null){FSharpOption<int>fsharpOption2=fsharpOption;intx=fsharpOption2.Value;returnFSharpOption<int>.Some(x+this.f+((1/1!=1)?(2/3):(100/100)));}returnnull;}// Token: 0x0600002C RID: 44 RVA: 0x000025E8 File Offset: 0x000007E8[Benchmark(131,"")]publicFSharpOption<int>MapNone(){FSharpOption<int>fsharpOption=this.n;if(fsharpOption!=null){FSharpOption<int>fsharpOption2=fsharpOption;intx=fsharpOption2.Value;returnFSharpOption<int>.Some(x+this.f+((1/1!=1)?(2/3):(100/100)));}returnnull;}// Token: 0x0400000D RID: 13internalFSharpOption<int>s;// Token: 0x0400000E RID: 14internalFSharpOption<int>n;// Token: 0x0400000F RID: 15[NoCompilerInlining]internalintf;}// Token: 0x0200000C RID: 12[CompilationMapping(SourceConstructFlags.Module)]publicstaticclassInlineAndLambdaModule{// Token: 0x0600002D RID: 45 RVA: 0x0000262C File Offset: 0x0000082C[CompilationArgumentCounts(newint[]{1,1})]publicstaticadefaultWith<a>([InlineIfLambda]FSharpFunc<Unit,a>defThunk,FSharpOption<a>option){if(option!=null){returnoption.Value;}returndefThunk.Invoke(null);}// Token: 0x0600002E RID: 46 RVA: 0x00002654 File Offset: 0x00000854[CompilationArgumentCounts(newint[]{1,1})]publicstaticFSharpOption<b>map<a,b>([InlineIfLambda]FSharpFunc<a,b>mapping,FSharpOption<a>option){if(option!=null){ax=option.Value;returnFSharpOption<b>.Some(mapping.Invoke(x));}returnnull;}}// Token: 0x0200000D RID: 13[CompilationMapping(SourceConstructFlags.Module)]publicstaticclassInlineModule{// Token: 0x0600002F RID: 47 RVA: 0x00002680 File Offset: 0x00000880[CompilationArgumentCounts(newint[]{1,1})]publicstaticadefaultWith<a>(FSharpFunc<Unit,a>defThunk,FSharpOption<a>option){if(option!=null){returnoption.Value;}returndefThunk.Invoke(null);}// Token: 0x06000030 RID: 48 RVA: 0x000026A8 File Offset: 0x000008A8[CompilationArgumentCounts(newint[]{1,1})]publicstaticFSharpOption<b>map<a,b>(FSharpFunc<a,b>mapping,FSharpOption<a>option){if(option!=null){ax=option.Value;returnFSharpOption<b>.Some(mapping.Invoke(x));}returnnull;}}}

BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1413/22H2/2022Update/SunValley2)
AMD Ryzen 9 7900, 1 CPU, 24 logical and 12 physical cores
.NET SDK=8.0.100-preview.2.23157.25
[Host] : .NET 7.0.3 (7.0.323.6910), X64 RyuJIT AVX2 DEBUG
DefaultJob : .NET 7.0.3 (7.0.323.6910), X64 RyuJIT AVX2
TypeMethodMeanErrorStdDevMedianGen0Allocated
CurrentDefaultWithSingletonNone1.3237 ns0.0090 ns0.0070 ns1.3256 ns--
InlineDefaultWithSingletonNone0.2013 ns0.0003 ns0.0002 ns0.2013 ns--
InlineAndLambdaDefaultWithSingletonNone0.0033 ns0.0005 ns0.0004 ns0.0031 ns--
CurrentDefaultWithNone2.9997 ns0.0451 ns0.0422 ns3.0150 ns0.001424 B
InlineDefaultWithNone0.0039 ns0.0008 ns0.0007 ns0.0038 ns--
InlineAndLambdaDefaultWithNone0.0048 ns0.0002 ns0.0002 ns0.0048 ns--
CurrentMapSingletonSome4.2985 ns0.0974 ns0.0813 ns4.3439 ns0.001424 B
InlineMapSingletonSome2.5184 ns0.0872 ns0.0816 ns2.5410 ns0.001424 B
InlineAndLambdaMapSingletonSome2.1439 ns0.0820 ns0.1066 ns2.1745 ns0.001424 B
CurrentMapSingletonNone1.1640 ns0.0007 ns0.0006 ns1.1639 ns--
InlineMapSingletonNone0.0230 ns0.0003 ns0.0003 ns0.0230 ns--
InlineAndLambdaMapSingletonNone0.1058 ns0.0275 ns0.0283 ns0.1110 ns--
CurrentMapSome5.8279 ns0.0610 ns0.0571 ns5.8527 ns0.002948 B
InlineMapSome2.3016 ns0.0696 ns0.0651 ns2.3151 ns0.001424 B
InlineAndLambdaMapSome1.9041 ns0.0216 ns0.0180 ns1.9062 ns0.001424 B
CurrentMapNone2.7781 ns0.0257 ns0.0215 ns2.7681 ns0.001424 B
InlineMapNone0.2242 ns0.0061 ns0.0057 ns0.2275 ns--
InlineAndLambdaMapNone0.1775 ns0.0420 ns0.0712 ns0.2117 ns--

Analysis

inline has the largest performance impact, and adding InlineIfLambda improves on it only marginally. This stems from the fact that the compiler doesn't need the attribute to avoid both a virtual call and allocating a closure with captured variables, although this might not be the case in all circumstances.

Compare these 3 methods:

// Some function with a bunch of instructions that isn't going to cause lambda inlining without InlineIfLambdalet inliney()=if1/1=1then100/100else2/3typeT()=[<NoCompilerInlining>]letf=10letn= Option<int>.None
member_.DefaultWithStatusQuo()=
Option.defaultWith (fun()->41+ f + y ()) n
member_.DefaultWithInlined()=
Inline.defaultWith (fun()->41+ f + y ()) n
member_.DefaultWithInlinedAndAttribute()=
InlineAndLambda.defaultWith (fun()->41+ f + y ()) n

Decompiling to:

publicintDefaultWithStatusQuo(){// Allocating a closure + virtual call in `Option.defaultWith`returnOptionModule.DefaultWith<int>(newProgram.DefaultWithStatusQuo@109(this),this.n);}publicintDefaultWithInlined(){FSharpOption<int>fsharpOption=this.n;if(fsharpOption!=null){returnfsharpOption.Value;}// Only a singleton closure, or, as in this case, a static method call, and no virtual callsreturnProgram.defThunk@5-2(this,null);}publicintDefaultWithInlinedAndAttribute(){FSharpOption<int>fsharpOption=this.n;if(fsharpOption!=null){returnfsharpOption.Value;}// No closures or extra indirectionsreturn41+this.f+((1/1!=1)?(2/3):(100/100));}

Inlining Option.defaultWith adds around 6 IL instructions at the call site, with the number increasing a little bit for more complex functions like Option.map3. InlineIfLambda will naturally increase the function size further by whatever amount of instruction the lambda contains. This might in theory cause a cascade of inlinability changes, where some functions might swell past the limit the F# compiler and JIT consider automatically inlinable, or the F# compiler might split the function in 2.

Nonetheless, I believe adding inline to all of the function would be a clear net win. InlineIfLambda will generally improve performance a little bit more still, but we could conceive of edge cases where this would cause regressions, specifically with respect to JIT and what functions it considers eligible for inlining and optimizing. I'm leaning towards adding the attribute too, but don't mind removing it.

@kerams
kerams requested a review from a team as a code ownerMarch 18, 2023 13:53
@kerams

Copy link
Copy Markdown
ContributorAuthor

For what it's worth, the size of FSharp.Compiler.Service.dll decreased from 18,335,232 to 18,252,288 bytes. Can't speak to the performance of the compiler, but I doubt the impact will be measurable.

@vzarytovskii

vzarytovskii commented Mar 20, 2023

Copy link
Copy Markdown
Member

Just thinking out loud here - option is much more widely used than many others, so I guess, one of the downsides of inlining things like defaultWith, bind, map and their respective lambdas might result in codegen'd methods being very big. Which might affect JIT inlining and optimizations.

Wondering if it might become a problem in big codebases with use a lot of options everywhere.

@EgorBo how does JIT determine those thresholds for inlining/optimizing now?

@vzarytovskii

Copy link
Copy Markdown
Member

For what it's worth, the size of FSharp.Compiler.Service.dll decreased from 18,335,232 to 18,252,288 bytes.

That is actually quite interesting

@kerams

kerams commented Mar 20, 2023

Copy link
Copy Markdown
ContributorAuthor

and their respective lambdas might result in codegen'd methods being very big. Which might affect JIT inlining and optimizations.

That's what I mean in the final 2 paragraphs.

option is much more widely used than many others

That's also why making it as fast as possible will be noticed the most.

Wondering if it might become a problem in big codebases with use a lot of options everywhere.

I expect this might be an issue in very special scenarios when you absolutely require JIT inlining and specific optimizations. If that's the case, the solution is simply to move the guts of the lambda into a function.

In the vast majority of cases (shameless guess :)) we'd just be leaving a bit of performance on the board without InlineIfLambda. That's the gist of my reasoning.

That is actually quite interesting

I reckon it would be the closures and extra functions that are no longer needed as the bodies are inlined.

@vzarytovskii

vzarytovskii commented Mar 20, 2023

Copy link
Copy Markdown
Member

and their respective lambdas might result in codegen'd methods being very big. Which might affect JIT inlining and optimizations.

That's what I mean in the final 2 paragraphs.

Yeah, it seems I've failed to read them properly, sorry.

Wondering if it might become a problem in big codebases with use a lot of options everywhere.

I expect this might be an issue in very special scenarios when you absolutely require JIT inlining and specific optimizations. If that's the case, the solution is simply to move the guts of the lambda into a function.

Yeah, just wondering how often actually this would be the case.

@EgorBo

Copy link
Copy Markdown
Member

Just thinking out loud here - option is much more widely used than many others, so I guess, one of the downsides of inlining things like defaultWith, bind, map and their respective lambdas might result in codegen'd methods being very big. Which might affect JIT inlining and optimizations.

Wondering if it might become a problem in big codebases with use a lot of options everywhere.

@EgorBo how does JIT determine those thresholds for inlining/optimizing now?

So very large methods have several limitattions:

  1. If jit needs to create too many locals (e.g. >1024) it will switch to slow stack spills/restore
  2. For very huge methods JIT just gives up and always use Tier0/MinOpts
  3. Inlining decisions mostly don't depend on the size of the caller method, although, there is a heuristic that takes number of locals into account.

So overall it's hard to say/predict anything, but in general very large methods aren't good for JIT and its Register Allocator.

@dsyme

Copy link
Copy Markdown
Contributor

Looks great - My intuition is it's fine to add both inline and InlineIfLambda to these.

@vzarytovskii Code size shouldn't be a problem as the size of a closure will usually be bigger.

My understanding is that adding InlineIfLambda tends to be safe for any function argument of an inline method that only has one callsite in the method.

@T-Gro

Copy link
Copy Markdown
Member

The trimming test also reports a smaller code size with this PR, although nothing dramatical.

Expected:
247808 Bytes
Actual:
247296 Bytes

@vzarytovskii

Copy link
Copy Markdown
Member

The trimming test also reports a smaller code size with this PR, although nothing dramatical.

Expected: 247808 Bytes Actual: 247296 Bytes

The overall code size is not important in this case, but rather each individual method size matters.

@T-Gro

Copy link
Copy Markdown
Member

(CI will fail at first, then expected size of trimmed app has to be updated one more time)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

5 participants

@kerams@vzarytovskii@EgorBo@dsyme@T-Gro