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

C#
このエントリーをはてなブックマークに追加

仕事中に無意識に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化こそが遅いのです。

ベンチマーク

詳細なコードなどは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  

MethodxMeanErrorStdDevMinMaxGen 0Allocated
HasValue11.065 ns0.0716 ns0.0670 ns0.9574 ns1.150 ns-0 B
IsInt152.917 ns0.3573 ns0.2984 ns52.5679 ns53.499 ns0.005724 B
IsIntY11.184 ns0.0492 ns0.0460 ns1.1125 ns1.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に対しても最適化がかかって速度差がなくなると思うので書き方はどれでもいいかもしれませんね