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

Inline Option module #14927

Merged
merged 6 commits into from
Mar 25, 2023
Merged

Inline Option module #14927

merged 6 commits into from
Mar 25, 2023

Conversation

kerams
Copy link
Contributor

@kerams kerams commented Mar 18, 2023

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

open BenchmarkDotNet.Attributes
open BenchmarkDotNet.Configs

module Inline =
    let inline defaultWith defThunk option =
        match option with
        | None -> defThunk ()
        | Some v -> v

    let inline map mapping option =
        match option with
        | None -> None
        | Some x -> Some (mapping x)

module InlineAndLambda =
    let inline defaultWith ([<InlineIfLambda>]defThunk) option =
        match option with
        | None -> defThunk ()
        | Some v -> v

    let inline map ([<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 InlineIfLambda
let inline y () =
    if 1 / 1 = 1 then
        100 / 100
    else
        2 / 3

[<MemoryDiagnoser>]
type Current () =

    let s = Some System.DateTime.Now.Day

    let n = Option<int>.None

    [<NoCompilerInlining>]
    let f = 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>]
type Inline () =

    let s = Some System.DateTime.Now.Day

    let n = Option<int>.None

    [<NoCompilerInlining>]
    let f = 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>]
type InlineAndLambda () =

    let s = Some System.DateTime.Now.Day

    let n = Option<int>.None

    [<NoCompilerInlining>]
    let f = 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

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using <StartupCode$Bench>;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Reports;
using Microsoft.FSharp.Core;

// Token: 0x02000002 RID: 2
[CompilationMapping(SourceConstructFlags.Module)]
public static class Program
{
	// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
	public static int y()
	{
		if (1 / 1 == 1)
		{
			return 100 / 100;
		}
		return 2 / 3;
	}

	// Token: 0x06000002 RID: 2 RVA: 0x00002064 File Offset: 0x00000264
	[CompilerGenerated]
	internal static int defThunk@5(Unit unitVar0)
	{
		return 41 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
	}

	// Token: 0x06000003 RID: 3 RVA: 0x0000207C File Offset: 0x0000027C
	[CompilerGenerated]
	internal static int defThunk@5-1(Program.Inline _, Unit unitVar0)
	{
		return 42 + _.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
	}

	// Token: 0x06000004 RID: 4 RVA: 0x0000209C File Offset: 0x0000029C
	[CompilerGenerated]
	internal static int mapping@10(int x)
	{
		return x + 43 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
	}

	// Token: 0x06000005 RID: 5 RVA: 0x000020B8 File Offset: 0x000002B8
	[CompilerGenerated]
	internal static int mapping@10-1(int x)
	{
		return x + 44 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
	}

	// Token: 0x06000006 RID: 6 RVA: 0x000020D4 File Offset: 0x000002D4
	[CompilerGenerated]
	internal static int mapping@10-2(Program.Inline _, int x)
	{
		return x + _.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
	}

	// Token: 0x06000007 RID: 7 RVA: 0x000020F4 File Offset: 0x000002F4
	[CompilerGenerated]
	internal static int mapping@10-3(Program.Inline _, int x)
	{
		return x + _.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)]
	internal static Summary[] arg@1
	{
		get
		{
			return $Program.arg@1;
		}
	}

	// Token: 0x02000003 RID: 3
	[MemoryDiagnoser(true)]
	[CompilationMapping(SourceConstructFlags.ObjectType)]
	[Serializable]
	public class Current
	{
		// Token: 0x06000009 RID: 9 RVA: 0x0000211C File Offset: 0x0000031C
		public Current()
			: 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, "")]
		public int DefaultWithSingletonNone()
		{
			return OptionModule.DefaultWith<int>(Program.DefaultWithSingletonNone@45.@_instance, this.n);
		}

		// Token: 0x0600000B RID: 11 RVA: 0x0000216C File Offset: 0x0000036C
		[Benchmark(47, "")]
		public int DefaultWithNone()
		{
			return OptionModule.DefaultWith<int>(new Program.DefaultWithNone@49(this), this.n);
		}

		// Token: 0x0600000C RID: 12 RVA: 0x00002184 File Offset: 0x00000384
		[Benchmark(51, "")]
		public FSharpOption<int> MapSingletonSome()
		{
			return OptionModule.Map<int, int>(Program.MapSingletonSome@53.@_instance, this.s);
		}

		// Token: 0x0600000D RID: 13 RVA: 0x00002198 File Offset: 0x00000398
		[Benchmark(55, "")]
		public FSharpOption<int> MapSingletonNone()
		{
			return OptionModule.Map<int, int>(Program.MapSingletonNone@57.@_instance, this.n);
		}

		// Token: 0x0600000E RID: 14 RVA: 0x000021AC File Offset: 0x000003AC
		[Benchmark(59, "")]
		public FSharpOption<int> MapSome()
		{
			return OptionModule.Map<int, int>(new Program.MapSome@61(this), this.s);
		}

		// Token: 0x0600000F RID: 15 RVA: 0x000021C4 File Offset: 0x000003C4
		[Benchmark(63, "")]
		public FSharpOption<int> MapNone()
		{
			return OptionModule.Map<int, int>(new Program.MapNone@65(this), this.n);
		}

		// Token: 0x04000001 RID: 1
		internal FSharpOption<int> s;

		// Token: 0x04000002 RID: 2
		internal FSharpOption<int> n;

		// Token: 0x04000003 RID: 3
		[NoCompilerInlining]
		internal int f;
	}

	// Token: 0x02000004 RID: 4
	[Serializable]
	internal sealed class DefaultWithSingletonNone@45 : FSharpFunc<Unit, int>
	{
		// Token: 0x06000010 RID: 16 RVA: 0x000021DC File Offset: 0x000003DC
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal DefaultWithSingletonNone@45()
		{
		}

		// Token: 0x06000011 RID: 17 RVA: 0x000021E4 File Offset: 0x000003E4
		public override int Invoke(Unit unitVar0)
		{
			return 41 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		// Token: 0x06000012 RID: 18 RVA: 0x000021FC File Offset: 0x000003FC
		// Note: this type is marked as 'beforefieldinit'.
		static DefaultWithSingletonNone@45()
		{
		}

		// Token: 0x04000004 RID: 4
		internal static readonly Program.DefaultWithSingletonNone@45 @_instance = new Program.DefaultWithSingletonNone@45();
	}

	// Token: 0x02000005 RID: 5
	[Serializable]
	internal sealed class DefaultWithNone@49 : FSharpFunc<Unit, int>
	{
		// Token: 0x06000013 RID: 19 RVA: 0x00002214 File Offset: 0x00000414
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal DefaultWithNone@49(Program.Current _)
		{
			this._ = _;
		}

		// Token: 0x06000014 RID: 20 RVA: 0x00002224 File Offset: 0x00000424
		public override int Invoke(Unit unitVar0)
		{
			return 42 + this._.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		// Token: 0x04000005 RID: 5
		public Program.Current _;
	}

	// Token: 0x02000006 RID: 6
	[Serializable]
	internal sealed class MapSingletonSome@53 : FSharpFunc<int, int>
	{
		// Token: 0x06000015 RID: 21 RVA: 0x00002248 File Offset: 0x00000448
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal MapSingletonSome@53()
		{
		}

		// Token: 0x06000016 RID: 22 RVA: 0x00002250 File Offset: 0x00000450
		public override int Invoke(int x)
		{
			return x + 43 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		// Token: 0x06000017 RID: 23 RVA: 0x0000226C File Offset: 0x0000046C
		// Note: this type is marked as 'beforefieldinit'.
		static MapSingletonSome@53()
		{
		}

		// Token: 0x04000006 RID: 6
		internal static readonly Program.MapSingletonSome@53 @_instance = new Program.MapSingletonSome@53();
	}

	// Token: 0x02000007 RID: 7
	[Serializable]
	internal sealed class MapSingletonNone@57 : FSharpFunc<int, int>
	{
		// Token: 0x06000018 RID: 24 RVA: 0x00002284 File Offset: 0x00000484
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal MapSingletonNone@57()
		{
		}

		// Token: 0x06000019 RID: 25 RVA: 0x0000228C File Offset: 0x0000048C
		public override int Invoke(int x)
		{
			return x + 44 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		// Token: 0x0600001A RID: 26 RVA: 0x000022A8 File Offset: 0x000004A8
		// Note: this type is marked as 'beforefieldinit'.
		static MapSingletonNone@57()
		{
		}

		// Token: 0x04000007 RID: 7
		internal static readonly Program.MapSingletonNone@57 @_instance = new Program.MapSingletonNone@57();
	}

	// Token: 0x02000008 RID: 8
	[Serializable]
	internal sealed class MapSome@61 : FSharpFunc<int, int>
	{
		// Token: 0x0600001B RID: 27 RVA: 0x000022C0 File Offset: 0x000004C0
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal MapSome@61(Program.Current _)
		{
			this._ = _;
		}

		// Token: 0x0600001C RID: 28 RVA: 0x000022D0 File Offset: 0x000004D0
		public override int Invoke(int x)
		{
			return x + this._.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		// Token: 0x04000008 RID: 8
		public Program.Current _;
	}

	// Token: 0x02000009 RID: 9
	[Serializable]
	internal sealed class MapNone@65 : FSharpFunc<int, int>
	{
		// Token: 0x0600001D RID: 29 RVA: 0x000022F4 File Offset: 0x000004F4
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal MapNone@65(Program.Current _)
		{
			this._ = _;
		}

		// Token: 0x0600001E RID: 30 RVA: 0x00002304 File Offset: 0x00000504
		public override int Invoke(int x)
		{
			return x + this._.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		// Token: 0x04000009 RID: 9
		public Program.Current _;
	}

	// Token: 0x0200000A RID: 10
	[MemoryDiagnoser(true)]
	[CompilationMapping(SourceConstructFlags.ObjectType)]
	[Serializable]
	public class Inline
	{
		// Token: 0x0600001F RID: 31 RVA: 0x00002328 File Offset: 0x00000528
		public Inline()
			: 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, "")]
		public int DefaultWithSingletonNone()
		{
			FSharpOption<int> fsharpOption = this.n;
			if (fsharpOption != null)
			{
				return fsharpOption.Value;
			}
			return Program.defThunk@5(null);
		}

		// Token: 0x06000021 RID: 33 RVA: 0x0000238C File Offset: 0x0000058C
		[Benchmark(81, "")]
		public int DefaultWithNone()
		{
			FSharpOption<int> fsharpOption = this.n;
			if (fsharpOption != null)
			{
				return fsharpOption.Value;
			}
			return Program.defThunk@5-1(this, null);
		}

		// Token: 0x06000022 RID: 34 RVA: 0x000023B4 File Offset: 0x000005B4
		[Benchmark(85, "")]
		public FSharpOption<int> MapSingletonSome()
		{
			FSharpOption<int> fsharpOption = this.s;
			if (fsharpOption != null)
			{
				FSharpOption<int> fsharpOption2 = fsharpOption;
				int value = fsharpOption2.Value;
				return FSharpOption<int>.Some(Program.mapping@10(value));
			}
			return null;
		}

		// Token: 0x06000023 RID: 35 RVA: 0x000023E4 File Offset: 0x000005E4
		[Benchmark(89, "")]
		public FSharpOption<int> MapSingletonNone()
		{
			FSharpOption<int> fsharpOption = this.n;
			if (fsharpOption != null)
			{
				FSharpOption<int> fsharpOption2 = fsharpOption;
				int value = fsharpOption2.Value;
				return FSharpOption<int>.Some(Program.mapping@10-1(value));
			}
			return null;
		}

		// Token: 0x06000024 RID: 36 RVA: 0x00002414 File Offset: 0x00000614
		[Benchmark(93, "")]
		public FSharpOption<int> MapSome()
		{
			FSharpOption<int> fsharpOption = this.s;
			if (fsharpOption != null)
			{
				FSharpOption<int> fsharpOption2 = fsharpOption;
				int value = fsharpOption2.Value;
				return FSharpOption<int>.Some(Program.mapping@10-2(this, value));
			}
			return null;
		}

		// Token: 0x06000025 RID: 37 RVA: 0x00002444 File Offset: 0x00000644
		[Benchmark(97, "")]
		public FSharpOption<int> MapNone()
		{
			FSharpOption<int> fsharpOption = this.n;
			if (fsharpOption != null)
			{
				FSharpOption<int> fsharpOption2 = fsharpOption;
				int value = fsharpOption2.Value;
				return FSharpOption<int>.Some(Program.mapping@10-3(this, value));
			}
			return null;
		}

		// Token: 0x0400000A RID: 10
		internal FSharpOption<int> s;

		// Token: 0x0400000B RID: 11
		internal FSharpOption<int> n;

		// Token: 0x0400000C RID: 12
		[NoCompilerInlining]
		internal int f;
	}

	// Token: 0x0200000B RID: 11
	[MemoryDiagnoser(true)]
	[CompilationMapping(SourceConstructFlags.ObjectType)]
	[Serializable]
	public class InlineAndLambda
	{
		// Token: 0x06000026 RID: 38 RVA: 0x00002474 File Offset: 0x00000674
		public InlineAndLambda()
			: 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, "")]
		public int DefaultWithSingletonNone()
		{
			FSharpOption<int> fsharpOption = this.n;
			if (fsharpOption != null)
			{
				return fsharpOption.Value;
			}
			return 41 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		// Token: 0x06000028 RID: 40 RVA: 0x000024E8 File Offset: 0x000006E8
		[Benchmark(115, "")]
		public int DefaultWithNone()
		{
			FSharpOption<int> fsharpOption = this.n;
			if (fsharpOption != null)
			{
				return fsharpOption.Value;
			}
			return 42 + this.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		// Token: 0x06000029 RID: 41 RVA: 0x00002524 File Offset: 0x00000724
		[Benchmark(119, "")]
		public FSharpOption<int> MapSingletonSome()
		{
			FSharpOption<int> fsharpOption = this.s;
			if (fsharpOption != null)
			{
				FSharpOption<int> fsharpOption2 = fsharpOption;
				int x = fsharpOption2.Value;
				return FSharpOption<int>.Some(x + 43 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100)));
			}
			return null;
		}

		// Token: 0x0600002A RID: 42 RVA: 0x00002564 File Offset: 0x00000764
		[Benchmark(123, "")]
		public FSharpOption<int> MapSingletonNone()
		{
			FSharpOption<int> fsharpOption = this.n;
			if (fsharpOption != null)
			{
				FSharpOption<int> fsharpOption2 = fsharpOption;
				int x = fsharpOption2.Value;
				return FSharpOption<int>.Some(x + 44 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100)));
			}
			return null;
		}

		// Token: 0x0600002B RID: 43 RVA: 0x000025A4 File Offset: 0x000007A4
		[Benchmark(127, "")]
		public FSharpOption<int> MapSome()
		{
			FSharpOption<int> fsharpOption = this.s;
			if (fsharpOption != null)
			{
				FSharpOption<int> fsharpOption2 = fsharpOption;
				int x = fsharpOption2.Value;
				return FSharpOption<int>.Some(x + this.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100)));
			}
			return null;
		}

		// Token: 0x0600002C RID: 44 RVA: 0x000025E8 File Offset: 0x000007E8
		[Benchmark(131, "")]
		public FSharpOption<int> MapNone()
		{
			FSharpOption<int> fsharpOption = this.n;
			if (fsharpOption != null)
			{
				FSharpOption<int> fsharpOption2 = fsharpOption;
				int x = fsharpOption2.Value;
				return FSharpOption<int>.Some(x + this.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100)));
			}
			return null;
		}

		// Token: 0x0400000D RID: 13
		internal FSharpOption<int> s;

		// Token: 0x0400000E RID: 14
		internal FSharpOption<int> n;

		// Token: 0x0400000F RID: 15
		[NoCompilerInlining]
		internal int f;
	}

	// Token: 0x0200000C RID: 12
	[CompilationMapping(SourceConstructFlags.Module)]
	public static class InlineAndLambdaModule
	{
		// Token: 0x0600002D RID: 45 RVA: 0x0000262C File Offset: 0x0000082C
		[CompilationArgumentCounts(new int[] { 1, 1 })]
		public static a defaultWith<a>([InlineIfLambda] FSharpFunc<Unit, a> defThunk, FSharpOption<a> option)
		{
			if (option != null)
			{
				return option.Value;
			}
			return defThunk.Invoke(null);
		}

		// Token: 0x0600002E RID: 46 RVA: 0x00002654 File Offset: 0x00000854
		[CompilationArgumentCounts(new int[] { 1, 1 })]
		public static FSharpOption<b> map<a, b>([InlineIfLambda] FSharpFunc<a, b> mapping, FSharpOption<a> option)
		{
			if (option != null)
			{
				a x = option.Value;
				return FSharpOption<b>.Some(mapping.Invoke(x));
			}
			return null;
		}
	}

	// Token: 0x0200000D RID: 13
	[CompilationMapping(SourceConstructFlags.Module)]
	public static class InlineModule
	{
		// Token: 0x0600002F RID: 47 RVA: 0x00002680 File Offset: 0x00000880
		[CompilationArgumentCounts(new int[] { 1, 1 })]
		public static a defaultWith<a>(FSharpFunc<Unit, a> defThunk, FSharpOption<a> option)
		{
			if (option != null)
			{
				return option.Value;
			}
			return defThunk.Invoke(null);
		}

		// Token: 0x06000030 RID: 48 RVA: 0x000026A8 File Offset: 0x000008A8
		[CompilationArgumentCounts(new int[] { 1, 1 })]
		public static FSharpOption<b> map<a, b>(FSharpFunc<a, b> mapping, FSharpOption<a> option)
		{
			if (option != null)
			{
				a x = option.Value;
				return FSharpOption<b>.Some(mapping.Invoke(x));
			}
			return null;
		}
	}
}

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
Type Method Mean Error StdDev Median Gen0 Allocated
Current DefaultWithSingletonNone 1.3237 ns 0.0090 ns 0.0070 ns 1.3256 ns - -
Inline DefaultWithSingletonNone 0.2013 ns 0.0003 ns 0.0002 ns 0.2013 ns - -
InlineAndLambda DefaultWithSingletonNone 0.0033 ns 0.0005 ns 0.0004 ns 0.0031 ns - -
Current DefaultWithNone 2.9997 ns 0.0451 ns 0.0422 ns 3.0150 ns 0.0014 24 B
Inline DefaultWithNone 0.0039 ns 0.0008 ns 0.0007 ns 0.0038 ns - -
InlineAndLambda DefaultWithNone 0.0048 ns 0.0002 ns 0.0002 ns 0.0048 ns - -
Current MapSingletonSome 4.2985 ns 0.0974 ns 0.0813 ns 4.3439 ns 0.0014 24 B
Inline MapSingletonSome 2.5184 ns 0.0872 ns 0.0816 ns 2.5410 ns 0.0014 24 B
InlineAndLambda MapSingletonSome 2.1439 ns 0.0820 ns 0.1066 ns 2.1745 ns 0.0014 24 B
Current MapSingletonNone 1.1640 ns 0.0007 ns 0.0006 ns 1.1639 ns - -
Inline MapSingletonNone 0.0230 ns 0.0003 ns 0.0003 ns 0.0230 ns - -
InlineAndLambda MapSingletonNone 0.1058 ns 0.0275 ns 0.0283 ns 0.1110 ns - -
Current MapSome 5.8279 ns 0.0610 ns 0.0571 ns 5.8527 ns 0.0029 48 B
Inline MapSome 2.3016 ns 0.0696 ns 0.0651 ns 2.3151 ns 0.0014 24 B
InlineAndLambda MapSome 1.9041 ns 0.0216 ns 0.0180 ns 1.9062 ns 0.0014 24 B
Current MapNone 2.7781 ns 0.0257 ns 0.0215 ns 2.7681 ns 0.0014 24 B
Inline MapNone 0.2242 ns 0.0061 ns 0.0057 ns 0.2275 ns - -
InlineAndLambda MapNone 0.1775 ns 0.0420 ns 0.0712 ns 0.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 InlineIfLambda
let inline y () =
    if 1 / 1 = 1 then
        100 / 100
    else
        2 / 3

type T () =
    [<NoCompilerInlining>]
    let f = 10

    let n = 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:

public int DefaultWithStatusQuo()
{
    // Allocating a closure + virtual call in `Option.defaultWith`
    return OptionModule.DefaultWith<int>(new Program.DefaultWithStatusQuo@109(this), this.n);
}

public int DefaultWithInlined()
{
    FSharpOption<int> fsharpOption = this.n;
    if (fsharpOption != null)
    {
        return fsharpOption.Value;
    }
    // Only a singleton closure, or, as in this case, a static method call, and no virtual calls
    return Program.defThunk@5-2(this, null);
}

public int DefaultWithInlinedAndAttribute()
{
    FSharpOption<int> fsharpOption = this.n;
    if (fsharpOption != null)
    {
        return fsharpOption.Value;
    }
    // No closures or extra indirections
    return 41 + 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 owner March 18, 2023 13:53
@kerams
Copy link
Contributor Author

kerams commented Mar 18, 2023

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
Copy link
Member

vzarytovskii commented Mar 20, 2023

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
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
Copy link
Contributor Author

kerams commented Mar 20, 2023

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
Copy link
Member

vzarytovskii commented Mar 20, 2023

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
Member

EgorBo commented Mar 20, 2023

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
Contributor

dsyme commented Mar 20, 2023

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
Member

T-Gro commented Mar 20, 2023

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

Expected:
247808 Bytes
Actual:
247296 Bytes

@vzarytovskii
Copy link
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
Member

T-Gro commented Mar 24, 2023

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

@vzarytovskii vzarytovskii merged commit e549888 into dotnet:main Mar 25, 2023
@kerams kerams deleted the inline branch March 25, 2023 16:55
kant2002 pushed a commit to kant2002/fsharp that referenced this pull request Apr 1, 2023
* Inline Option module

* Do not inline Option.get

* Update expected trimmed size

* Update check.ps1

---------

Co-authored-by: Tomas Grosup <tomasgrosup@microsoft.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

5 participants