是否有 C# 代码可以直接在 object 引用上调用 `brtrue`/`brfalse` 指令的 null-check 特性?

Is there C# code to invoke the null-check feature of `brtrue`/`brfalse` instruction directly on an object reference?

如果标题有意义,请跳到下面我的问题部分。如果没有,这里有一些背景信息。

我正在写一些代码,正好用到了null-coalescing运算符。 我的代码:

_handle = handle ?? throw new ArgumentNullException (nameof(handle));

_handlehandle 都是 class Handle 类型(我写的)。

我决定调查 IL。这是它吐出的内容:

...
IL_0002: ldarg.1      // handle
IL_0003: dup          
IL_0004: brtrue.s     IL_0012   // transfer to line that stores into _handle
IL_0006: pop          
IL_0007: ldstr        "handle"
IL_000c: newobj       instance void [mscorlib]System.ArgumentNullException::.ctor(string)
IL_0011: throw        
IL_0012: stfld        class Sandbox.V1.Handle Sandbox.V1.Spade::_handle
...

我在这里看到 brtrue.s(或者可能是代码略有不同的 brfalse.s)指令 (IL_0004) 并没有真正检查 handle 引用等于(或不等于)null (ldnull).
相反,它直接在 handle 引用上调用 brfalse

也就是说,它一开始并没有按照这样的思路去做:

...
IL_0001: ldarg.1      // handle
IL_0002: ldnull       
IL_0003: ceq          
IL_0004: brfalse.s    IL_0012   // transfer to line that stores into _handle
...

当然第二种方式生成的IL代码会多一些,但是为了本题的目的,我只敲了相关的部分。

问题:
1) 这两种方法之间是否存在任何可衡量的差异?
第一种方法(这是编译器对我上面的 C# 代码所做的)直接在 object 引用上调用 brtrue/false),而第二种方法在 brtrue/false 的比较结果上调用 brtrue/false =65=] 和 ldnull(这是编译器用 == null 检查生成的内容)。
据我了解,第一种方法至少处理了一条较少的指令,因此在某种程度上,这是可以衡量的。
2) 如果是这样,有没有办法强制编译器在 C# 中为 null-checks 生成这样的 IL?
即在 C# 中编写 myObj != null 的等价物,其中生成的 IL 直接在 myObj 引用上调用 brtrue.s

或者我错过了一些基本的东西? IL 不是我的强项。
我也欢迎有人合理地说服我,我只是在这里疯了:)

提前致谢!

问题是您正在查看调试版本。如果您担心微优化,查看关闭优化的构建是荒谬的。

在发布版本中,handle == null 也将使用 btrue.s。然而,在调试版本中,生成的 IL 将尽可能接近实际的 C# 代码,以获得良好的调试体验。

以下代码:

public void M(object o) {
    if (o == null)
        throw new ArgumentException(); }

在Release模式下会输出如下IL:

IL_0000: ldarg.1
IL_0001: brtrue.s IL_0009
IL_0003: newobj instance void [mscorlib]System.ArgumentException::.ctor()
IL_0008: throw
IL_0009: ret

正如评论中提到的,IL 只是在执行代码时实际运行的部分。因此,关于您问题的第一部分,ldnull/ceq 版本较长这一事实自然意味着加载它需要 稍微 更长的时间到内存中,更多的指令供 JIT 分析和编译为本机代码。

然而,考虑到差异有多么微小,这些差异基本上可以忽略不计,并且由于它们是一次性活动,因此实际上与 JIT-ted 方法实际运行时的性能无关。为此,我们需要查看在 JIT 方法时实际生成了哪些指令,以及是否存在任何差异。

为此,我将两个动态方法放在一起,显式发出所需的 IL,并检查 JIT 为两个版本生成了哪些本机代码。

结果可能完全相同(具体来说,test XXX,XXX 后跟 je ...),因此显然 JIT 有能力识别这些 IL 模式并在以下情况下以相同的方式对待它们发出本机代码。

对于你问题的第二部分,如所示,一旦在Release模式下编译,生成的IL之间的差异就消失了,并且都使用较短的brfalse/brtrue方法。