为什么在没有约束的泛型方法上将可空值类型与 null 进行比较会更慢?

问题描述 投票:0回答:3

我遇到了一个非常有趣的情况,在泛型方法中比较可空类型与 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 能够对此进行优化,但事实并非如此(我正在运行优化) .

c# performance nullable jit
3个回答
14
投票

您应该执行以下操作来调查此问题。

首先重写程序,使其执行“所有事情”两次。 在两次迭代之间放置一个消息框。 在启用优化的情况下编译程序,然后运行该程序而不是在调试器中。 这确保了抖动能够生成最佳的代码。抖动知道调试器何时被连接,并且如果它认为这就是您正在做的事情,则可以生成更糟糕的代码以使调试更容易。 当弹出消息框时,附加调试器,然后在汇编代码级别跟踪代码的三个不同版本(如果实际上甚至存在三个不同版本)。我愿意打一美元的赌注,因为第一个代码不会生成任何代码,因为抖动知道整个事情可以优化为“返回 false”,然后可以内联返回 false,也许甚至可以删除循环。

(将来,您在编写性能测试时可能应该考虑这一点。请记住,如果您不

使用结果

,那么抖动可以完全优化掉产生该结果的一切,只要它具有无副作用。) 一旦您查看汇编代码,您就会明白发生了什么。

我本人没有对此进行过调查,但很有可能发生的事情是这样的:

    在 int 代码路径中,抖动是意识到装箱 int 永远不会为 null 并将该方法变成“返回 false”
  • 在字符串代码路径中,抖动意识到测试字符串是否为空相当于测试指向字符串的托管指针是否为零,因此它生成一条测试寄存器是否为零的指令。
  • 在整数中? codepath,可能抖动是意识到测试 int ?对于无效性可以通过装箱 int 来完成吗? -- 因为装箱的 null int 是一个 null 引用,所以这就减少了之前测试托管指针是否为零的问题。但你承担拳击的费用。
  • 如果是这种情况,那么这里的抖动可能会更加复杂,并且意识到测试 int 了吗? for null 可以通过返回 int? 内 HasValue bool 的逆来完成。

但就像我说的,这只是一个猜测。如果您有兴趣,可以自己生成代码并看看它在做什么。


5
投票

第一个看起来像:

.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 优化。


3
投票

© www.soinside.com 2019 - 2024. All rights reserved.