我写了两个等价的方法:
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 * 显示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,发布,优化,无需附加。
64
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%
86
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快三倍......
PS:我的目标平台是x64。
答案 0 :(得分:18)
我确实对您的代码进行了一些更改,以正确测量性能。
以下是代码:
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 Windows 7)。 但速度几乎没有可测量的差异。我会说两者都是平等的。 为了完整性,这里是通用版本的汇编代码: 我在发布模式下通过调试器获得了汇编代码,并启用了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
两种方法都生成完全相同的机器代码。您测量的任何差异都是您的测量误差。
答案 1 :(得分:5)
您的测试方法存在缺陷。如何做到这一点有一些大问题。
首先,您没有提供“warm-up”。在.NET中,第一次访问它时,它将比后续调用慢,因此它可以加载任何所需的程序集。如果您要执行这样的测试,您必须至少执行一次每个功能,否则第一次运行测试将受到很大的惩罚。继续交换订单,您可能会看到相反的结果。
第二个DateTime
is only accurate to 16ms,所以当比较两次时,你的+/-错误为32毫秒。两个结果之间的差异是21毫秒,完全在实验误差范围内。您必须使用更准确的计时器,例如Stopwatch类。
最后,不要做这样的人工测试。除了吹嘘一个或另一个类别的权利之外,它们不会向您显示任何有用的信息。而是学会使用Code Profiler。这将向您展示什么在减慢您的代码速度,您可以就如何解决问题做出明智的决定,而不是“猜测”不使用模板化的类将使您的代码更快。
这是一个示例代码,显示了它应该如何完成:
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 :(得分:5)
不要担心时间安排,担心正确性。
这些方法不等效。其中一个使用class A
的{{1}},另一个使用operator==
的{{1}}。
答案 3 :(得分:3)
两件事:
DateTime.Now
进行基准测试。请改用Stopwatch
。如果你改变测试的顺序(即首先测试非泛型方法),你的结果会反转吗?我会怀疑的。当我将代码插入LINQPad,然后将其复制以便它运行两次两次时,第二次迭代的执行时间在彼此的几百个小时内。
所以,回答你的问题:是的,有人知道为什么。这是因为你的基准测试不准确!
答案 4 :(得分:3)
我重写了你的测试代码:
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
所以拳击手术是你时差的原因。问题是为什么添加拳击操作。检查一下,Stack Overflow问题 Boxing when using generics in C#
答案 5 :(得分:2)
在我的职业生涯中,我已经多次以专业身份进行过绩效分析,并且有几点意见。
我曾经在一个具有大胆的性能目标的编译团队工作过。一个构建引入了一个优化,消除了特定序列的几个指令。它应该有改进的性能,但相反,一个基准的性能急剧下降。我们使用直接映射缓存在硬件上运行。事实证明,循环的代码和内部循环中调用的函数占用了相同的缓存行,并且新的优化就位,但没有使用先前生成的代码。换句话说,该基准测试实际上是一个内存基准测试,完全依赖于内存缓存命中和未命中,而作者认为他们已经编写了计算基准。
答案 6 :(得分: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上,通用更快而不是非通用。
答案 7 :(得分:1)
我认为这是我的问题的答案,但我可以使用什么魔法来否定拳击?
如果你的目标只是比较,你可以这样做:
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;
}
这将避免拳击。
至于主要的时间偏差,我想每个人都已经确定了你如何设置秒表的问题。我使用了一种不同的技术,如果我想从时间结果中删除循环本身,我会采用空基线并从时间差中减去它。它并不完美,但它会产生一个公平的结果,并且不会因反复启动和停止计时器而减慢。