1. ホーム
  2. c#

[解決済み】"as "とnullable型によるパフォーマンスの驚き

2022-03-25 20:25:18

質問

C# in Depthの第4章を改訂しているところなのですが、nullable型を扱っていて、"as"演算子の使い方についてのセクションを追加していて、これで書けるようになりました。

object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    ... // Use x.Value in here
}

この方法は、C# 1と同じように"is"の後にキャストを使用するよりもパフォーマンスを向上させることができると思いました。

しかし、そうではないようです。このアプリは基本的にオブジェクト配列内のすべての整数を合計するものですが、配列には箱型整数のほか、多くのNULL参照や文字列参照が含まれています。ベンチマークでは、C# 1 で使用するコード、"as" 演算子を使用するコード、および LINQ ソリューションを測定しています。驚いたことに、C# 1のコードはこの場合20倍速く、LINQのコード(イテレータを含むのでもっと遅いと思っていた)でも"as"のコードに勝っています。

の.NET実装は isinst null可能な型に対して、本当に遅いのか?それは追加の unbox.any という問題が発生するのでしょうか?また、他に説明があるのでしょうか?今のところ、パフォーマンスに敏感な状況でこれを使わないようにという警告を含めなければならないような気がします...。

結果は?

キャスト 10000000 : 121

As: 10000000 : 2211

Linq: 10000000 : 2143

コード

using System;
using System.Diagnostics;
using System.Linq;

class Test
{
    const int Size = 30000000;

    static void Main()
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i+1] = "";
            values[i+2] = 1;
        }

        FindSumWithCast(values);
        FindSumWithAs(values);
        FindSumWithLinq(values);
    }

    static void FindSumWithCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            if (o is int)
            {
                int x = (int) o;
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Cast: {0} : {1}", sum, 
                          (long) sw.ElapsedMilliseconds);
    }

    static void FindSumWithAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;
            if (x.HasValue)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As: {0} : {1}", sum, 
                          (long) sw.ElapsedMilliseconds);
    }

    static void FindSumWithLinq(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = values.OfType<int>().Sum();
        sw.Stop();
        Console.WriteLine("LINQ: {0} : {1}", sum, 
                          (long) sw.ElapsedMilliseconds);
    }
}

解決方法は?

JITコンパイラが生成できる機械語コードは、明らかに最初のケースの方がはるかに効率的です。 この場合、オブジェクトはボックス化された値と同じ型の変数にのみアンボックスできるというルールが非常に役に立ちます。 これにより、JITコンパイラは、値の変換を考慮する必要がなく、非常に効率的なコードを生成することができます。

その 演算子のテストは簡単で、オブジェクトがヌルでなく、期待される型であるかどうかをチェックするだけで、わずかな機械語命令で済みます。 キャストも簡単で、JITコンパイラはオブジェクトの値ビットの位置を知っているので、それを直接使用します。 コピーや変換は行わず、すべての機械語はインラインで、数十の命令で済みます。 これは、ボクシングが一般的だった.NET 1.0では、本当に効率的である必要がありました。

int?にキャストするのはもっと手間がかかります。 箱型整数の値表現が Nullable<int> . 変換が必要で、ボックス型の列挙型の可能性があるため、コードが厄介です。 JITコンパイラは、JIT_Unbox_NullableというCLRヘルパー関数の呼び出しを生成して、この作業を実行します。 これはあらゆる値型に対応する汎用関数で、型チェックのためのコードがたくさんあります。 そして、値がコピーされます。 このコードはmscorwks.dllの中に閉じこめられているので、コストを見積もるのは難しいですが、何百ものマシンコード命令があると思われます。

Linq OfType() 拡張メソッドでは、さらに 演算子とキャストがあります。 しかし、これは一般的な型へのキャストです。 JIT コンパイラは、任意の値型へのキャストを実行できるヘルパー関数 JIT_Unbox() の呼び出しを生成します。 へのキャストと同じぐらい遅い理由はうまく説明できません。 Nullable<int> というのも、本来はもっと少ない労力で済むはずだからです。 ngen.exeが原因ではないでしょうか。