我遇到了一个非常有趣的情况,在泛型方法中比较可空类型与 null 比比较值类型或引用类型慢 234 倍。代码如下:
static bool IsNull<T>(T instance)
{
return instance == null;
}
执行代码为:
int? a = 0;
string b = "A";
int c = 0;
var watch = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
var r1 = IsNull(a);
}
Console.WriteLine(watch.Elapsed.ToString());
watch.Restart();
for (int i = 0; i < 1000000; i++)
{
var r2 = IsNull(b);
}
Console.WriteLine(watch.Elapsed.ToString());
watch.Restart();
for (int i = 0; i < 1000000; i++)
{
var r3 = IsNull(c);
}
watch.Stop();
Console.WriteLine(watch.Elapsed.ToString());
Console.ReadKey();
上面代码的输出是:
00:00:00.1879827
00:00:00.0008779
00:00:00.0008532
如您所见,比较可空 int 与 null 比比较 int 或字符串慢 234 倍。如果我添加具有正确约束的第二个重载,结果会发生巨大变化:
static bool IsNull<T>(T? instance) where T : struct
{
return instance == null;
}
现在结果是:
00:00:00.0006040
00:00:00.0006017
00:00:00.0006014
这是为什么呢?我没有检查字节码,因为我对它不太熟悉,但即使字节码有点不同,我也希望 JIT 能够对此进行优化,但事实并非如此(我正在运行优化) .
您应该执行以下操作来调查此问题。
首先重写程序,使其执行“所有事情”两次。 在两次迭代之间放置一个消息框。 在启用优化的情况下编译程序,然后运行该程序而不是在调试器中。 这确保了抖动能够生成最佳的代码。抖动知道调试器何时被连接,并且如果它认为这就是您正在做的事情,则可以生成更糟糕的代码以使调试更容易。 当弹出消息框时,附加调试器,然后在汇编代码级别跟踪代码的三个不同版本(如果实际上甚至存在三个不同版本)。我愿意打一美元的赌注,因为第一个代码不会生成任何代码,因为抖动知道整个事情可以优化为“返回 false”,然后可以内联返回 false,也许甚至可以删除循环。
(将来,您在编写性能测试时可能应该考虑这一点。请记住,如果您不
使用结果,那么抖动可以完全优化掉产生该结果的一切,只要它具有无副作用。) 一旦您查看汇编代码,您就会明白发生了什么。
我本人没有对此进行过调查,但很有可能发生的事情是这样的:
但就像我说的,这只是一个猜测。如果您有兴趣,可以自己生成代码并看看它在做什么。
第一个看起来像:
.method private hidebysig static bool IsNull<T>(!!T instance) cil managed
{
.maxstack 2
.locals init (
[0] bool CS$1$0000)
L_0000: nop
L_0001: ldarg.0
L_0002: box !!T
L_0007: ldnull
L_0008: ceq
L_000a: stloc.0
L_000b: br.s L_000d
L_000d: ldloc.0
L_000e: ret
}
第二个看起来像:
.method private hidebysig static bool IsNull<valuetype ([mscorlib]System.ValueType) .ctor T>(valuetype [mscorlib]System.Nullable`1<!!T> instance) cil managed
{
.maxstack 2
.locals init (
[0] bool CS$1$0000)
L_0000: nop
L_0001: ldarga.s instance
L_0003: call instance bool [mscorlib]System.Nullable`1<!!T>::get_HasValue()
L_0008: ldc.i4.0
L_0009: ceq
L_000b: stloc.0
L_000c: br.s L_000e
L_000e: ldloc.0
L_000f: ret
}
在第二种情况下,编译器知道该类型是 Nullable,因此它可以对此进行优化。在第一种情况下,它必须处理任何类型,包括引用类型和值类型。所以它必须跳过一些额外的障碍。
至于为什么 int 比 int 快?,我想这里面涉及到一些 JIT 优化。