Skip to content

Instantly share code, notes, and snippets.

@ufcpp
Created March 17, 2018 05:17
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ufcpp/fef52f31e11e0bb11ccbd22638e2dfc0 to your computer and use it in GitHub Desktop.
Save ufcpp/fef52f31e11e0bb11ccbd22638e2dfc0 to your computer and use it in GitHub Desktop.
構造体のインターフェイス明示的実装の devirtualize 問題
using System;
using System.Runtime.CompilerServices;
struct X : IDisposable
{
public bool IsDisposed;
/// <summary>
/// 明示的実装にしているので、呼ぶときには ((IDispose)x).Dispose() みたいに呼ばないといけない。
/// この型は構造体なので、(IDispose)x としたときのボックス化が気になる。
///
/// これまで、この型のままで ((IDispose)x).Dispose() と呼ぶよりも、ジェネリックメソッドを介した方がパフォーマンスが良かった。
/// それが、 .NET Core 2.1 でちゃんと最適化掛かるようになったみたい。
/// </summary>
void IDisposable.Dispose() => IsDisposed = true;
}
static class BenchmarkTarget
{
/// <summary>
/// このメソッドに構造体を渡すとボックス化を起こす。
/// 要するに遅い。
///
/// 中身がシンプルなのでボックス化のオーバーヘッドが相対的に大きくて、結構なペナルティ度合。
/// 例えば、<see cref="BenchmarkRunning.StructVirtualMethodBenchmark"/> で、手元のPCでベンチマークを取ったら、
/// <see cref="Generic{T}(T)"/> が 1ns 程度なのに対して、このメソッドの呼び出しは 6.5ns 程度だった。
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Interface(IDisposable x) => x.Dispose();
/// <summary>
/// 具象型を渡しているのでこれが一番速そうに見えるものの…
/// ((IDispose)x).Dispose() って呼び方がボックス化を起こす(起こしてた)。
///
/// 以下のような IL が生成される。
/// ldarg.0
/// box X
/// callvirt instance void [mscorlib]System.IDisposable::Dispose()
///
/// なんか、 .NET Core 2.1 でこいつを devirtualize する(box を消す) JIT 最適化が入ったらしく、
/// - .NET Core 2.0 以前で実行すると <see cref="Interface(IDisposable)"/> とほぼ同じパフォーマンス
/// - .NET Core 2.1 以降で実行すると <see cref="Generic{T}(T)"/> とほぼ同じパフォーマンス
/// になってる。
///
/// 参考:
/// https://github.com/dotnet/coreclr/pull/14698
/// https://github.com/dotnet/coreclr/pull/17006
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
public static void NonGeneric(X x) => ((IDisposable)x).Dispose();
/// <summary>
/// 実は、構造体のインターフェイス明示的実装を呼び出すためには、ジェネリックなメソッドを介した方が速かった。
/// こんな感じでジェネリックにすると、((IDispose)x).Dispose() がボックス化なしで呼ばれるようになる。
/// (なのでわざわざ一段階ジェネリックメソッドで囲う最適化手法が使えた。)
///
/// 以下のような IL が生成される。
/// ldarg.0
/// constrained. !!T
/// callvirt instance void [mscorlib]System.IDisposable::Dispose()
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Generic<T>(T x) where T : IDisposable => x.Dispose();
}
namespace BenchmarkRunning
{
// ここから、説明には関係ないんで namespace 区切って下に追いやってるだけ
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Toolchains.CsProj;
using BenchmarkDotNet.Toolchains.DotNetCli;
public class StructVirtualMethodBenchmark
{
[Benchmark]
public bool Interface()
{
var x = new X();
BenchmarkTarget.Interface(x);
return x.IsDisposed;
}
[Benchmark]
public bool NonGeneric()
{
var x = new X();
BenchmarkTarget.NonGeneric(x);
return x.IsDisposed;
}
[Benchmark]
public bool Generic()
{
var x = new X();
BenchmarkTarget.Generic(x);
return x.IsDisposed;
}
}
class Program
{
static void Main()
{
BenchmarkRunner.Run<StructVirtualMethodBenchmark>(new MultipleRuntimesConfig());
}
}
public class MultipleRuntimesConfig : ManualConfig
{
public MultipleRuntimesConfig()
{
Add(Job.Default
.With(CsProjCoreToolchain.NetCoreApp20)
);
Add(Job.Default
.With(Runtime.Core)
.With(CsProjCoreToolchain.From(new NetCoreAppSettings(
targetFrameworkMoniker: "netcoreapp2.1",
runtimeFrameworkVersion: "2.1.0-preview1-26112-04", // <-- Adjust version here
name: "Core 2.1.0-preview")))
);
Add(DefaultColumnProviders.Instance);
Add(MarkdownExporter.GitHub);
Add(new ConsoleLogger());
Add(new HtmlExporter());
Add(MemoryDiagnoser.Default);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment