隐式空值检查是一种技术,用于在高级语言本机表示中移除对空指针/引用的显式检查,而是依靠处理器发出访问冲突,然后对其进行处理(即 SEH),并将其转换为托管异常。它主要用于异常处理开销次要的情况,例如,如果我们知道空值异常很少见。
在我发现的所有例子中,这些检查都是针对访问相关 ptr 的语句进行的:
int m(Foo foo) {
return foo.x;
}
这里,我们可以简单地发出 asm 代码:
mov rax,[rcx]
并让本机异常处理机制处理生成 NullReferenceException,而不是崩溃。
但是,函数调用又如何呢?
int m(Foo foo) {
return foo.MemberFunction();
}
是否也可以在那里使用隐式空检查?我对 x64-asm 特别感兴趣。那里似乎更难。让我们看一个 asm 中的非虚拟函数调用示例(代码与函数 1:1 不匹配,它包含一个“mov”,只是为了显示一个对象被设置到用于 Windows 上的成员函数调用的寄存器中):
mov rcx,[rsp+20h] // load target-object from stack-local (Foo*)
call Foo::MemberFunction // call Foo::MemberFunction, can be represented with an address w/o fixups of the ptr
在这里,我们无法访问“rcx”指向的内存。因此,如果根据语言的定义,这样的调用必须在调用点抛出 NullReferenceException,我们需要使用显式检查:
mov rcx,[rsp+32h] // load target-object from stack-local (Foo*)
test rcx,rcx
je .L0 // exception-handler already moved out of hot-path
call Foo::MemberFunction // call Foo::MemberFunction, can be represented with an address w/o fixups of the ptr
...
.L0:
call throwNullReferenceException();
或者有没有更有效的方法用一条产生访问冲突的指令替换 test+je 对?我想我可以这样做
mov rcx,[rsp+32h] // load target-object from stack-local (Foo*)
mov rax,[rcx] // mov into unused reg, to trigger access-violation
call Foo::MemberFunction // call Foo::MemberFunction, can be represented with an address w/o fixups of the ptr
这不会使用分支,也不需要额外调用异常调用。但是,它可能需要读取 [rcx] 的内存,而另一种方法不需要。与分支相比,它的性能如何?如果更差,有没有更好的方法?请参阅下文以进一步解释完整的用例。
背景
我有一种自定义的高级语言,它被编译为字节码,然后被编译为本机 ASM。该语言使用 NullReference 异常优雅地处理空检查。异常仍然始终是需要解决的错误,而不是正常发生的事件。因此,处理异常的代码可能效率低下。重要的是,在通常没有异常(因此没有空引用)的情况下,代码运行得尽可能快。这就是隐式空检查看起来很有吸引力的原因。删除处理调用异常所需的所有分支和额外代码可能会有益。不过,即使是现有的检查也应该已经很快了。分支应该可以很好地预测为始终为假,并且我已经这样做了,所以这种情况根本不需要 jmp,而是让代码线性执行(我读过这是更优化的)。
那么考虑到这一点,我在上述情况下试图摆脱这些检查是否愚蠢,或者是否有某种方法可以最佳地实现它?