web-dev-qa-db-ja.com

「as」およびNULL入力可能型によるパフォーマンスの驚き

Null許容型を扱うC#の詳細な第4章を改訂し、「as」演算子の使用に関するセクションを追加します。

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

これは本当にすてきで、C#1と同等のパフォーマンスを向上させることができると思いました。 "is"に続いてキャストを使用します-結局、このように、動的な型チェックを1回だけ要求して。

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

Null許容型のisinstの.NET実装は本当に遅いのですか?追加の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);
    }
}
321
Jon Skeet

明らかに、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が問題を引き起こす可能性があると思います。

204
Hans Passant

isinstは、null許容型では本当に遅いように思えます。メソッドFindSumWithCastで変更しました

if (o is int)

if (o is int?)

また、実行速度が大幅に低下します。私が見ることができるILの唯一の違いは

isinst     [mscorlib]System.Int32

に変更されます

isinst     valuetype [mscorlib]System.Nullable`1<int32>
26
Dirk Vollmar

これはもともとHans Passantの優れた答えに対するコメントとして始まりましたが、長すぎたので、ここにいくつかのビットを追加します。

まず、C#as演算子はisinst IL命令を生成します(is演算子も生成します)。 (別の興味深い命令はcastclassです。これは、直接キャストを行うときに発行され、コンパイラはランタイムチェックを省略できないことを認識しています。)

isinstの機能は次のとおりです( ECMA 335 Partition III、4.6 ):

形式:isinsttypeTok

typeTokはメタデータトークン(typereftypedefまたはtypespec)であり、目的のクラスを示します。

typeTokがnull入力不可の値型またはジェネリックパラメーター型である場合、「ボックス化された」typeTokと解釈されます。

typeTokがnull許容型Nullable<T>の場合、「ボックス化」Tとして解釈されます

最も重要なこと:

objの実際のタイプ(検証者が追跡するタイプではない)がverifier-assignable-toタイプtypeTokの場合、isinstは成功し、obj(asresult)は変更されずに返されますが、検証ではそのタイプをtypeTokとして追跡します強制(§1.6)および変換(§3.27)とは異なり、isinstはオブジェクトの実際のタイプを変更せず、オブジェクトIDを保持します(パーティションIを参照)。

したがって、この場合のパフォーマンスキラーはisinstではなく、追加のunbox.anyです。これはハンスの答えからは明らかではありませんでした。彼はJITされたコードだけを見ていたからです。一般に、C#コンパイラはunbox.anyの後にisinst T?を出力します(ただし、Tが参照型の場合、isinst Tを実行する場合は省略します)。

なぜそうするのですか? isinst T?には明らかな効果はありません。つまり、T?が返されます。代わりに、これらのすべての指示は、"boxed T"に展開できるT?があることを保証します。実際のT?を取得するには、"boxed T"T?にアンボックスする必要があります。これが、コンパイラーがisinstの後にunbox.anyを出力する理由です。考えてみると、T?の「ボックス形式」は"boxed T"にすぎず、castclassisinstを実行してunboxを実行することは一貫性がないため、これは理にかなっています。

標準 からの情報を使用してHansの発見をバックアップします。

(ECMA 335パーティションIII、4.33):unbox.any

値型のボックス形式に適用されると、unbox.any命令はobj(O型)に含まれる値を抽出します。 (unboxに続いてldobjと同等です。)参照型に適用される場合、unbox.any命令はcastclass typeTokと同じ効果があります。

(ECMA 335パーティションIII、4.32):unbox

通常、unboxは、ボックス化されたオブジェクト内に既に存在する値型のアドレスを単に計算します。 null可能な値型をボックス化解除する場合、このアプローチは不可能です。 Nullable<T>値はボックス操作中にボックス化Tsに変換されるため、実装は多くの場合、ヒープ上で新しいNullable<T>を製造し、新しく割り当てられたオブジェクトのアドレスを計算する必要があります。

22

興味深いことに、Nullable<T>の桁違いに遅いdynamicを介してオペレーターのサポートに関するフィードバックを渡しました( この初期テスト と同様)-非常に似ていると思われます理由。

Nullable<T>が大好きです。もう1つの楽しい方法は、JITがnullを許可しない構造体のnullを検出(および削除)しても、Nullable<T>の場合はそれをブロックすることです。

using System;
using System.Diagnostics;
static class Program {
    static void Main() { 
        // JIT
        TestUnrestricted<int>(1,5);
        TestUnrestricted<string>("abc",5);
        TestUnrestricted<int?>(1,5);
        TestNullable<int>(1, 5);

        const int LOOP = 100000000;
        Console.WriteLine(TestUnrestricted<int>(1, LOOP));
        Console.WriteLine(TestUnrestricted<string>("abc", LOOP));
        Console.WriteLine(TestUnrestricted<int?>(1, LOOP));
        Console.WriteLine(TestNullable<int>(1, LOOP));

    }
    static long TestUnrestricted<T>(T x, int loop) {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
    static long TestNullable<T>(T? x, int loop) where T : struct {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
}
19
Marc Gravell

これは、上記のFindSumWithAsAndHasの結果です。 alt text

これはFindSumWithCastの結果です: alt text

調査結果:

  • asを使用して、オブジェクトがInt32のインスタンスかどうかを最初にテストします。内部では_isinst Int32_を使用しています(手書きコードに似ています:if(o is int))。また、asを使用して、オブジェクトを無条件でボックス化解除します。そして、プロパティを呼び出すのは本当のパフォーマンスキラーです(それはまだ内部の機能です)、IL_0027

  • キャストを使用して、オブジェクトがintif (o is int);かどうかを最初にテストします内部では、これは_isinst Int32_を使用しています。 intのインスタンスである場合、値IL_002Dを安全にアンボックスできます。

簡単に言えば、これはasアプローチを使用した擬似コードです。

_int? x;

(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)

if (x.HasValue)
    sum += x.Value;    
_

そして、これはキャストアプローチを使用した擬似コードです:

_if (o isinst Int32)
    sum += (o unbox Int32)
_

したがって、キャスト(_(int)a[i]_、まあ構文はキャストのように見えますが、実際にはボックス化解除、キャスト、およびボックス化解除は同じ構文を共有します。オブジェクトが明らかにintである場合に値をボックス化解除する必要がありました。 asアプローチを使用することと同じことは言えません。

12
Michael Buen

さらにプロファイリング:

using System;
using System.Diagnostics;

class Program
{
    const int Size = 30000000;

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

        FindSumWithIsThenCast(values);

        FindSumWithAsThenHasThenValue(values);
        FindSumWithAsThenHasThenCast(values);

        FindSumWithManualAs(values);
        FindSumWithAsThenManualHasThenValue(values);



        Console.ReadLine();
    }

    static void FindSumWithIsThenCast(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("Is then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenValue(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 then Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (x.HasValue)
            {
                sum += (int)o;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Has then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithManualAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            bool hasValue = o is int;
            int x = hasValue ? (int)o : 0;

            if (hasValue)
            {
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Manual As: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenManualHasThenValue(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

}

出力:

Is then Cast: 10000000 : 303
As then Has then Value: 10000000 : 3524
As then Has then Cast: 10000000 : 3272
Manual As: 10000000 : 395
As then Manual Has then Value: 10000000 : 3282

これらの数字から何を推測できますか?

  • まず、is-then-castアプローチはasアプローチよりも大幅に高速です。 303対3524
  • 第二に、.Valueはキャストよりもわずかに遅いです。 3524対3272
  • 第三に、.HasValueは、手動のhas(isを使用)を使用するよりもわずかに遅くなります。 3524対3282
  • 4番目に、simulated asreal asアプローチの間でApple対Appleの比較(つまり、シミュレートされたHasValueの割り当てとシミュレートされた値の変換の両方が行われます)を行うと、 simulated asは、real asよりも大幅に高速です。 395対3524
  • 最後に、最初と4番目の結論に基づいて、as implementation ^ _ ^に何か問題があります
9
Michael Buen

この回答を最新の状態に保つために、このページでの議論のほとんどがC#7.1および。NET 4.7は、最適なILコードも生成するスリムな構文をサポートします。

OPの元の例...

_object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    // ...use x.Value in here
}
_

単純になります...

_if (o is int x)
{
    // ...use x in here
}
_

新しい構文の一般的な使用法の1つは、.NETvalue type(つまり、struct inC#)_IEquatable<MyStruct>_を実装します(ほとんどの場合)。強く型付けされたEquals(MyStruct other)メソッドを実装した後、型なしのEquals(Object obj)オーバーライド(Objectから継承)を次のように優雅にリダイレクトできるようになりました。

_public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);
_


付録:Release build[〜#〜] il [〜#〜]この回答の上記の最初の2つのサンプル関数のコード(それぞれ)をここに示します。新しい構文のILコードは実際には1バイト小さくなりますが、ほとんど呼び出しを行わず(2対)、unbox操作を可能な限り完全に回避することで大きく勝ちます。

_// static void test1(Object o, ref int y)
// {
//     int? x = o as int?;
//     if (x.HasValue)
//         y = x.Value;
// }

[0] valuetype [mscorlib]Nullable`1<int32> x
        ldarg.0
        isinst [mscorlib]Nullable`1<int32>
        unbox.any [mscorlib]Nullable`1<int32>
        stloc.0
        ldloca.s x
        call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()
        brfalse.s L_001e
        ldarg.1
        ldloca.s x
        call instance !0 [mscorlib]Nullable`1<int32>::get_Value()
        stind.i4
L_001e: ret
_
_// static void test2(Object o, ref int y)
// {
//     if (o is int x)
//         y = x;
// }

[0] int32 x,
[1] object obj2
        ldarg.0
        stloc.1
        ldloc.1
        isinst int32
        ldnull
        cgt.un
        dup
        brtrue.s L_0011
        ldc.i4.0
        br.s L_0017
L_0011: ldloc.1
        unbox.any int32
L_0017: stloc.0
        brfalse.s L_001d
        ldarg.1
        ldloc.0
        stind.i4
L_001d: ret
_

以前の利用可能なオプションを超える新しいC#7構文のパフォーマンスに関する発言を実証するさらなるテストについては、 here を参照してください(特に、例「D」)。

9
Glenn Slayden

正確な型チェック構造を試しました

typeof(int) == item.GetType()は、_item is int_バージョンと同じ速度で実行され、常に数値を返します(強調:_Nullable<int>_を配列に書き込んでも、typeof(int))。また、追加の_null != item_チェックも必要です。

しかしながら

typeof(int?) == item.GetType()は(_item is int?_とは対照的に)高速ですが、常にfalseを返します。

Typeof-constructは、RuntimeTypeHandleを使用するため、exactタイプチェックの最速の方法です。この場合の正確な型はnullableと一致しないため、_is/as_は実際にNullable型のインスタンスであることを確認するために、ここでさらに重量挙げを行う必要があります。

そして正直に言うと、あなたの_is Nullable<xxx> plus HasValue_は何を買うのでしょうか?なし。基礎となる(値)タイプにいつでも直接アクセスできます(この場合)。値を取得するか、「いいえ、要求したタイプのインスタンスではありません」のいずれかです。 _(int?)null_を配列に書き込んだ場合でも、型チェックはfalseを返します。

8
dalo

私はそれを試してみる時間はありませんが、あなたがしたいかもしれません:

foreach (object o in values)
        {
            int? x = o as int?;

なので

int? x;
foreach (object o in values)
        {
            x = o as int?;

毎回新しいオブジェクトを作成しているため、問題を完全に説明することはできませんが、貢献する可能性があります。

8
James Black
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);
        FindSumWithAsAndHas(values);
        FindSumWithAsAndIs(values);


        FindSumWithIsThenAs(values);
        FindSumWithIsThenConvert(values);

        FindSumWithLinq(values);



        Console.ReadLine();
    }

    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 FindSumWithAsAndHas(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 and Has: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }


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







    static void FindSumWithIsThenAs(object[] values)
    {
        // Apple-to-Apple comparison with Cast routine above.
        // Using the similar steps in Cast routine above,
        // the AS here cannot be slower than Linq.



        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {

            if (o is int)
            {
                int? x = o as int?;
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then As: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithIsThenConvert(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {            
            if (o is int)
            {
                int x = Convert.ToInt32(o);
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then Convert: {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);
    }
}

出力:

Cast: 10000000 : 456
As and Has: 10000000 : 2103
As and Is: 10000000 : 2029
Is then As: 10000000 : 1376
Is then Convert: 10000000 : 566
LINQ: 10000000 : 1811

[編集:2010-06-19]

注:前のテストは、VS 2009を使用して、Core i7(会社の開発マシン)を使用して、VS内で構成デバッグを行いました。

VS2010を使用してCore 2 Duoを使用しているマシンで次のことが行われました

Inside VS, Configuration: Debug

Cast: 10000000 : 309
As and Has: 10000000 : 3322
As and Is: 10000000 : 3249
Is then As: 10000000 : 1926
Is then Convert: 10000000 : 410
LINQ: 10000000 : 2018




Outside VS, Configuration: Debug

Cast: 10000000 : 303
As and Has: 10000000 : 3314
As and Is: 10000000 : 3230
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 418
LINQ: 10000000 : 1944




Inside VS, Configuration: Release

Cast: 10000000 : 305
As and Has: 10000000 : 3327
As and Is: 10000000 : 3265
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1932




Outside VS, Configuration: Release

Cast: 10000000 : 301
As and Has: 10000000 : 3274
As and Is: 10000000 : 3240
Is then As: 10000000 : 1904
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1936
7
Michael Buen