Skip to content

Instantly share code, notes, and snippets.

@Horusiath
Last active July 1, 2018 18:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Horusiath/8ab0da6fdac1d6619c16cb0dfc33c5fa to your computer and use it in GitHub Desktop.
Save Horusiath/8ab0da6fdac1d6619c16cb0dfc33c5fa to your computer and use it in GitHub Desktop.
Delegates vs. generic readonly structs
using System;
using BenchmarkDotNet.Attributes;
namespace Tachyon.Benchmarks.Core
{
// Instead of delegate, use interface.
interface IFunc<in I, out O>
{
O Invoke(I arg);
}
[Config(typeof(TachyonConfig))]
public class DelegationPatternBenchmark
{
public const int OPS = 1_000_000;
public readonly int Context = 10;
[Benchmark(Baseline = true)]
public int Call_direct()
{
var sum = 0;
for (int i = 0; i < OPS; i++)
{
sum += Context + i;
}
return sum;
}
[Benchmark]
public int Call_via_struct()
{
var sum = 0;
for (int i = 0; i < OPS; i++)
{
var r = new MyFunc(i);
sum += RunStructure(r);
}
return sum;
}
[Benchmark]
public int Call_via_delegate()
{
var sum = 0;
for (int i = 0; i < OPS; i++)
{
sum += RunDelegate(context => i + context);
}
return sum;
}
// Normally C# generates an anonymous class for capturing lambda delegates.
// Here we use structs and interfaces instead.
internal readonly struct MyFunc : IFunc<int, int>
{
public readonly int message;
public MyFunc(int message)
{
this.message = message;
}
public int Invoke(int context) => context + message;
}
// Run the our own version of "capturing lambda" which uses structs over interface.
private int RunStructure<T>(in T call) where T : struct, IFunc<int, int> =>
call.Invoke(Context);
// Run standard C# delegate.
private int RunDelegate(Func<int, int> call) =>
call(Context);
}
}
BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i5-4430 CPU 3.00GHz(Haswell), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK = 2.1.300-preview1-008174

[Host]     : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
DefaultJob : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
Method Mean Error StdDev Scaled ScaledSD Gen 0 Allocated
Call_direct 670.5 us 3.196 us 2.990 us 1.00 0.00 - 0 B
Call_via_struct 669.3 us 1.774 us 1.659 us 1.00 0.00 - 0 B
Call_via_delegate 9,832.2 us 196.651 us 193.137 us 14.66 0.29 20343.7500 64000024 B
open BenchmarkDotNet.Configs
open BenchmarkDotNet.Diagnosers
open BenchmarkDotNet.Exporters
open BenchmarkDotNet.Attributes
type DiagnosedConfig() as this =
inherit ManualConfig()
do
this.Add(MemoryDiagnoser())
this.Add(MarkdownExporter.GitHub)
[<Interface>]
type IFSharpFunc<'t,'u> =
abstract Invoke: 't -> 'u
[<Struct>]
type CapturedFunc(capturedValue: int) =
interface IFSharpFunc<int,int> with
member this.Invoke i = i + capturedValue
[<Config(typeof<DiagnosedConfig>)>]
type FunctionBenchmarks() =
[<Literal>]
let Operations = 1000000
let captured = 10
member __.RunFunc fn = fn captured
member __.RunStructFunc<'t when 't : struct and 't :> IFSharpFunc<int, int>>(fn: 't byref) =
fn.Invoke(captured)
[<Benchmark(Baseline = true)>]
member __.Direct_call() =
let mutable sum = 0
let mutable i = 0
while i < Operations do
sum <- sum + (captured + i)
i <- i + 1
sum
[<Benchmark>]
member this.Call_lambda() =
let mutable sum = 0
let mutable i = 0
while i < Operations do
sum <- sum + this.RunFunc ((+) i)
i <- i + 1
sum
[<Benchmark>]
member this.Call_struct_lambda() =
let mutable sum = 0
let mutable i = 0
while i < Operations do
let mutable func = CapturedFunc(i)
sum <- sum + this.RunStructFunc(&func)
i <- i + 1
sum
open BenchmarkDotNet.Running
open System.Reflection
BenchmarkSwitcher.FromAssembly(Assembly.GetExecutingAssembly()).Run() |> ignore
System.Console.ReadLine() |> ignore
BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i5-4430 CPU 3.00GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
Frequency=2929696 Hz, Resolution=341.3323 ns, Timer=TSC
.NET Core SDK=2.1.300
  [Host]     : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT DEBUG
  DefaultJob : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT

Method Mean Error StdDev Scaled Allocated
Direct_call 661.7 us 6.213 us 5.812 us 1.00 0 B
Call_lambda 665.5 us 7.528 us 7.042 us 1.01 0 B
Call_struct_lambda 660.0 us 2.248 us 2.103 us 1.00 0 B
@zpodlovics
Copy link

Take a look at the generated IL for each test case, and also the generated JIT code (benchmarkdotnet now has asm diagnostics now). The F# compiler automatically inline small functions and methods (related issue: dotnet/fsharp#5178). The C# (afaik) will not inline automatically, but you can hint the JIT to inline it in generated code with AgressiveInlining flags.

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