「as」とnull許容型のパフォーマンスの驚き 質問する

「as」とnull許容型のパフォーマンスの驚き 質問する

私は、null 許容型を扱っている C# in Depth の第 4 章を改訂しており、次のように記述できる "as" 演算子の使用に関するセクションを追加しています。

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

これは本当にすばらしいと思いました。また、「is」の後にキャストを使用することで、C# 1 の同等のものよりもパフォーマンスを向上できると思いました。結局のところ、この方法では、動的な型チェックを 1 回だけ要求し、その後は単純な値チェックを行うだけで済みます。

しかし、そうではないようです。以下にサンプル テスト アプリを掲載しましたが、これは基本的にオブジェクト配列内のすべての整数を合計しますが、配列にはボックス化された整数だけでなく、多数の null 参照と文字列参照が含まれています。ベンチマークでは、C# 1 で使用する必要のあるコード、"as" 演算子を使用するコード、そして興味本位で LINQ ソリューションを測定します。驚いたことに、この場合、C# 1 コードは 20 倍高速です。また、LINQ コード (反復子が関係するため、より低速になると思っていました) でさえ、"as" コードより高速です。

.NETisinstの null 許容型の実装は、単に非常に遅いのでしょうか? 問題の原因は追加機能なのでしょうかunbox.any? これについて別の説明はありますか? 現時点では、パフォーマンスが重要な状況ではこれを使用しないように警告する必要があるように感じます...

結果:

キャスト: 10000000 : 121
アサーション: 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);
    }
}

ベストアンサー1

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

is演算子テストは簡単で、オブジェクトが null でなく、期待される型であるかどうかを確認するだけで、いくつかのマシン コード命令しかかかりません。キャストも簡単で、JIT コンパイラはオブジェクト内の値ビットの位置を認識し、それを直接使用します。コピーや変換は行われず、すべてのマシン コードはインラインで、約 12 個の命令しかかかりません。ボックス化が一般的だった .NET 1.0 では、これは非常に効率的である必要がありました。

int? へのキャストには、さらに多くの作業が必要です。ボックス化された整数の値表現は、のメモリ レイアウトと互換性がありませんNullable<int>。変換が必要であり、ボックス化された列挙型の可能性によりコードは複雑になります。JIT コンパイラは、ジョブを完了するために、JIT_Unbox_Nullable という CLR ヘルパー関数への呼び出しを生成します。これは、任意の値型に対する汎用関数であり、型をチェックするためのコードが多数あります。そして、値がコピーされます。このコードは mscorwks.dll 内にロックされているため、コストを見積もることは困難ですが、数百のマシン コード命令が必要になる可能性があります。

Linq OfType() 拡張メソッドもis演算子とキャストを使用します。ただし、これはジェネリック型へのキャストです。JIT コンパイラは、任意の値型へのキャストを実行できるヘルパー関数 JIT_Unbox() への呼び出しを生成します。必要なNullable<int>作業が少ないはずなのに、なぜ へのキャストと同じくらい遅いのか、よくわかりません。ngen.exe がここで問題を引き起こすのではないかと疑っています。

おすすめ記事