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 ValueOption module #15709

Merged
merged 3 commits into from
Jul 31, 2023
Merged

Inline ValueOption module #15709

merged 3 commits into from
Jul 31, 2023

Conversation

kerams
Copy link
Contributor

@kerams kerams commented Jul 31, 2023

Continuing with #14927 and https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1115-InlineIfLambda-in-FSharp-Core.md.

Benchmark 1

Code

open BenchmarkDotNet.Attributes
open BenchmarkDotNet.Configs

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

    let inline map mapping option =
        match option with
        | ValueNone -> ValueNone
        | ValueSome x -> ValueSome (mapping x)

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

    let inline map ([<InlineIfLambda>]mapping) option =
        match option with
        | ValueNone -> ValueNone
        | ValueSome x -> ValueSome (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 = ValueSome System.DateTime.Now.Day

    let n = ValueOption<int>.None

    [<NoCompilerInlining>]
    let f = 10

    [<Benchmark>]
    member _.DefaultWithSingletonNone() =
        ValueOption.defaultWith (fun () -> 41 + y ()) n

    [<Benchmark>]
    member _.DefaultWithNone() =
        ValueOption.defaultWith (fun () -> 42 + f + y ()) n

    [<Benchmark>]
    member _.MapSingletonSome() =
        ValueOption.map (fun x -> x + 43 + y ()) s

    [<Benchmark>]
    member _.MapSingletonNone() =
        ValueOption.map (fun x -> x + 44 + y ()) n

    [<Benchmark>]
    member _.MapSome() =
        ValueOption.map (fun x -> x + f + y ()) s

    [<Benchmark>]
    member _.MapNone() =
        ValueOption.map (fun x -> x + f + y ()) n

[<MemoryDiagnoser>]
type Inline () =

    let s = ValueSome System.DateTime.Now.Day

    let n = ValueOption<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 = ValueSome System.DateTime.Now.Day

    let n = ValueOption<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;

[CompilationMapping(SourceConstructFlags.Module)]
public static class Program
{
	public static int y()
	{
		if (1 / 1 == 1)
		{
			return 100 / 100;
		}
		return 2 / 3;
	}

	[CompilerGenerated]
	internal static int defThunk@5(Unit unitVar0)
	{
		return 41 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
	}

	[CompilerGenerated]
	internal static int defThunk@5-1(Program.Inline _, Unit unitVar0)
	{
		return 42 + _.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
	}

	[CompilerGenerated]
	internal static int mapping@10(int x)
	{
		return x + 43 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
	}

	[CompilerGenerated]
	internal static int mapping@10-1(int x)
	{
		return x + 44 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
	}

	[CompilerGenerated]
	internal static int mapping@10-2(Program.Inline _, int x)
	{
		return x + _.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
	}

	[CompilerGenerated]
	internal static int mapping@10-3(Program.Inline _, int x)
	{
		return x + _.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
	}

	[CompilationMapping(SourceConstructFlags.Value)]
	internal static Summary[] arg@1
	{
		get
		{
			return $Program.arg@1;
		}
	}

	[MemoryDiagnoser(true)]
	[CompilationMapping(SourceConstructFlags.ObjectType)]
	[Serializable]
	public class Current
	{
		public Current()
			: this()
		{
			this.s = FSharpValueOption<int>.NewValueSome(DateTime.Now.Day);
			this.n = FSharpValueOption<int>.ValueNone;
			this.f = 10;
		}

		[Benchmark(43, "C:\\code\\Test\\Bench\\Program.fs")]
		public int DefaultWithSingletonNone()
		{
			return ValueOption.DefaultWith<int>(Program.DefaultWithSingletonNone@45.@_instance, this.n);
		}

		[Benchmark(47, "C:\\code\\Test\\Bench\\Program.fs")]
		public int DefaultWithNone()
		{
			return ValueOption.DefaultWith<int>(new Program.DefaultWithNone@49(this), this.n);
		}

		[Benchmark(51, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<int> MapSingletonSome()
		{
			return ValueOption.Map<int, int>(Program.MapSingletonSome@53.@_instance, this.s);
		}

		[Benchmark(55, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<int> MapSingletonNone()
		{
			return ValueOption.Map<int, int>(Program.MapSingletonNone@57.@_instance, this.n);
		}

		[Benchmark(59, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<int> MapSome()
		{
			return ValueOption.Map<int, int>(new Program.MapSome@61(this), this.s);
		}

		[Benchmark(63, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<int> MapNone()
		{
			return ValueOption.Map<int, int>(new Program.MapNone@65(this), this.n);
		}

		internal FSharpValueOption<int> s;

		internal FSharpValueOption<int> n;

		[NoCompilerInlining]
		internal int f;
	}

	[Serializable]
	internal sealed class DefaultWithSingletonNone@45 : FSharpFunc<Unit, int>
	{
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal DefaultWithSingletonNone@45()
		{
		}

		public override int Invoke(Unit unitVar0)
		{
			return 41 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		// Note: this type is marked as 'beforefieldinit'.
		static DefaultWithSingletonNone@45()
		{
		}

		internal static readonly Program.DefaultWithSingletonNone@45 @_instance = new Program.DefaultWithSingletonNone@45();
	}

	[Serializable]
	internal sealed class DefaultWithNone@49 : FSharpFunc<Unit, int>
	{
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal DefaultWithNone@49(Program.Current _)
		{
			this._ = _;
		}

		public override int Invoke(Unit unitVar0)
		{
			return 42 + this._.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		public Program.Current _;
	}

	[Serializable]
	internal sealed class MapSingletonSome@53 : FSharpFunc<int, int>
	{
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal MapSingletonSome@53()
		{
		}

		public override int Invoke(int x)
		{
			return x + 43 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		// Note: this type is marked as 'beforefieldinit'.
		static MapSingletonSome@53()
		{
		}

		internal static readonly Program.MapSingletonSome@53 @_instance = new Program.MapSingletonSome@53();
	}

	[Serializable]
	internal sealed class MapSingletonNone@57 : FSharpFunc<int, int>
	{
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal MapSingletonNone@57()
		{
		}

		public override int Invoke(int x)
		{
			return x + 44 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		// Note: this type is marked as 'beforefieldinit'.
		static MapSingletonNone@57()
		{
		}

		internal static readonly Program.MapSingletonNone@57 @_instance = new Program.MapSingletonNone@57();
	}

	[Serializable]
	internal sealed class MapSome@61 : FSharpFunc<int, int>
	{
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal MapSome@61(Program.Current _)
		{
			this._ = _;
		}

		public override int Invoke(int x)
		{
			return x + this._.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		public Program.Current _;
	}

	[Serializable]
	internal sealed class MapNone@65 : FSharpFunc<int, int>
	{
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal MapNone@65(Program.Current _)
		{
			this._ = _;
		}

		public override int Invoke(int x)
		{
			return x + this._.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		public Program.Current _;
	}

	[MemoryDiagnoser(true)]
	[CompilationMapping(SourceConstructFlags.ObjectType)]
	[Serializable]
	public class Inline
	{
		public Inline()
			: this()
		{
			this.s = FSharpValueOption<int>.NewValueSome(DateTime.Now.Day);
			this.n = FSharpValueOption<int>.ValueNone;
			this.f = 10;
		}

		[Benchmark(77, "C:\\code\\Test\\Bench\\Program.fs")]
		public int DefaultWithSingletonNone()
		{
			FSharpValueOption<int> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				return fsharpValueOption.Item;
			}
			return Program.defThunk@5(null);
		}

		[Benchmark(81, "C:\\code\\Test\\Bench\\Program.fs")]
		public int DefaultWithNone()
		{
			FSharpValueOption<int> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				return fsharpValueOption.Item;
			}
			return Program.defThunk@5-1(this, null);
		}

		[Benchmark(85, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<int> MapSingletonSome()
		{
			FSharpValueOption<int> fsharpValueOption = this.s;
			if (fsharpValueOption.Tag == 1)
			{
				int item = fsharpValueOption.Item;
				return FSharpValueOption<int>.NewValueSome(Program.mapping@10(item));
			}
			return FSharpValueOption<int>.ValueNone;
		}

		[Benchmark(89, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<int> MapSingletonNone()
		{
			FSharpValueOption<int> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				int item = fsharpValueOption.Item;
				return FSharpValueOption<int>.NewValueSome(Program.mapping@10-1(item));
			}
			return FSharpValueOption<int>.ValueNone;
		}

		[Benchmark(93, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<int> MapSome()
		{
			FSharpValueOption<int> fsharpValueOption = this.s;
			if (fsharpValueOption.Tag == 1)
			{
				int item = fsharpValueOption.Item;
				return FSharpValueOption<int>.NewValueSome(Program.mapping@10-2(this, item));
			}
			return FSharpValueOption<int>.ValueNone;
		}

		[Benchmark(97, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<int> MapNone()
		{
			FSharpValueOption<int> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				int item = fsharpValueOption.Item;
				return FSharpValueOption<int>.NewValueSome(Program.mapping@10-3(this, item));
			}
			return FSharpValueOption<int>.ValueNone;
		}

		internal FSharpValueOption<int> s;

		internal FSharpValueOption<int> n;

		[NoCompilerInlining]
		internal int f;
	}

	[MemoryDiagnoser(true)]
	[CompilationMapping(SourceConstructFlags.ObjectType)]
	[Serializable]
	public class InlineAndLambda
	{
		public InlineAndLambda()
			: this()
		{
			this.s = FSharpValueOption<int>.NewValueSome(DateTime.Now.Day);
			this.n = FSharpValueOption<int>.ValueNone;
			this.f = 10;
		}

		[Benchmark(111, "C:\\code\\Test\\Bench\\Program.fs")]
		public int DefaultWithSingletonNone()
		{
			FSharpValueOption<int> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				return fsharpValueOption.Item;
			}
			return 41 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		[Benchmark(115, "C:\\code\\Test\\Bench\\Program.fs")]
		public int DefaultWithNone()
		{
			FSharpValueOption<int> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				return fsharpValueOption.Item;
			}
			return 42 + this.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100));
		}

		[Benchmark(119, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<int> MapSingletonSome()
		{
			FSharpValueOption<int> fsharpValueOption = this.s;
			if (fsharpValueOption.Tag == 1)
			{
				int x = fsharpValueOption.Item;
				return FSharpValueOption<int>.NewValueSome(x + 43 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100)));
			}
			return FSharpValueOption<int>.ValueNone;
		}

		[Benchmark(123, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<int> MapSingletonNone()
		{
			FSharpValueOption<int> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				int x = fsharpValueOption.Item;
				return FSharpValueOption<int>.NewValueSome(x + 44 + ((1 / 1 != 1) ? (2 / 3) : (100 / 100)));
			}
			return FSharpValueOption<int>.ValueNone;
		}

		[Benchmark(127, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<int> MapSome()
		{
			FSharpValueOption<int> fsharpValueOption = this.s;
			if (fsharpValueOption.Tag == 1)
			{
				int x = fsharpValueOption.Item;
				return FSharpValueOption<int>.NewValueSome(x + this.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100)));
			}
			return FSharpValueOption<int>.ValueNone;
		}

		[Benchmark(131, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<int> MapNone()
		{
			FSharpValueOption<int> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				int x = fsharpValueOption.Item;
				return FSharpValueOption<int>.NewValueSome(x + this.f + ((1 / 1 != 1) ? (2 / 3) : (100 / 100)));
			}
			return FSharpValueOption<int>.ValueNone;
		}

		internal FSharpValueOption<int> s;

		internal FSharpValueOption<int> n;

		[NoCompilerInlining]
		internal int f;
	}

	[CompilationMapping(SourceConstructFlags.Module)]
	public static class InlineAndLambdaModule
	{
		[CompilationArgumentCounts(new int[] { 1, 1 })]
		public static a defaultWith<a>([InlineIfLambda] FSharpFunc<Unit, a> defThunk, FSharpValueOption<a> option)
		{
			if (option.Tag == 1)
			{
				return option.Item;
			}
			return defThunk.Invoke(null);
		}

		[CompilationArgumentCounts(new int[] { 1, 1 })]
		public static FSharpValueOption<b> map<a, b>([InlineIfLambda] FSharpFunc<a, b> mapping, FSharpValueOption<a> option)
		{
			if (option.Tag == 1)
			{
				a x = option.Item;
				return FSharpValueOption<b>.NewValueSome(mapping.Invoke(x));
			}
			return FSharpValueOption<b>.ValueNone;
		}
	}

	[CompilationMapping(SourceConstructFlags.Module)]
	public static class InlineModule
	{
		[CompilationArgumentCounts(new int[] { 1, 1 })]
		public static a defaultWith<a>(FSharpFunc<Unit, a> defThunk, FSharpValueOption<a> option)
		{
			if (option.Tag == 1)
			{
				return option.Item;
			}
			return defThunk.Invoke(null);
		}

		[CompilationArgumentCounts(new int[] { 1, 1 })]
		public static FSharpValueOption<b> map<a, b>(FSharpFunc<a, b> mapping, FSharpValueOption<a> option)
		{
			if (option.Tag == 1)
			{
				a x = option.Item;
				return FSharpValueOption<b>.NewValueSome(mapping.Invoke(x));
			}
			return FSharpValueOption<b>.ValueNone;
		}
	}
}

BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1992/22H2/2022Update/SunValley2)
AMD Ryzen 9 7900, 1 CPU, 24 logical and 12 physical cores
.NET SDK=8.0.100-preview.6.23330.14
  [Host]     : .NET 8.0.0 (8.0.23.32907), X64 RyuJIT AVX2 DEBUG
  DefaultJob : .NET 8.0.0 (8.0.23.32907), X64 RyuJIT AVX2

Type Method Mean Error StdDev Gen0 Allocated
Current DefaultWithSingletonNone 0.6079 ns 0.0107 ns 0.0100 ns - -
Inline DefaultWithSingletonNone 0.0060 ns 0.0014 ns 0.0013 ns - -
InlineAndLambda DefaultWithSingletonNone 0.1994 ns 0.0096 ns 0.0090 ns - -
Current DefaultWithNone 3.3048 ns 0.0588 ns 0.0550 ns 0.0014 24 B
Inline DefaultWithNone 0.1957 ns 0.0078 ns 0.0069 ns - -
InlineAndLambda DefaultWithNone 0.1968 ns 0.0097 ns 0.0090 ns - -
Current MapSingletonSome 3.2679 ns 0.0240 ns 0.0212 ns - -
Inline MapSingletonSome 2.8726 ns 0.0082 ns 0.0076 ns - -
InlineAndLambda MapSingletonSome 2.8581 ns 0.0035 ns 0.0028 ns - -
Current MapSingletonNone 0.5504 ns 0.0037 ns 0.0033 ns - -
Inline MapSingletonNone 0.2003 ns 0.0063 ns 0.0059 ns - -
InlineAndLambda MapSingletonNone 0.2014 ns 0.0031 ns 0.0028 ns - -
Current MapSome 4.6083 ns 0.0375 ns 0.0350 ns 0.0014 24 B
Inline MapSome 3.0991 ns 0.0823 ns 0.0979 ns - -
InlineAndLambda MapSome 3.0532 ns 0.0084 ns 0.0075 ns - -
Current MapNone 3.0646 ns 0.0280 ns 0.0262 ns 0.0014 24 B
Inline MapNone 0.2004 ns 0.0015 ns 0.0012 ns - -
InlineAndLambda MapNone 0.2042 ns 0.0032 ns 0.0026 ns - -

Benchmark 2

Code

open BenchmarkDotNet.Attributes
open BenchmarkDotNet.Configs

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

    let inline map mapping option =
        match option with
        | ValueNone -> ValueNone
        | ValueSome x -> ValueSome (mapping x)

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

    let inline map ([<InlineIfLambda>]mapping) option =
        match option with
        | ValueNone -> ValueNone
        | ValueSome x -> ValueSome (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
        100L / 100L
    else
        2L / 3L

[<MemoryDiagnoser>]
type Current () =

    let s = ValueSome (int64 System.DateTime.Now.Day)

    let n = ValueOption<int64>.None

    [<NoCompilerInlining>]
    let f = 10L

    [<Benchmark>]
    member _.DefaultWithSingletonNone() =
        ValueOption.defaultWith (fun () -> 41L + y ()) n

    [<Benchmark>]
    member _.DefaultWithNone() =
        ValueOption.defaultWith (fun () -> 42L + f + y ()) n

    [<Benchmark>]
    member _.MapSingletonSome() =
        ValueOption.map (fun x -> x + 43L + y ()) s

    [<Benchmark>]
    member _.MapSingletonNone() =
        ValueOption.map (fun x -> x + 44L + y ()) n

    [<Benchmark>]
    member _.MapSome() =
        ValueOption.map (fun x -> x + f + y ()) s

    [<Benchmark>]
    member _.MapNone() =
        ValueOption.map (fun x -> x + f + y ()) n

[<MemoryDiagnoser>]
type Inline () =

    let s = ValueSome (int64 System.DateTime.Now.Day)

    let n = ValueOption<int64>.None

    [<NoCompilerInlining>]
    let f = 10L

    [<Benchmark>]
    member _.DefaultWithSingletonNone() =
        Inline.defaultWith (fun () -> 41L + y ()) n

    [<Benchmark>]
    member _.DefaultWithNone() =
        Inline.defaultWith (fun () -> 42L + f + y ()) n

    [<Benchmark>]
    member _.MapSingletonSome() =
        Inline.map (fun x -> x + 43L + y ()) s

    [<Benchmark>]
    member _.MapSingletonNone() =
        Inline.map (fun x -> x + 44L + 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 = ValueSome (int64 System.DateTime.Now.Day)

    let n = ValueOption<int64>.None

    [<NoCompilerInlining>]
    let f = 10L

    [<Benchmark>]
    member _.DefaultWithSingletonNone() =
        InlineAndLambda.defaultWith (fun () -> 41L + y ()) n

    [<Benchmark>]
    member _.DefaultWithNone() =
        InlineAndLambda.defaultWith (fun () -> 42L + f + y ()) n

    [<Benchmark>]
    member _.MapSingletonSome() =
        InlineAndLambda.map (fun x -> x + 43L + y ()) s

    [<Benchmark>]
    member _.MapSingletonNone() =
        InlineAndLambda.map (fun x -> x + 44L + 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;

[CompilationMapping(SourceConstructFlags.Module)]
public static class Program
{
	public static long y()
	{
		if (1 / 1 == 1)
		{
			return 100L / 100L;
		}
		return 2L / 3L;
	}

	[CompilerGenerated]
	internal static long defThunk@5(Unit unitVar0)
	{
		return 41L + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
	}

	[CompilerGenerated]
	internal static long defThunk@5-1(Program.Inline _, Unit unitVar0)
	{
		return 42L + _.f + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
	}

	[CompilerGenerated]
	internal static long mapping@10(long x)
	{
		return x + 43L + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
	}

	[CompilerGenerated]
	internal static long mapping@10-1(long x)
	{
		return x + 44L + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
	}

	[CompilerGenerated]
	internal static long mapping@10-2(Program.Inline _, long x)
	{
		return x + _.f + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
	}

	[CompilerGenerated]
	internal static long mapping@10-3(Program.Inline _, long x)
	{
		return x + _.f + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
	}

	[CompilationMapping(SourceConstructFlags.Value)]
	internal static Summary[] arg@1
	{
		get
		{
			return $Program.arg@1;
		}
	}

	[MemoryDiagnoser(true)]
	[CompilationMapping(SourceConstructFlags.ObjectType)]
	[Serializable]
	public class Current
	{
		public Current()
			: this()
		{
			this.s = FSharpValueOption<long>.NewValueSome((long)DateTime.Now.Day);
			this.n = FSharpValueOption<long>.ValueNone;
			this.f = 10L;
		}

		[Benchmark(43, "C:\\code\\Test\\Bench\\Program.fs")]
		public long DefaultWithSingletonNone()
		{
			return ValueOption.DefaultWith<long>(Program.DefaultWithSingletonNone@45.@_instance, this.n);
		}

		[Benchmark(47, "C:\\code\\Test\\Bench\\Program.fs")]
		public long DefaultWithNone()
		{
			return ValueOption.DefaultWith<long>(new Program.DefaultWithNone@49(this), this.n);
		}

		[Benchmark(51, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<long> MapSingletonSome()
		{
			return ValueOption.Map<long, long>(Program.MapSingletonSome@53.@_instance, this.s);
		}

		[Benchmark(55, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<long> MapSingletonNone()
		{
			return ValueOption.Map<long, long>(Program.MapSingletonNone@57.@_instance, this.n);
		}

		[Benchmark(59, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<long> MapSome()
		{
			return ValueOption.Map<long, long>(new Program.MapSome@61(this), this.s);
		}

		[Benchmark(63, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<long> MapNone()
		{
			return ValueOption.Map<long, long>(new Program.MapNone@65(this), this.n);
		}

		internal FSharpValueOption<long> s;

		internal FSharpValueOption<long> n;

		[NoCompilerInlining]
		internal long f;
	}

	[Serializable]
	internal sealed class DefaultWithSingletonNone@45 : FSharpFunc<Unit, long>
	{
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal DefaultWithSingletonNone@45()
		{
		}

		public override long Invoke(Unit unitVar0)
		{
			return 41L + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
		}

		// Note: this type is marked as 'beforefieldinit'.
		static DefaultWithSingletonNone@45()
		{
		}

		internal static readonly Program.DefaultWithSingletonNone@45 @_instance = new Program.DefaultWithSingletonNone@45();
	}

	[Serializable]
	internal sealed class DefaultWithNone@49 : FSharpFunc<Unit, long>
	{
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal DefaultWithNone@49(Program.Current _)
		{
			this._ = _;
		}

		public override long Invoke(Unit unitVar0)
		{
			return 42L + this._.f + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
		}

		public Program.Current _;
	}

	[Serializable]
	internal sealed class MapSingletonSome@53 : FSharpFunc<long, long>
	{
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal MapSingletonSome@53()
		{
		}

		public override long Invoke(long x)
		{
			return x + 43L + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
		}

		// Note: this type is marked as 'beforefieldinit'.
		static MapSingletonSome@53()
		{
		}

		internal static readonly Program.MapSingletonSome@53 @_instance = new Program.MapSingletonSome@53();
	}

	[Serializable]
	internal sealed class MapSingletonNone@57 : FSharpFunc<long, long>
	{
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal MapSingletonNone@57()
		{
		}

		public override long Invoke(long x)
		{
			return x + 44L + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
		}

		// Note: this type is marked as 'beforefieldinit'.
		static MapSingletonNone@57()
		{
		}

		internal static readonly Program.MapSingletonNone@57 @_instance = new Program.MapSingletonNone@57();
	}

	[Serializable]
	internal sealed class MapSome@61 : FSharpFunc<long, long>
	{
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal MapSome@61(Program.Current _)
		{
			this._ = _;
		}

		public override long Invoke(long x)
		{
			return x + this._.f + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
		}

		public Program.Current _;
	}

	[Serializable]
	internal sealed class MapNone@65 : FSharpFunc<long, long>
	{
		[CompilerGenerated]
		[DebuggerNonUserCode]
		internal MapNone@65(Program.Current _)
		{
			this._ = _;
		}

		public override long Invoke(long x)
		{
			return x + this._.f + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
		}

		public Program.Current _;
	}

	[MemoryDiagnoser(true)]
	[CompilationMapping(SourceConstructFlags.ObjectType)]
	[Serializable]
	public class Inline
	{
		public Inline()
			: this()
		{
			this.s = FSharpValueOption<long>.NewValueSome((long)DateTime.Now.Day);
			this.n = FSharpValueOption<long>.ValueNone;
			this.f = 10L;
		}

		[Benchmark(77, "C:\\code\\Test\\Bench\\Program.fs")]
		public long DefaultWithSingletonNone()
		{
			FSharpValueOption<long> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				return fsharpValueOption.Item;
			}
			return Program.defThunk@5(null);
		}

		[Benchmark(81, "C:\\code\\Test\\Bench\\Program.fs")]
		public long DefaultWithNone()
		{
			FSharpValueOption<long> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				return fsharpValueOption.Item;
			}
			return Program.defThunk@5-1(this, null);
		}

		[Benchmark(85, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<long> MapSingletonSome()
		{
			FSharpValueOption<long> fsharpValueOption = this.s;
			if (fsharpValueOption.Tag == 1)
			{
				long item = fsharpValueOption.Item;
				return FSharpValueOption<long>.NewValueSome(Program.mapping@10(item));
			}
			return FSharpValueOption<long>.ValueNone;
		}

		[Benchmark(89, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<long> MapSingletonNone()
		{
			FSharpValueOption<long> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				long item = fsharpValueOption.Item;
				return FSharpValueOption<long>.NewValueSome(Program.mapping@10-1(item));
			}
			return FSharpValueOption<long>.ValueNone;
		}

		[Benchmark(93, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<long> MapSome()
		{
			FSharpValueOption<long> fsharpValueOption = this.s;
			if (fsharpValueOption.Tag == 1)
			{
				long item = fsharpValueOption.Item;
				return FSharpValueOption<long>.NewValueSome(Program.mapping@10-2(this, item));
			}
			return FSharpValueOption<long>.ValueNone;
		}

		[Benchmark(97, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<long> MapNone()
		{
			FSharpValueOption<long> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				long item = fsharpValueOption.Item;
				return FSharpValueOption<long>.NewValueSome(Program.mapping@10-3(this, item));
			}
			return FSharpValueOption<long>.ValueNone;
		}

		internal FSharpValueOption<long> s;

		internal FSharpValueOption<long> n;

		[NoCompilerInlining]
		internal long f;
	}

	[MemoryDiagnoser(true)]
	[CompilationMapping(SourceConstructFlags.ObjectType)]
	[Serializable]
	public class InlineAndLambda
	{
		public InlineAndLambda()
			: this()
		{
			this.s = FSharpValueOption<long>.NewValueSome((long)DateTime.Now.Day);
			this.n = FSharpValueOption<long>.ValueNone;
			this.f = 10L;
		}

		[Benchmark(111, "C:\\code\\Test\\Bench\\Program.fs")]
		public long DefaultWithSingletonNone()
		{
			FSharpValueOption<long> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				return fsharpValueOption.Item;
			}
			return 41L + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
		}

		[Benchmark(115, "C:\\code\\Test\\Bench\\Program.fs")]
		public long DefaultWithNone()
		{
			FSharpValueOption<long> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				return fsharpValueOption.Item;
			}
			return 42L + this.f + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L));
		}

		[Benchmark(119, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<long> MapSingletonSome()
		{
			FSharpValueOption<long> fsharpValueOption = this.s;
			if (fsharpValueOption.Tag == 1)
			{
				long x = fsharpValueOption.Item;
				return FSharpValueOption<long>.NewValueSome(x + 43L + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L)));
			}
			return FSharpValueOption<long>.ValueNone;
		}

		[Benchmark(123, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<long> MapSingletonNone()
		{
			FSharpValueOption<long> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				long x = fsharpValueOption.Item;
				return FSharpValueOption<long>.NewValueSome(x + 44L + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L)));
			}
			return FSharpValueOption<long>.ValueNone;
		}

		[Benchmark(127, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<long> MapSome()
		{
			FSharpValueOption<long> fsharpValueOption = this.s;
			if (fsharpValueOption.Tag == 1)
			{
				long x = fsharpValueOption.Item;
				return FSharpValueOption<long>.NewValueSome(x + this.f + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L)));
			}
			return FSharpValueOption<long>.ValueNone;
		}

		[Benchmark(131, "C:\\code\\Test\\Bench\\Program.fs")]
		public FSharpValueOption<long> MapNone()
		{
			FSharpValueOption<long> fsharpValueOption = this.n;
			if (fsharpValueOption.Tag == 1)
			{
				long x = fsharpValueOption.Item;
				return FSharpValueOption<long>.NewValueSome(x + this.f + ((1 / 1 != 1) ? (2L / 3L) : (100L / 100L)));
			}
			return FSharpValueOption<long>.ValueNone;
		}

		internal FSharpValueOption<long> s;

		internal FSharpValueOption<long> n;

		[NoCompilerInlining]
		internal long f;
	}

	[CompilationMapping(SourceConstructFlags.Module)]
	public static class InlineAndLambdaModule
	{
		[CompilationArgumentCounts(new int[] { 1, 1 })]
		public static a defaultWith<a>([InlineIfLambda] FSharpFunc<Unit, a> defThunk, FSharpValueOption<a> option)
		{
			if (option.Tag == 1)
			{
				return option.Item;
			}
			return defThunk.Invoke(null);
		}

		[CompilationArgumentCounts(new int[] { 1, 1 })]
		public static FSharpValueOption<b> map<a, b>([InlineIfLambda] FSharpFunc<a, b> mapping, FSharpValueOption<a> option)
		{
			if (option.Tag == 1)
			{
				a x = option.Item;
				return FSharpValueOption<b>.NewValueSome(mapping.Invoke(x));
			}
			return FSharpValueOption<b>.ValueNone;
		}
	}

	[CompilationMapping(SourceConstructFlags.Module)]
	public static class InlineModule
	{
		[CompilationArgumentCounts(new int[] { 1, 1 })]
		public static a defaultWith<a>(FSharpFunc<Unit, a> defThunk, FSharpValueOption<a> option)
		{
			if (option.Tag == 1)
			{
				return option.Item;
			}
			return defThunk.Invoke(null);
		}

		[CompilationArgumentCounts(new int[] { 1, 1 })]
		public static FSharpValueOption<b> map<a, b>(FSharpFunc<a, b> mapping, FSharpValueOption<a> option)
		{
			if (option.Tag == 1)
			{
				a x = option.Item;
				return FSharpValueOption<b>.NewValueSome(mapping.Invoke(x));
			}
			return FSharpValueOption<b>.ValueNone;
		}
	}
}

BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1992/22H2/2022Update/SunValley2)
AMD Ryzen 9 7900, 1 CPU, 24 logical and 12 physical cores
.NET SDK=8.0.100-preview.6.23330.14
  [Host]     : .NET 8.0.0 (8.0.23.32907), X64 RyuJIT AVX2 DEBUG
  DefaultJob : .NET 8.0.0 (8.0.23.32907), X64 RyuJIT AVX2

Type Method Mean Error StdDev Median Gen0 Allocated
Current DefaultWithSingletonNone 30.7785 ns 0.0699 ns 0.0546 ns 30.7517 ns - -
Inline DefaultWithSingletonNone 0.0061 ns 0.0023 ns 0.0020 ns 0.0065 ns - -
InlineAndLambda DefaultWithSingletonNone 0.0112 ns 0.0068 ns 0.0064 ns 0.0081 ns - -
Current DefaultWithNone 32.9885 ns 0.1077 ns 0.0954 ns 32.9771 ns 0.0014 24 B
Inline DefaultWithNone 0.0079 ns 0.0017 ns 0.0014 ns 0.0079 ns - -
InlineAndLambda DefaultWithNone 0.1987 ns 0.0069 ns 0.0065 ns 0.1995 ns - -
Current MapSingletonSome 31.2620 ns 0.0629 ns 0.0491 ns 31.2560 ns - -
Inline MapSingletonSome 0.2333 ns 0.0028 ns 0.0024 ns 0.2324 ns - -
InlineAndLambda MapSingletonSome 0.2293 ns 0.0026 ns 0.0025 ns 0.2294 ns - -
Current MapSingletonNone 31.4863 ns 0.1252 ns 0.1046 ns 31.4460 ns - -
Inline MapSingletonNone 0.0468 ns 0.0018 ns 0.0016 ns 0.0467 ns - -
InlineAndLambda MapSingletonNone 0.0318 ns 0.0024 ns 0.0022 ns 0.0306 ns - -
Current MapSome 35.9461 ns 0.0499 ns 0.0417 ns 35.9411 ns 0.0014 24 B
Inline MapSome 0.2216 ns 0.0044 ns 0.0041 ns 0.2203 ns - -
InlineAndLambda MapSome 0.2208 ns 0.0053 ns 0.0049 ns 0.2192 ns - -
Current MapNone 38.2091 ns 0.1792 ns 0.1589 ns 38.1327 ns 0.0014 24 B
Inline MapNone 0.2368 ns 0.0016 ns 0.0015 ns 0.2365 ns - -
InlineAndLambda MapNone 0.0373 ns 0.0028 ns 0.0026 ns 0.0380 ns - -

Analysis

My conclusion is identical to #14927 - do use both inline and InlineIfLambda. However, there is also one important anomaly to be aware of, as highlighted between the 2 sets of benchmarks.

ValueOption<int> functions are namely surprisingly slow, to the point that they might be slower than Option<int> despite the latter having to allocate. This is due to a deficiency in JIT, where it produces suboptimal machine code when structs are smaller than the register size. When we switch to ValueOption<int64>, ValueOption is clearly the winner compared to Option. The difference between status quo and inlined functions becomes even more stark (several orders of magnitude), because the non-inlined functions take a significant performance hit with int64.

This is something to keep in mind when benchmarking fsharp/fslang-suggestions#739.

@kerams kerams requested a review from a team as a code owner July 31, 2023 08:25
@T-Gro
Copy link
Member

T-Gro commented Jul 31, 2023

For the default scenarios (DefaultWithSingletonNone, DefaultWithNone), doing InlineAndLambda is doing worse than Inline.
Wouldn't it then make sense to have the function inline, but not the argument?

@kerams
Copy link
Contributor Author

kerams commented Jul 31, 2023

I don't know. It's not massively slower in absolute terms, and who knows what the JIT would produce if the caller function and/or lambda bodies were larger. I'd use the attribute too, if only for consistency's sake.

@T-Gro
Copy link
Member

T-Gro commented Jul 31, 2023

If the bodies were larger, it should probably also stay un-inlined.
You are right the absolute diff is small, and compared to non-inlined scenario this is a massive improvement either way.

Just the relative diff stands out in this case, whereas elsewhere the lambda inlining was either superior or at least on par.
image

@vzarytovskii
Copy link
Member

I think JIT should be fine in general. It should'be be a HUGE lambda for it to not inline. We probably can remove IILAttribute for the singleton version, with comment that it was slower, I guess?

@kerams
Copy link
Contributor Author

kerams commented Jul 31, 2023

No, because we're talking about one ValueOption.defaultWith used either with a singleton closure or one that captures values.

@vzarytovskii
Copy link
Member

No, because we're talking about one ValueOption.defaultWith used either with a singleton closure or one that captures values.

Wait, that's what I meant, remove attribute for the defaultWith alltogether.

@kerams
Copy link
Contributor Author

kerams commented Jul 31, 2023

Ok, I was confused by the mention of singleton.

@vzarytovskii
Copy link
Member

Ok, I was confused by the mention of singleton.

I'm a bit slow today, so it's possible that I thought of one thing, and wrote a different one.

@vzarytovskii vzarytovskii enabled auto-merge (squash) July 31, 2023 14:29
@T-Gro
Copy link
Member

T-Gro commented Jul 31, 2023

I ran the same benchmark and added also cases with a lot larger lambda body, as well as a case which maps over ValueSome (therefore never invoking the lambda/code).

The Inline version (without InlineIfLambda attr) was always slightly faster.

=> Would prefer to have the attr removed (as is now with the latest commit)

@vzarytovskii vzarytovskii merged commit 2580bfd into dotnet:main Jul 31, 2023
24 checks passed
@kerams kerams deleted the vo branch July 31, 2023 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

3 participants