AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / coding / Perguntas / 79091005
Accepted
Juliean
Juliean
Asked: 2024-10-16 01:00:06 +0800 CST2024-10-16 01:00:06 +0800 CST 2024-10-16 01:00:06 +0800 CST

Verificações de nulos implícitos para chamadas de função

  • 772

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?

windows
  • 1 1 respostas
  • 45 Views

1 respostas

  • Voted
  1. Best Answer
    Peter Cordes
    2024-10-16T02:37:43+08:002024-10-16T02:37:43+08:00

    No entanto, seria potencialmente necessário ler a memória de[rcx]

    Isso é barato, a menos que falte no cache. E o chamado pagaria esse custo mais tarde de qualquer maneira, a menos que nunca toque em seu *this.

    Se isso acontecer, então o carregamento anterior foi basicamente uma pré-busca... a menos que o primeiro acesso "real" tenha sido uma gravação, caso em que poderíamos ter ido direto para o estado MESI Exclusive / Modified, sem primeiro obter uma cópia que está apenas no estado Shared de um acesso somente leitura, se vários threads acessarem esse objeto. Se nenhum outro núcleo possuía a linha de cache antes, um carregamento simples normalmente colocará a linha de cache no estado Exclusive, que pode fazer a transição para Modified sem outra transação off-core (um Read For Ownership = RFO).

    Se o chamado também fizer uma leitura como primeiro acesso, não há desvantagens em ler aqui.

    Com um objeto grande, se a função membro tocasse apenas em membros que estivessem em linhas de cache posteriores, tocar na primeira linha de cache poluiria o cache.


    Deixar um exec fora de ordem lidar com uma carga é ótimo, provavelmente mais barato do que um teste/ramificação assumindo que ele nunca realmente falha. Um teste/ramificação estaria sujeito a previsões erradas de ramificação. Como para cada instrução, o pipeline assume fortemente que as cargas não falharão, apenas fazendo algo se uma instrução com falha atingir a aposentadoria (tornar-se não especulativa).

    Mas os ramos são sempre previstos de uma forma ou de outra, e consomem recursos de previsão de ramos e podem criar alias para outros ramos, de modo que são previstos incorretamente, mesmo que sempre sejam fortemente rejeitados.

    • O que exatamente acontece quando uma CPU skylake prevê incorretamente uma ramificação?
    • Uma ramificação de CPU executada especulativamente pode conter opcodes que acessam a RAM? (minha resposta fala principalmente sobre buffers de armazenamento necessários para tornar possível a execução especulativa de armazenamentos, já que, diferentemente das cargas, eles modificariam o cache.)
    • Execução fora de ordem vs. execução especulativa - todas as instruções são tratadas como especulativas até serem desativadas.

    CPUs x86 modernas têm um ótimo throughput de porta de carga, como 2 ou 3 por ciclo de clock (com acertos de cache L1d para cargas naturalmente alinhadas) e números bastante generosos de buffers de carga para rastrear cargas pendentes. Por exemplo, Haswell de mais de 10 anos atrás ( https://www.realworldtech.com/haswell-cpu/5/ ) tem 72 entradas de buffer de carga para um ROB (ReOrder Buffer) de 192 entradas.

    Cargas em um registrador de 32 bits com mov eax, [rcx](2 bytes de código de máquina) ou movzx eax, byte ptr [rcx](3 bytes) para estruturas menores que 4 bytes são provavelmente sua melhor aposta, até mais barata do que a sugestão de @user555045 de testcom um imediato 0. O encaminhamento de armazenamento de 8 bytes para cargas de 4 bytes dos primeiros 8 bytes é eficiente em CPUs x86, mesmo com muitos anos de idade, e o tamanho do operando de 32 bits evita um prefixo REX.

    test [rcx], cleconomizaria tamanho de código e ainda não teria uma dependência falsa, mas movtambém evitaria um ALU uop para as unidades de execução de back-end. Deveria ser apenas 1 micro-op (uop) para o front-end e emitir/renomear estágios em qualquer CPU que faça qualquer tipo de microfusão. (Ou a AMD simplesmente decodificando-o como 1 uop em primeiro lugar).

    Ambas as principais convenções de chamada x86-64 têm pelo menos um reg puramente sobrecarregado por chamadas, no qual é sempre seguro carregar antes de uma chamada (por exemplo, EAX para Win x64, R11D para AMD64 SysV : funções variádicas usam AL para passar o número de argumentos XMM. Embora você possa simplesmente defini-lo como uma constante depois movno EAX, a menos que seja um shim/trampolim que passa argumentos variádicos para outra função).

    Escrever um GPR e/ou FLAGS é equivalente em termos de limites de arquivo de registro sobre o quão longe o exec fora de ordem pode ver à frente : uma entrada de arquivo de registro físico tem espaço para um inteiro de 64 bits mais um resultado FLAGS, então instruções como essas add rax, rcxpodem ser tratadas como se estivessem escrevendo apenas um resultado pela máquina exec fora de ordem.

    test cl, [rcx]ou test eax, [rcx]são apenas um pouco piores que mov eax, [rcx], então não se preocupe muito em usá-los se por algum motivo você não conseguir escolher facilmente um registro para escrever.

    • 2

relate perguntas

  • C++ ReadFile inteiramente, onde tamanho> 2 GB (Win64)

  • BSOD do Windows Palo Alto Cortex XDR com verificação de bug 0x139

  • Não consigo importar pacotes locais no meu projeto go

  • O KERNEL32.DLL é sempre o terceiro módulo carregado em um processo do Windows?

  • Qual é o problema neste código Rust inseguro para que funcione no Windows, mas não no Ubuntu?

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    Vue 3: Erro na criação "Identificador esperado, mas encontrado 'import'" [duplicado]

    • 1 respostas
  • Marko Smith

    Por que esse código Java simples e pequeno roda 30x mais rápido em todas as JVMs Graal, mas não em nenhuma JVM Oracle?

    • 1 respostas
  • Marko Smith

    Qual é o propósito de `enum class` com um tipo subjacente especificado, mas sem enumeradores?

    • 1 respostas
  • Marko Smith

    Como faço para corrigir um erro MODULE_NOT_FOUND para um módulo que não importei manualmente?

    • 6 respostas
  • Marko Smith

    `(expression, lvalue) = rvalue` é uma atribuição válida em C ou C++? Por que alguns compiladores aceitam/rejeitam isso?

    • 3 respostas
  • Marko Smith

    Quando devo usar um std::inplace_vector em vez de um std::vector?

    • 3 respostas
  • Marko Smith

    Um programa vazio que não faz nada em C++ precisa de um heap de 204 KB, mas não em C

    • 1 respostas
  • Marko Smith

    PowerBI atualmente quebrado com BigQuery: problema de driver Simba com atualização do Windows

    • 2 respostas
  • Marko Smith

    AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos

    • 1 respostas
  • Marko Smith

    Estou tentando fazer o jogo pacman usando apenas o módulo Turtle Random e Math

    • 1 respostas
  • Martin Hope
    Aleksandr Dubinsky Por que a correspondência de padrões com o switch no InetAddress falha com 'não cobre todos os valores de entrada possíveis'? 2024-12-23 06:56:21 +0800 CST
  • Martin Hope
    Phillip Borge Por que esse código Java simples e pequeno roda 30x mais rápido em todas as JVMs Graal, mas não em nenhuma JVM Oracle? 2024-12-12 20:46:46 +0800 CST
  • Martin Hope
    Oodini Qual é o propósito de `enum class` com um tipo subjacente especificado, mas sem enumeradores? 2024-12-12 06:27:11 +0800 CST
  • Martin Hope
    sleeptightAnsiC `(expression, lvalue) = rvalue` é uma atribuição válida em C ou C++? Por que alguns compiladores aceitam/rejeitam isso? 2024-11-09 07:18:53 +0800 CST
  • Martin Hope
    The Mad Gamer Quando devo usar um std::inplace_vector em vez de um std::vector? 2024-10-29 23:01:00 +0800 CST
  • Martin Hope
    Chad Feller O ponto e vírgula agora é opcional em condicionais bash com [[ .. ]] na versão 5.2? 2024-10-21 05:50:33 +0800 CST
  • Martin Hope
    Wrench Por que um traço duplo (--) faz com que esta cláusula MariaDB seja avaliada como verdadeira? 2024-05-05 13:37:20 +0800 CST
  • Martin Hope
    Waket Zheng Por que `dict(id=1, **{'id': 2})` às vezes gera `KeyError: 'id'` em vez de um TypeError? 2024-05-04 14:19:19 +0800 CST
  • Martin Hope
    user924 AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos 2024-03-20 03:12:31 +0800 CST
  • Martin Hope
    MarkB Por que o GCC gera código que executa condicionalmente uma implementação SIMD? 2024-02-17 06:17:14 +0800 CST

Hot tag

python javascript c++ c# java typescript sql reactjs html

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve