是否有 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));
_handle
和 handle
都是 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
方法。
如果标题有意义,请跳到下面我的问题部分。如果没有,这里有一些背景信息。
我正在写一些代码,正好用到了null-coalescing运算符。 我的代码:
_handle = handle ?? throw new ArgumentNullException (nameof(handle));
_handle
和 handle
都是 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 模式并在以下情况下以相同的方式对待它们发出本机代码。
对于你问题的第二部分,如brfalse
/brtrue
方法。