Verificações de nulos implícitos são uma técnica para remover verificações explícitas para referências/pontos nulos em uma representação nativa de linguagens de alto nível e, em vez disso, confiar no processador emitindo uma violação de acesso, que é então manipulada (ou seja, SEH) e traduzida em uma exceção gerenciada. É usada principalmente em casos em que a sobrecarga de manipulação de exceções é secundária, por exemplo, se sabemos que exceções nulas são raras.
Em todos os exemplos que encontrei, essas verificações são feitas para instruções que fazem um acesso ao ptr em questão:
int m(Foo foo) {
return foo.x;
}
Aqui, poderíamos simplesmente emitir o código asm:
mov rax,[rcx]
E fazer com que o mecanismo nativo de tratamento de exceções lide com a geração de uma NullReferenceException, em vez de travar.
Mas e quanto às chamadas de função?
int m(Foo foo) {
return foo.MemberFunction();
}
É possível usar verificações de nulos implícitas lá também? Estou interessado especificamente em x64-asm. Parece mais difícil lá. Vamos dar uma olhada em uma chamada de função não virtual exemplar em asm (o código não corresponde à função 1:1, ele contém um "mov" apenas para mostrar que um objeto é configurado no registro usado para uma chamada de função de membro no 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
Aqui, não temos nenhum acesso à memória para a qual "rcx" aponta. Então, se por definição da linguagem, tal chamada deve lançar uma NullReferenceException no call-site, precisaríamos usar verificações explícitas:
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();
Ou existe alguma maneira mais eficiente de substituir o par test+je por uma instrução, que gera uma violação de acesso? Eu estava pensando que poderia fazer
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
Isso não usaria nenhuma ramificação e não exigiria uma chamada adicional para uma invocação de exceção. No entanto, ele potencialmente precisaria ler a memória de [rcx], o que não é necessário no outro método. Como isso funciona em comparação com a ramificação? Se for pior, há alguma maneira que seja melhor? Veja abaixo para mais explicações do caso de uso completo.
Fundo
Tenho uma linguagem de alto nível personalizada, que é compilada para bytecode e, em seguida, para ASM nativo. A linguagem lida com verificações de nulos graciosamente com exceções de NullReference. Exceções ainda são sempre erros que precisam ser abordados , e não algo que ocorre normalmente. Portanto, o código para lidar com exceções pode ser ineficiente. O importante é que o código seja executado o mais rápido possível, dado o caso comum de nenhuma exceção (portanto, nenhuma referência nula). É por isso que verificações de nulos implícitas parecem atraentes. Remover todas as ramificações e código adicional necessário para lidar com a exceção para chamadas pode ser benéfico. No entanto, mesmo as verificações existentes já devem ser rápidas. A ramificação deve ser bem previsível para ser sempre falsa, e eu já fiz isso para que este caso não exija um jmp, mas tenha o código executado linearmente (o que eu li ser mais otimizado).
Então, dado isso, minha tentativa de me livrar desses cheques no caso que mencionei é tola ou há alguma maneira de fazer isso de forma otimizada?