340

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」コードよりも優れています。

null許容型の.NET実装はisinst本当に遅いですか?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);
    }
}
4

10 に答える 10

212

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

is演算子のテストは簡単です。オブジェクトがヌルではなく、期待されるタイプであるかどうかを確認するだけで、いくつかのマシンコード命令が必要になります。キャストも簡単です。JITコンパイラはオブジェクト内の値ビットの位置を認識しており、それらを直接使用します。コピーや変換は発生しません。すべてのマシンコードはインラインであり、約12の命令が必要です。ボクシングが一般的だったとき、これは.NET1.0に戻って本当に効率的である必要がありました。

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

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

于 2010-06-19T17:28:37.433 に答える
26

isinstnull許容型では本当に遅いように思えます。方法FindSumWithCastを変更しました

if (o is int)

if (o is int?)

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

isinst     [mscorlib]System.Int32

に変更されます

isinst     valuetype [mscorlib]System.Nullable`1<int32>
于 2009-10-17T20:10:02.613 に答える
22

これは元々、Hans Passantの優れた回答へのコメントとして始まりましたが、長すぎたため、ここにいくつか追加したいと思います。

まず、C#asオペレーターはisinstIL命令を発行します(isオペレーターも同様です)。(もう1つの興味深い命令はcastclass、直接キャストを実行し、ランタイムチェックを省略できないことをコンパイラーが認識している場合に発行されます。)

これが何をするかですisinstECMA 335パーティションIII、4.6):

フォーマット:isinst typeTok

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

typeTokがnull許容でない値型またはジェネリックパラメータ型である場合、「ボックス化された」typeTokとして解釈されます。

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

最も重要なこと:

objの実際のタイプ(ベリファイア追跡タイプではない)がベリファイア割り当て可能である場合、タイプtypeTokにisinst成功し、検証がそのタイプをtypeTokとして追跡している間、 obj結果として)は変更されずに返されます。強制(§1.6)や変換(§3.27)とは異なり、オブジェクトの実際のタイプを変更することはなく、オブジェクトのIDを保持します(パーティションIを参照)。isinst

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

なぜそれをするのですか?isinst T?明らかだったであろう効果は決してありません。つまり、あなたは戻ってきますT?。代わりに、これらのすべての手順は、"boxed T"ボックスから外すことができるを持っていることを確認しT?ます。実際のを取得するには、 toT?のボックスを解除する必要があります。これが、コンパイラがafterを発行する理由です。あなたがそれについて考えるならば、これは理にかなっています。なぜなら、の「ボックス形式」は単なるaであり、アンボックスの作成と実行には一貫性がないからです。"boxed T"T?unbox.anyisinstT?"boxed T"castclassisinst

標準からのいくつかの情報でハンスの発見をバックアップします、ここにそれは行きます:

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

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

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

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

于 2011-08-15T09:50:41.873 に答える
19

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

お奨めの愛Nullable<T>。もう1つの楽しい点は、JITがnullnull許容でない構造体を見つけた(そして削除した)としても、それを次のように中断することです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;
    }
}
于 2009-10-17T21:26:50.370 に答える
13

この回答を最新の状態に保つために、このページでの議論のほとんどは、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値型(つまりC#)を作成する場合です。強く型付けされたメソッドを実装した後、次のように、型指定されていないオーバーライド(から継承)を適切にリダイレクトできるようになりました。structIEquatable<MyStruct>Equals(MyStruct other)Equals(Object obj)Object

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

 


付録:この回答で上記に示した最初の2つの関数例のReleaseビルドILコード(それぞれ)をここに示します。新しい構文の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構文のパフォーマンスに関する私の意見を実証するさらなるテストについては、ここを参照してください(特に例「D」)。

于 2017-08-21T01:30:56.043 に答える
12

これは、上記のFindSumWithAsAndHasの結果です。代替テキスト

これはFindSumWithCastの結果です:代替テキスト

調査結果:

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

  • キャストを使用して、オブジェクトがint if (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]構文はキャストのように見えますが、実際にはアンボックス化され、キャストとアンボックス化は同じ構文を共有します。次回は正しい用語で衒学者になります)アプローチは本当に高速で、値をアンボックス化するだけで済みます。オブジェクトが明らかにintasアプローチを使用することについても同じことは言えません。

于 2010-06-19T14:04:30.403 に答える
9

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

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に、 simulatedasとrealasのアプローチ間でアップル間の比較(つまり、シミュレートされたHasValueの割り当てとシミュレートされたValueの変換の両方が同時に行われる)を実行すると、 simulatedasはrealasよりも大幅に高速であることがわかります。395対3524
  • 最後に、1番目と4番目の結論に基づいて、実装として何か問題があり ます^ _ ^
于 2010-06-21T14:17:32.887 に答える
8

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

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

なので

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

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

于 2009-10-17T19:59:51.323 に答える
8

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

typeof(int) == item.GetType()item is intバージョンと同じ速度で実行され、常に数値を返します(強調:Nullable<int>配列にを書き込んだ場合でも、を使用する必要がありますtypeof(int))。null != itemここで追加のチェックも必要です。

でも

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

typeof-constructは、RuntimeTypeHandleを使用するため、正確な型チェックを行うための最速の方法です。この場合の正確な型はnullableと一致しないので、私の推測では、is/asそれが実際にNullable型のインスタンスであることを確認するために、ここで追加の重労働を行う必要があります。

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

于 2010-06-19T10:01:21.650 に答える
7
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内で、構成のデバッグ、VS2009を使用、Core i7(会社の開発マシン)を使用して行われました。

以下は、VS2010を使用してCore2Duoを使用して私のマシンで実行されました

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
于 2010-04-21T00:28:15.413 に答える