c# - "as"型とnull可能型のパフォーマンス驚き




performance clr nullable unboxing (9)

私はC#の第4章を変更しています。Depthではヌル可能な型を取り扱っています。「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"コードに勝っています。

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);
    }
}

Answers

isinstはヌル可能な型では本当に遅いです。 メソッドFindSumWithCastで変更されました

if (o is int)

if (o is int?)

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

isinst     [mscorlib]System.Int32

変更される

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

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]

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

私のマシンでは、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

興味深いことに、私は、 Nullable<T>この初期テストと同様)のdynamicを一桁遅くすることでオペレータのサポートについてのフィードバックを受けました。 これは非常によく似た理由によるものです。

Nullable<T>大好きです。 もう一つの楽しいものは、JITがヌル可能な構造体のためにNullable<T>発見(および削除)しても、 Nullable<T>ためにそれをborksするということです。

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;
    }
}

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

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

として

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

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


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

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手法はアプローチ手法よりもはるかに高速です。 303対3524
  • 第二に、。値は鋳造よりわずかに遅いです。 3524対3272
  • 第3に、.HasValueは、マニュアルを使用する(すなわちisを使用する)場合よりわずかに遅くなります。 3524対3282
  • 第4に、 シミュレートさ れたアプローチと実際のアプローチの間でリンゴとリンゴの比較(つまり、シミュレートされたHasValueの割り当てとシミュレートされたValueの変換は共に起こります)を実行するとシミュレートされたシミュレーション実際よりもはるかに高速です。 395対3524
  • 最後に、第1と第4の結論に基づいて実装には何か問題があります^ _ ^

この回答を最新の状態に保つために、このページの議論の大部分は現在、 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つの共通の使用が、 IEquatable<MyStruct> (ほとんどの場合)を実装する.NETの値の型 (つまりC#での struct )をIEquatable<MyStruct>ていることがIEquatable<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構文のパフォーマンスに関する私の発言を裏付けるさらなるテストについては、 here (特に「D」の例)を参照してください。


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

まず、C# as演算子はisinst IL命令をisinstます( is演算子も同様is )。 (別の興味深い命令は、直接キャストを実行するときにcastclassであり、コンパイラは実行時の検査を省略できないことを知っています)。

isinstは何をしていますか( ECMA 335 Partition III、4.6 ):

フォーマット: isinst typeTok

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

typeTokがnullでない値の型またはジェネリックなパラメータの型である場合、それは "boxed" typeTokとして解釈されます。

typeTokがヌル可能型、 Nullable<T>場合、それは "boxed" Tと解釈されます

最も重要なこと:

objの実際の型(ベリファイアトラッキングされた型ではない)がベリファイアによって typeTok型に代入可能な場合、 isinst成功し、 obj結果として)はそのまま返され、検証はtypeをtypeTokとして追跡します。 変換(§1.6)と変換(§3.27)とは異なり、 isinstはオブジェクトの実際のタイプを決して変更せず、オブジェクトIDを保持します(パーティションIを参照)。

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

それはなぜそれをするのですか? isinst T? 明らかになっていたはずの効果はありません。すなわち、あなたはT?戻しT? 。 代わりに、これらの指示はすべてあなたが"boxed T"できない"boxed T"を持っていることを保証しますT? 。 実際のT?を得るにはT? 、私たちはまだ私たちの"boxed T"T?にunboxする必要がありT? これはコンパイラがunbox.any後にisinstする理由isinst 。 あなたがそれについて考えるなら、これは理にかなっています。なぜなら、 T? "箱形式" 単なる"boxed T"あり、 castclassisinst実行すると、unboxは矛盾したものになります。

Hansの検索結果を標準からの情報でバックアップすると、ここに行く:

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

unbox.any命令は、値タイプのボックス形式に適用すると、obj( O型)に含まれる値を抽出します。 (これはunboxunbox相当します)。参照型に適用すると、 unbox.any命令はunbox.anyunbox.anyと同じ効果をcastclassます。

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

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


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

is演算子テストは簡単です。オブジェクトがヌルではなく、期待される型のものであるかどうかをチェックするだけですが、いくつかのマシンコード命令が必要です。 キャストも簡単で、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がここで問題を引き起こすかもしれないと思う。


public AbstractType New
{
    get
    {
        return (AbstractType) Activator.CreateInstance(GetType());
    }
}




c# performance clr nullable unboxing