web-dev-qa-db-ja.com

C#でのジェネリックパフォーマンスと非ジェネリックパフォーマンス

私は2つの同等のメソッドを書きました:

static bool F<T>(T a, T b) where T : class
{
    return a == b;
}

static bool F2(A a, A b)
{
    return a == b;
}

時差:
00:00:00.0380022
00:00:00.0170009

テスト用のコード:

var a = new A();
for (int i = 0; i < 100000000; i++)
    F<A>(a, a);
Console.WriteLine(DateTime.Now - dt);

dt = DateTime.Now;
for (int i = 0; i < 100000000; i++)
    F2(a, a);
Console.WriteLine(DateTime.Now - dt);

誰かが理由を知っていますか?

以下のコメントでは、dtb *show [〜#〜] cil [〜#〜]

IL for F2: ldarg.0, ldarg.1, ceq, ret. IL for F<T>: ldarg.0, box !!T, ldarg.1, box !!T, ceq, ret.

それが私の質問の答えだと思いますが、ボクシングを拒否するためにどのような魔法を使うことができますか?

次に、Psilonのコードを使用します。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace ConsoleApplication58
{
    internal class Program
    {
        private class A
        {

        }

        private static bool F<T>(T a, T b) where T : class
        {
            return a == b;
        }

        private static bool F2(A a, A b)
        {
            return a == b;
        }

        private static void Main()
        {
            const int rounds = 100, n = 10000000;
            var a = new A();
            var fList = new List<TimeSpan>();
            var f2List = new List<TimeSpan>();
            for (int i = 0; i < rounds; i++)
            {
                // Test generic
                GCClear();
                bool res;
                var sw = new Stopwatch();
                sw.Start();
                for (int j = 0; j < n; j++)
                {
                    res = F(a, a);
                }
                sw.Stop();
                fList.Add(sw.Elapsed);

                // Test not-generic
                GCClear();
                bool res2;
                var sw2 = new Stopwatch();
                sw2.Start();
                for (int j = 0; j < n; j++)
                {
                    res2 = F2(a, a);
                }
                sw2.Stop();
                f2List.Add(sw2.Elapsed);
            }
            double f1AverageTicks = fList.Average(ts => ts.Ticks);
            Console.WriteLine("Elapsed for F = {0} \t ticks = {1}", fList.Average(ts => ts.TotalMilliseconds),
                              f1AverageTicks);
            double f2AverageTicks = f2List.Average(ts => ts.Ticks);
            Console.WriteLine("Elapsed for F2 = {0} \t ticks = {1}", f2List.Average(ts => ts.TotalMilliseconds),
                  f2AverageTicks);
            Console.WriteLine("Not-generic method is {0} times faster, or on {1}%", f1AverageTicks/f2AverageTicks,
                              (f1AverageTicks/f2AverageTicks - 1)*100);
            Console.ReadKey();
        }

        private static void GCClear()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }
    }
}

Windows 7、.NET 4.5、Visual Studio 2012、リリース、最適化、添付なし。

x64

Elapsed for F = 23.68157         ticks = 236815.7
Elapsed for F2 = 1.701638        ticks = 17016.38
Not-generic method is 13.916925926666 times faster, or on 1291.6925926666%

x86

Elapsed for F = 6.713223         ticks = 67132.23
Elapsed for F2 = 6.729897        ticks = 67298.97
Not-generic method is 0.997522398931217 times faster, or on -0.247760106878314%

そして、私は新しい魔法を手に入れました:x64は3倍高速です...

PS:私のターゲットプラットフォームはx64です。

26
Dmitry

パフォーマンスを正しく測定するために、コードにいくつかの変更を加えました。

  1. ストップウォッチを使用する
  2. リリースモードの実行
  3. インライン化を防止します。
  4. GetHashCode()を使用して実際の作業を行う
  5. 生成されたアセンブリコードを見てください

コードは次のとおりです。

class A
{
}

[MethodImpl(MethodImplOptions.NoInlining)]
static bool F<T>(T a, T b) where T : class
{
    return a.GetHashCode() == b.GetHashCode();
}

[MethodImpl(MethodImplOptions.NoInlining)]
static bool F2(A a, A b)
{
    return a.GetHashCode() == b.GetHashCode();
}

static int Main(string[] args)
{
    const int Runs = 100 * 1000 * 1000;
    var a = new A();
    bool lret = F<A>(a, a);
    var sw = Stopwatch.StartNew();
    for (int i = 0; i < Runs; i++)
    {
        F<A>(a, a);
    }
    sw.Stop();
    Console.WriteLine("Generic: {0:F2}s", sw.Elapsed.TotalSeconds);

    lret = F2(a, a);
    sw = Stopwatch.StartNew();
    for (int i = 0; i < Runs; i++)
    {
        F2(a, a);
    }
    sw.Stop();
    Console.WriteLine("Non Generic: {0:F2}s", sw.Elapsed.TotalSeconds);

    return lret ? 1 : 0;
}

私のテスト中、非汎用バージョンはわずかに高速でした(.NET 4.5 x32 Windows7)。しかし、速度に測定可能な違いは実際にはありません。私は両方が等しいと言うでしょう。完全を期すために、汎用バージョンのアセンブリコードを示します。JIT最適化を有効にしたリリースモードでデバッガーを介してアセンブリコードを取得しました。デフォルトでは、デバッグ中にJIT最適化を無効にして、ブレークポイントと変数の検査の設定を容易にします。

ジェネリック

static bool F<T>(T a, T b) where T : class
{
        return a.GetHashCode() == b.GetHashCode();
}

Push        ebp 
mov         ebp,esp 
Push        ebx 
sub         esp,8 // reserve stack for two locals 
mov         dword ptr [ebp-8],ecx // store first arg on stack
mov         dword ptr [ebp-0Ch],edx // store second arg on stack
mov         ecx,dword ptr [ebp-8] // get first arg from stack --> stupid!
mov         eax,dword ptr [ecx]   // load MT pointer from a instance
mov         eax,dword ptr [eax+28h] // Locate method table start
call        dword ptr [eax+8] //GetHashCode // call GetHashCode function pointer which is the second method starting from the method table
mov         ebx,eax           // store result in ebx
mov         ecx,dword ptr [ebp-0Ch] // get second arg
mov         eax,dword ptr [ecx]     // call method as usual ...
mov         eax,dword ptr [eax+28h] 
call        dword ptr [eax+8] //GetHashCode
cmp         ebx,eax 
sete        al 
movzx       eax,al 
lea         esp,[ebp-4] 
pop         ebx 
pop         ebp 
ret         4 

非ジェネリック

static bool F2(A a, A b)
{
  return a.GetHashCode() == b.GetHashCode();
}

Push        ebp 
mov         ebp,esp 
Push        esi 
Push        ebx 
mov         esi,edx 
mov         eax,dword ptr [ecx] 
mov         eax,dword ptr [eax+28h] 
call        dword ptr [eax+8] //GetHashCode
mov         ebx,eax 
mov         ecx,esi 
mov         eax,dword ptr [ecx] 
mov         eax,dword ptr [eax+28h] 
call        dword ptr [eax+8] //GetHashCode
cmp         ebx,eax 
sete        al 
movzx       eax,al 
pop         ebx 
pop         esi 
pop         ebp 
ret 

ご覧のとおり、完全ではないスタックメモリ操作が多いため、汎用バージョンは少し非効率に見えますが、実際には、すべてがプロセッサのL1キャッシュに収まり、メモリ操作のコストがに比べて低くなるため、違いは測定できません。非汎用バージョンの純粋なレジスタ操作。 CPUキャッシュから来ていない実際のメモリアクセスにお金を払う必要がある場合は、非汎用バージョンの方が現実の世界で少しパフォーマンスが向上するはずです。

すべての実用的な目的で、これらの両方の方法は同じです。実際のパフォーマンスを向上させるには、他の場所を検討する必要があります。まず、データアクセスパターンと使用されるデータ構造を確認します。アルゴリズムの変更は、そのような低レベルのものよりもはるかに多くのパフォーマンスゲインをもたらす傾向があります。

編集1:==を使用したい場合は、次のようになります

00000000  Push        ebp 
00000001  mov         ebp,esp 
00000003  cmp         ecx,edx // Check for reference equality 
00000005  sete        al 
00000008  movzx       eax,al 
0000000b  pop         ebp 
0000000c  ret         4 

どちらの方法でも、まったく同じマシンコードが生成されます。測定した違いは、測定誤差です。

20
Alois Kraus

テスト方法に欠陥があります。あなたがそれをした方法にはいくつかの大きな問題があります。

まず、「 warm-up "」を指定しませんでした。 .NETでは、初めて何かにアクセスすると、後続の呼び出しよりも遅くなるため、必要なアセンブリをロードできます。このようなテストを実行する場合は、各機能を少なくとも1回実行する必要があります。そうしないと、最初に実行するテストに大きなペナルティが発生します。先に進んで順序を入れ替えると、逆の結果が表示される可能性があります。

2番目 DateTimeは16ミリ秒までしか正確ではありません したがって、2回比較すると、32ミリ秒の+/-エラーが発生します。 2つの結果の差は21ミリ秒で、実験誤差の範囲内です。 Stopwatch クラスのようなより正確なタイマーを使用する必要があります。

最後に、このような人工的なテストを行わないでください。あるクラスまたは別のクラスの自慢する権利以外に役立つ情報は表示されません。代わりに コードプロファイラー の使い方を学びましょう。これにより、コードの速度が低下している原因がわかり、テンプレートクラスを使用しないとコードが高速になると「推測」する代わりに、情報に基づいて問題を解決する方法を決定できます。

これは、それがどのように「行われるべきか」を示すサンプルコードです。

using System;
using System.Diagnostics;

namespace Sandbox_Console
{
    class A
    {
    }

    internal static class Program
    {
        static bool F<T>(T a, T b) where T : class
        {
            return a == b;
        }

        static bool F2(A a, A b)
        {
            return a == b;
        }

        private static void Main()
        {
            var a = new A();
            Stopwatch st = new Stopwatch();

            Console.WriteLine("warmup");
            st.Start();
            for (int i = 0; i < 100000000; i++)
                F<A>(a, a);
            Console.WriteLine(st.Elapsed);

            st.Restart();
            for (int i = 0; i < 100000000; i++)
                F2(a, a);
            Console.WriteLine(st.Elapsed);

            Console.WriteLine("real");
            st.Restart();
            for (int i = 0; i < 100000000; i++)
                F<A>(a, a);
            Console.WriteLine(st.Elapsed);

            st.Restart();
            for (int i = 0; i < 100000000; i++)
                F2(a, a);
            Console.WriteLine(st.Elapsed);

            Console.WriteLine("Done");
            Console.ReadLine();
        }

    }
}

そしてここに結果があります:

warmup
00:00:00.0297904
00:00:00.0298949
real
00:00:00.0296838
00:00:00.0297823
Done

最後の2つの順序を入れ替えると、最初の順序は常に短くなるため、実験誤差の範囲内であるため、事実上「同じ時間」になります。

6

タイミングについて心配するのをやめ、正確さについて心配してください。

それらのメソッドはnot同等です。 1つはclass Aoperator==を使用し、もう1つはobjectoperator==を使用します。

5
Ben Voigt

私はあなたのテストコードを書き直しました:

var stopwatch = new Stopwatch();
var a = new A();

stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < 100000000; i++)
    F<A>(a, a);
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);

stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < 100000000; i++)
    F2(a, a);
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);

順序を入れ替えても何も変わりません。

[〜#〜] cil [〜#〜] ジェネリックメソッドの場合:

L_0000: nop
L_0001: ldarg.0
L_0002: box !!T
L_0007: ldarg.1
L_0008: box !!T
L_000d: ceq
L_000f: stloc.0
L_0010: br.s L_0012
L_0012: ldloc.0
L_0013: ret

そして非ジェネリックの場合:

L_0000: nop
L_0001: ldarg.0
L_0002: ldarg.1
L_0003: ceq
L_0005: stloc.0
L_0006: br.s L_0008
L_0008: ldloc.0
L_0009: ret

だからボクシングの操作はあなたの時差の理由です。問題は、ボクシング操作が追加される理由です。チェックしてください、スタックオーバーフローの質問C#でジェネリックを使用する場合のボクシング

3
nirmus

2つのこと:

  1. DateTime.Nowでベンチマークしています。代わりにStopwatchを使用してください。
  2. 通常の状況ではないコードを実行しています。 JITはおそらく最初の実行に影響を及ぼし、最初のメソッドを遅くします。

テストの順序を切り替えると(つまり、最初に非ジェネリックメソッドをテストする)、結果は逆になりますか?私はそう思うでしょう。コードを LINQPad に接続し、それをコピーして両方のテストを2回実行すると、 2回目の反復は、互いに数百ティック以内でした。

だから、あなたの質問に答えて:はい、誰かが理由を知っています。ベンチマークが不正確だからです!

3
Dan Puzey

私はキャリアの中で何度か専門的な能力でパフォーマンス分析を行い、いくつかの観察を行ってきました。

  • まず、テストが短すぎて有効ではありません。私の経験則では、パフォーマンステストは30分ほど実行する必要があります。
  • 次に、さまざまなタイミングを取得するために、テストを何度も実行することが重要です。
  • 第三に、関数の結果が使用されず、呼び出される関数に副作用がないため、コンパイラがループを最適化していないことに驚いています。
  • 第4に、マイクロベンチマークは誤解を招くことがよくあります。

私はかつて、大胆なパフォーマンス目標を掲げたコンパイラチームで働いていました。あるビルドでは、特定のシーケンスに対するいくつかの命令を排除する最適化が導入されました。パフォーマンスが向上するはずでしたが、代わりに1つのベンチマークのパフォーマンスが劇的に低下しました。直接マップされたキャッシュを備えたハードウェアで実行していました。ループのコードと内側のループで呼び出された関数は、新しい最適化が行われた同じキャッシュラインを占有していましたが、以前に生成されたコードでは占有されていなかったことがわかりました。言い換えると、そのベンチマークは実際にはメモリベンチマークであり、メモリキャッシュのヒットとミスに完全に依存していましたが、作成者は計算ベンチマークを作成したと考えていました。

2
KC-NH

それはもっと公平に思えますね?:D

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace ConsoleApplication58
{
    internal class Program
    {
        private class A
        {

        }

        private static bool F<T>(T a, T b) where T : class
        {
            return a == b;
        }

        private static bool F2(A a, A b)
        {
            return a == b;
        }

        private static void Main()
        {
            const int rounds = 100, n = 10000000;
            var a = new A();
            var fList = new List<TimeSpan>();
            var f2List = new List<TimeSpan>();
            for (int i = 0; i < rounds; i++)
            {
                //test generic
                GCClear();
                bool res;
                var sw = new Stopwatch();
                sw.Start();
                for (int j = 0; j < n; j++)
                {
                    res = F(a, a);
                }
                sw.Stop();
                fList.Add(sw.Elapsed);

                //test not-generic
                GCClear();
                bool res2;
                var sw2 = new Stopwatch();
                sw2.Start();
                for (int j = 0; j < n; j++)
                {
                    res2 = F2(a, a);
                }
                sw2.Stop();
                f2List.Add(sw2.Elapsed);
            }
            double f1AverageTicks = fList.Average(ts => ts.Ticks);
            Console.WriteLine("Elapsed for F = {0} \t ticks = {1}", fList.Average(ts => ts.TotalMilliseconds),
                              f1AverageTicks);
            double f2AverageTicks = f2List.Average(ts => ts.Ticks);
            Console.WriteLine("Elapsed for F2 = {0} \t ticks = {1}", f2List.Average(ts => ts.TotalMilliseconds),
                  f2AverageTicks);
            Console.WriteLine("Not-generic method is {0} times faster, or on {1}%", f1AverageTicks/f2AverageTicks,
                              (f1AverageTicks/f2AverageTicks - 1)*100);
            Console.ReadKey();
        }

        private static void GCClear()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }
    }
}

私のラップトップi7-3615qmでは、ジェネリックは非ジェネリックよりも高速です。

http://ideone.com/Y1GIJK を参照してください。

1
Psilon

それが私の質問の答えだと思いますが、ボクシングを拒否するためにどのような魔法を使うことができますか?

あなたの目標が比較することだけであるならば、あなたはこれをすることができます:

    public class A : IEquatable<A> {
        public bool Equals( A other ) { return this == other; }
    }
    static bool F<T>( IEquatable<T> a, IEquatable<T> b ) where T : IEquatable<T> {
        return a==b;
    }

これはボクシングを回避します。

主なタイミングのずれについては、ストップウォッチの設定に問題があったことは、すでに誰もが知っていると思います。時間の結果からループ自体を削除したい場合は、空のベースラインを取得し、それを時間の差から差し引くという別の手法を使用します。完璧ではありませんが、公正な結果が得られ、タイマーの開始と停止が何度も遅くなることはありません。

1
mdannov