C#+WPFチューニング戦記

C#とWPFで高速なコードと最適なシステムを書くためにやってきたいろいろな事を書いてみます。.NET Frameworkのソースコードを読み解きましょう。なお、ここに書かれているのは個人の見解であって何らかの団体や企業の見解を代表するものではありません。

共用体でGuidすら速くできる

CEDEC2018の傍ら、こんなベンチマークを思いつきで書いていました。 まずは結果からご覧ください。Meanの値から、だいたいSystem.Guidよりも2倍は高速です。

Method Mean Error StdDev
ImmutableGuid_In 20.83 us 0.1102 us 0.0977 us
ImmutableGuid 22.46 us 0.1960 us 0.1530 us
SystemGuid 48.51 us 0.5408 us 0.5058 us

StructLayoutやFieldOffsetって、Win32コールやバイナリシリアライズ以外の使いみちをあまり考えたことがなかったのですが、C#7.3になって共用体の意味をちょっと見直してみた。というのが今回のベンチマークです。

Guid.GetHashCodeとGuid.Equalsのベンチマークですが、Guidの値生成を再実装するというのはいかがなものかと思ったのと、Guidは単に比較して等しいかどうかが重要という原点に立ち返りました。

そして共用体と readonly struct の合わせ技で、高速に辞書のキーにすることができるGuidというのを作ってみました。

以下、ベンチマークと実装です。簡単ですが威力絶大です。

private int[] hashSets = new int[10000];
private bool[] equals = new bool[10000];

[Benchmark]
public void ImmutableGuid_In()
{
    var guid1 = new Immutable.Guid(Guid.NewGuid());
    var guid2 = new Immutable.Guid(Guid.NewGuid());

    for (int idx = 0; idx < 10000; idx++)
    {
        hashSets[idx] = guid2.GetHashCode();
        // 意識的に in とすることで同名の in引数のメソッドを選択できます。
        equals[idx] = guid1.Equals(in guid2);
    }
}

[Benchmark]
public void ImmutableGuid()
{
    var guid1 = new Immutable.Guid(Guid.NewGuid());
    var guid2 = new Immutable.Guid(Guid.NewGuid());

    for (int idx = 0; idx < 10000; idx++)
    {
        hashSets[idx] = guid2.GetHashCode();
        equals[idx] = guid1.Equals(guid2);
    }
}

[Benchmark]
public void SystemGuid()
{
    var guid1 = Guid.NewGuid();
    var guid2 = Guid.NewGuid();

    for (int idx = 0; idx < 10000; idx++)
    {
        hashSets[idx] = guid2.GetHashCode();
        equals[idx] = guid1.Equals(guid2);
    }
}

namespace Immutable
{
    [StructLayout(LayoutKind.Explicit)]
    public readonly struct Guid : IEquatable<Guid>
    {
        public static readonly Guid Empty = new Guid();

        [FieldOffset(0)]
        private readonly System.Guid _guid;

        [FieldOffset(0)]
        private readonly int _a;

        [FieldOffset(0)]
        private readonly ulong _low;

        [FieldOffset(8)]
        private readonly ulong _high;

        [FieldOffset(4)]
        private readonly int _bc;

        [FieldOffset(8)]
        private readonly int _defg;

        [FieldOffset(12)]
        private readonly int _hijk;

        public Guid(in System.Guid guid) : this() => _guid = guid;

        public static implicit operator System.Guid(in Guid source) => source._guid;

        public static implicit operator Guid(in System.Guid source) => new Guid(source);

        public static Guid AsRef(in System.Guid guid) => new Guid(guid);

        public override int GetHashCode() => _a ^ _bc ^ _defg ^ _hijk;

        public bool Equals(Guid other) => _low == other._low && _high == other._high;

        // in も用意しておくと辞書の独自実装時により高速にできます。IDictionary<T> を拡張してみようかと思っています。
        public bool Equals(in Guid other) => _low == other._low && _high == other._high;

        public override bool Equals(object obj)
        {
            if (obj is Guid guid)
                return Equals(guid);

            return false;
        }
    }
}

なお、Guidからのキャスト、Guidへのキャストは若干コストがあります。

「Guidを得たらずっと Immutable.Guid の形で in引数でひたすら渡し続け、APIなどに戻すときにGuidにする。」というような使い方を想定しています。 Guidの辞書を1億回引くようなプロジェクトにはうってつけの拡張だと思いませんか?