私は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です。
パフォーマンスを正しく測定するために、コードにいくつかの変更を加えました。
コードは次のとおりです。
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
どちらの方法でも、まったく同じマシンコードが生成されます。測定した違いは、測定誤差です。
テスト方法に欠陥があります。あなたがそれをした方法にはいくつかの大きな問題があります。
まず、「 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つの順序を入れ替えると、最初の順序は常に短くなるため、実験誤差の範囲内であるため、事実上「同じ時間」になります。
タイミングについて心配するのをやめ、正確さについて心配してください。
それらのメソッドはnot同等です。 1つはclass A
のoperator==
を使用し、もう1つはobject
のoperator==
を使用します。
私はあなたのテストコードを書き直しました:
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#でジェネリックを使用する場合のボクシング
2つのこと:
DateTime.Now
でベンチマークしています。代わりにStopwatch
を使用してください。テストの順序を切り替えると(つまり、最初に非ジェネリックメソッドをテストする)、結果は逆になりますか?私はそう思うでしょう。コードを LINQPad に接続し、それをコピーして両方のテストを2回実行すると、 2回目の反復は、互いに数百ティック以内でした。
だから、あなたの質問に答えて:はい、誰かが理由を知っています。ベンチマークが不正確だからです!
私はキャリアの中で何度か専門的な能力でパフォーマンス分析を行い、いくつかの観察を行ってきました。
私はかつて、大胆なパフォーマンス目標を掲げたコンパイラチームで働いていました。あるビルドでは、特定のシーケンスに対するいくつかの命令を排除する最適化が導入されました。パフォーマンスが向上するはずでしたが、代わりに1つのベンチマークのパフォーマンスが劇的に低下しました。直接マップされたキャッシュを備えたハードウェアで実行していました。ループのコードと内側のループで呼び出された関数は、新しい最適化が行われた同じキャッシュラインを占有していましたが、以前に生成されたコードでは占有されていなかったことがわかりました。言い換えると、そのベンチマークは実際にはメモリベンチマークであり、メモリキャッシュのヒットとミスに完全に依存していましたが、作成者は計算ベンチマークを作成したと考えていました。
それはもっと公平に思えますね?: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 を参照してください。
それが私の質問の答えだと思いますが、ボクシングを拒否するためにどのような魔法を使うことができますか?
あなたの目標が比較することだけであるならば、あなたはこれをすることができます:
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;
}
これはボクシングを回避します。
主なタイミングのずれについては、ストップウォッチの設定に問題があったことは、すでに誰もが知っていると思います。時間の結果からループ自体を削除したい場合は、空のベースラインを取得し、それを時間の差から差し引くという別の手法を使用します。完璧ではありませんが、公正な結果が得られ、タイマーの開始と停止が何度も遅くなることはありません。