質問

現在、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」コードよりも高速です。

の .NET 実装です isinst null 許容型の場合は本当に遅いのでしょうか?追加ですかね unbox.any それが問題の原因ですか?これについて別の説明はありますか?現時点では、パフォーマンスが重視される状況でこれを使用することに対する警告を含める必要があるように感じます...

結果:

キャスト:10000000 :121
として:10000000 :2211
リンク: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コンパイラは非常に効率的なコードを生成することができ、値の変換を考慮しなければならない。

のあるオペレータのテストでは、オブジェクトがnullでなく、予想されるタイプのものであり、かかるが、いくつかのマシンコード命令場合だけチェックし、簡単です。キャストが容易でもある、JITコンパイラは、オブジェクトの値のビットの位置を知っており、それらを直接使用しています。いかなるコピーや変換発生し、すべてのマシンコードは、インラインではありませんとなりますが、ダース命令について。これはボクシングが一般的だった.NET 1.0で、本当に効率的なバックする必要がありました。

intにキャスト?多くの手間がかかります。ボックス化整数値の表現はNullable<int>のメモリレイアウトと互換性がありません。変換が必要であり、コードが可能箱入りenum型によるトリッキーです。 JITコンパイラは、仕事を得るためにJIT_Unbox_Nullableという名前のCLRヘルパー関数の呼び出しを生成します。これは、任意の値タイプのための汎用的な機能が、そこのコードの多くは、種類を確認することです。そして、値がコピーされます。このコードはのMscorwks.dllの内側にロックアップしますが、マシンコード命令の数百人がそうであるからである。

コストを推定するのは難しいです

はLINQのOfType()拡張メソッドはまた、のオペレータとキャストを使用しています。しかし、これはジェネリック型へのキャストです。 JITコンパイラは、任意の値型にキャストを行うことができるヘルパー関数、JIT_Unbox()への呼び出しを生成します。それはあまり仕事が必要であるべきことを考えれば、Nullable<int>へのキャストとして遅いようですなぜ私は偉大な説明はありません。私はここでそのngen.exeかもしれない原因のトラブルを疑っています。

他のヒント

isinstがNULL可能なタイプで本当に遅いと私には思えます。メソッドFindSumWithCastで、私は変更

if (o is int)

if (o is int?)

これも大幅に実行が遅くなります。私が見ることができるILで唯一differencは

ということです
isinst     [mscorlib]System.Int32
に変更されます。

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

このためコメントをハンス-Passantの優れた回答だった長していきたいなと思っていますのも、ほかのビットはこちら

第一に、C# as オペレーターを発する isinst IL説明書(でも is オペレーター).(もう一つの興味深い指導を castclass,emitedだけど、直接キャストのコンパイラを知ってるランタイムで確認できないommited.)

これがその isinst は(ECMA335パIII4.6):

フォーマット: isinst typeTok

typeTok はメタデータのトークン( typeref, typedef または typespecが示唆されていることから、希望のクラスです。

の場合 typeTok は非null値の型は汎用のパラメータの型であるとして解釈される"弁当" typeTok.

の場合 typeTok はnullable型 Nullable<T>, であるとして解釈される"弁当" T

最も重要なのは:

場合に実際の型は、検証者を追跡型) obj検証者-割当可能に タイプtypeTokし isinst に成功し、 obj結果)を変更せずに返し検証トラック型として typeTok. とは異なりcoercions(§1.6)および変換(§3.27), isinst 変わることはありません実際の型のオブジェクト保存するとともにオブジェクトアイデンティティ参照の分配という。)

ることは可能でキラーな isinst この場合、追加 unbox.any.このなかのハンス'の答えは、彼の場合の近似式は以下のようになJITedコードのみです。一般のC#コンパイラを発する unbox.any した後、 isinst T? (ものを省略するとい isinst T, 時 T は参考タイプ)です。

なぜなる。 isinst T? などに効果があるとって自明であったかも知れる返されますが、 T?.代わりに、これらすべての指示を確保している人が、 "boxed T" できるunboxedる T?.得実績 T?, まだunbox社 "boxed T"T?, のコンパイラを放 unbox.anyisinst.考えてみれば、この意味での"ボックスフォーマット"のための T?"boxed T"castclassisinst を行うunboxうに統一性がない。

バックアップハンスの発見と情報の 標準, こちらでは:

(ECMA335パIII、4.33): unbox.any

適用される場合には箱の値型を、 unbox.any 指導を抽出し、値に含まれるobjの型 O).(相当するもので unbox 次いで ldobj.) 適用される場合には、参照型の unbox.any 指導には、以下と同じ効果があります castclass typeTok.

(ECMA335パIII4.32): unbox

通常、 unbox 単なる計算のアドレス値の型が存在してい内食のオブジェクトです。このアプローチできない場合unboxing nullable値です。ので Nullable<T> 値に変換され弁当 Ts の操作ボックス実装では、しばしば必ず新規に製作 Nullable<T> のヒープおよび計算の住所に新たに割り当てオブジェクトです。

興味深いことに、オペレーターのサポートに関するフィードバックを次の方法で送信しました。 dynamic 桁違いに遅い Nullable<T> (に似ている この初期のテスト) - 非常に似たような理由があるのではないかと思います。

お奨めの愛 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;
    }
}

この結果FindSumWithAsAndHas上: alt text

この結果FindSumWithCast: alt text

結果:

  • を使用 as, での試験の場合はオブジェクトのインスタンスであるInt32;の下にフードを使用しておりますので isinst Int32 (とよく似た手書きのコード:if(o int)).を使用 as, でも無条件にunboxのオブジェクトです。でリアルなパフォーマンス-キラーと呼性(機能のフード),IL_0027

  • をキャストは、試験の場合はオブジェクトが int if (o is int);ボンネットの下にはこの使用 isinst Int32.の場合はインスタンスのintきますので、その安全にunboxの価値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], もの書式のように見えだが、これは実際unboxing、キャストとunboxingでは、同じ構文を共有します、おすすめするpedanticの用語のアプローチは本当に速いだけに必要なunbox値があるオブジェクトの評価を得てき int.同じことをできないと利用 as ます。

プロファイリングさ:

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

何ができる推定したさらにこれらの数値は?

  • 最初に、そしてキャストアプローチが有意に高速 として ます。303vs3524
  • 第二に、.値が小幅より遅くなります。3524vs3272
  • 第三に、.HasValueは小幅より遅くな利用マニュアル(を使用 ).3524vs3282
  • 第四に、やっているファミマクレジットとの比較(両方の手配を模擬HasValueに転換する模擬値が起こるとと シミュレーションリアルとして アプローチまで シミュレーション もく リアルとして.395vs3524
  • 最後に、最初と最後にあるように として 実施^_^

この回答を最新の状態に保つために、このページでの議論のほとんどは現在議論の余地がないことに言及する価値があります。 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 つは、.NET を作成するときであることがわかりました。 値の型 (すなわち、 structC#) を実装する IEquatable<MyStruct> (ほとんどの場合そうであるように)。強く型付けされたメソッドを実装した後、 Equals(MyStruct other) メソッドを使用すると、型なしのメソッドを適切にリダイレクトできるようになりました。 Equals(Object obj) オーバーライド (から継承 Object) 次のように変更します。

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

 


付録:Release 建てる イリノイ州 この回答で上に示した最初の 2 つの関数例のコード (それぞれ) をここに示します。新しい構文の IL コードは確かに 1 バイト小さくなっていますが、ほとんどの場合、呼び出しを行わないことで大きな効果が得られます (vs.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 以前に使用可能なオプションを超える構文については、を参照してください。 ここ (特に例「D」)。

私はそれをしようとする時間がありませんが、あなたがしたいことがあります:

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

タグなど
int? x;
foreach (object o in values)
        {
            x = o as int?;

あなたは完全に問題を説明しませんが、貢献するかもしれない、毎回新しいオブジェクトを作成している。

私は正確な型チェック構文を試してみました。

typeof(int) == item.GetType()、常にitem is intバージョンの速さで行い、そして数(重点を:あなたは配列にNullable<int>を書いた場合でも、あなたが使用typeof(int)する必要があります)を返します。また、ここでは、追加のnull != itemチェックが必要になります。

ただし、

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

それはRuntimeTypeHandleを使用すると、 typeofの構築物は、私の目には、の正確なの型チェックのための最速の方法です。この場合、正確な型がNULL可能で一致していないので、私の推測では、is/asは、それが実際にのNullable型のインスタンスであることを確実にすることで、ここで追加のheavyliftingを行う必要がありますされます。

正直に

そして:あなたのis Nullable<xxx> plus HasValueがあなたに何を買うのか?何もありません。あなたはいつも(この場合)基礎となる(値)の型に直接行くことができます。あなたは、どちらかの値か「あなたが求めていたタイプの、いや、ないインスタンス」を取得します。あなたは配列に(int?)nullを書いた場合であっても、型チェックはfalseを返します。

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

[EDIT:2010-06-19]

注:前のテストは、コアi7の(会社の開発マシン)を使用して、VS2009を使用して、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
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top