滅入るんるん

何か書きます

【C#】nullable intのnonnull判定にはisで変数宣言したほうが早い話

仕事中に無意識にif (x?.y?.z is int value)(zはint?)みたいなコードを書いてしまい、ちょっと怪しかったのでいつもお世話になっているSharplabで確認してみたらなんかすごい最適化がされてるということを発見したのでその話

前置き

この話はC# 7.3 コンパイラーでの話であってC# 8.0とかになると違う結果になるかもしれません。
コンパイラーの最適化によっていろいろと変わりうるので小ネタ程度に思っといてください。

どれが早そうか

int? x = 0;

if (x.HasValue)  // ①
if (x is int) //  ②
if (x is int y) // ③

これらのうちどれが早いでしょうか?

正解は①と③です。

なぜ②だけが遅いのか

遅い理由はコンパイラーがコンパイルした結果のILを見れば一目瞭然ですが、それはめんどくさいのでC#にデコンパイルしたのを見てもらいましょう。

コンパイルしたコード:

        public bool HasValue(int? x)
        {
            if (x.HasValue)
            {
                return true;
            }
            return false;
        }

        public bool IsInt(int? x)
        {
            if (x is int)
            {
                return true;
            }
            return false;
        }

        public bool IsIntY(int? x)
        {
            if (x is int y)
            {
                return true;
            }
            return false;
        }

ILSpyでデコンパイルしたコード:

   public bool HasValue(int? x)
    {
        if (x.HasValue)
        {
            return true;
        }
        return false;
    }

    public bool IsInt(int? x)
    {
        if (((object)x) is int)
        {
            return true;
        }
        return false;
    }

    public bool IsIntY(int? x)
    {
        int? nullable = x;
        nullable.GetValueOrDefault();
        if (nullable.HasValue)
        {
            return true;
        }
        return false;
    }

①と③はint?.HasValueを利用して条件分岐していますが、②はそんなことをしないでbox化させています。 そのbox化こそが遅いのです。

ベンチマーク

int?のベンチマーク · GitHub

詳細なコードなどはgistに貼っています。(ベンチマークしたコードは上述のコードです)

そして結果:

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
Frequency=3328124 Hz, Resolution=300.4696 ns, Timer=TSC
.NET Core SDK=2.1.403
  [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
  Core   : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT

Job=Core  Runtime=Core  
Method x Mean Error StdDev Min Max Gen 0 Allocated
HasValue 1 1.065 ns 0.0716 ns 0.0670 ns 0.9574 ns 1.150 ns - 0 B
IsInt 1 52.917 ns 0.3573 ns 0.2984 ns 52.5679 ns 53.499 ns 0.0057 24 B
IsIntY 1 1.184 ns 0.0492 ns 0.0460 ns 1.1125 ns 1.273 ns - 0 B

デコンパイル結果通り②の場合はbox化していることで遅くなっていますしallocateされています。

つまりこういう風に書けばいいんでしょ?

以上の結果を踏まえると、int?などのNullable<T>な型の条件分岐はこんな感じに書けばいいということになるでしょう:

int? x = 0;

if (x is int _)
{
    // HasValueだけを見るなら_で値を捨ててもいい
    // もちろんHasValueでもいい
}

if (x is int y)
{
    // 値を使うならHasValueとValueの組み合わせよりこっちのほうが短くていいかも
}

まぁいずれis intに対しても最適化がかかって速度差がなくなると思うので書き方はどれでもいいかもしれませんね