Design: Documente porque os bits NaN não são totalmente determinísticos

Criado em 22 mar. 2016  ·  15Comentários  ·  Fonte: WebAssembly/design

(Não estou defendendo no momento; isso é para que tomemos uma decisão informada e coletamos material para uma justificativa.)

Olhando para Nondeterminism.md , as partes dos bits NaN se destacam. É claro que os threads podem disparar, os recursos podem se esgotar e os recursos podem ser adicionados; essas são consequências do design geral. Mas bits NaN? As VMs poderiam ser um pouco mais inteligentes e cuidar disso? Existem dois problemas:

Quando uma operação obtém mais de um operando NaN, qual ela propaga?

O IEEE 754 não especifica isso, mas pelo menos x86, ARM e Power selecionam o "primeiro" operando e é uma escolha razoável.

No entanto, corrigir a escolha no nível do wasm significaria que as implementações do wasm não podem comutar adições e multiplicações de ponto flutuante, o que às vezes é uma otimização útil no pré-VEX x86, onde as instruções sobrescrevem uma de suas entradas. Além disso, exigiria que qualquer um usando uma VM baseada em LLVM ensinasse que somar e multiplicar não são comutativos em wasm.

Pode-se argumentar que isso não é um obstáculo, então esse problema é teoricamente solucionável se houver um forte desejo.

Quando uma operação produz um NaN e não possui operandos NaN, qual é o bit de sinal?

x86 usa 1, ARM usa 0.

A maneira mais simples de corrigir isso seria canonizar após cada operação de ponto flutuante. Isso é factível, embora o problema seja que 0 e 1 são valores válidos possíveis quando há um operando NaN para propagar, portanto, não seria suficiente apenas verificar se há um resultado NaN e canonizar; seria necessário verificar se há um resultado NaN e se há falta de operandos NaN, e só então canonizar.

A canonização após _toda_ operação seria muito cara; outra opção é apenas canonizar em pontos de "escape" de cálculos (o caminho de canonização teria que reproduzir essencialmente um fluxo inteiro de computação para determinar a saída NaN correta). Isso é uma melhoria, mas provavelmente ainda adiciona uma quantidade significativa de sobrecarga.

Outra implementação possível seria desmascarar a exceção inválida, pegar uma armadilha sempre que um NaN for gerado e, em seguida, executar a canonização e o retorno. As desvantagens incluem usar um modo de CPU não padrão e ser muito lento se houver muitos inválidos sendo gerados.

Infelizmente, essas abordagens trazem desvantagens significativas. A menos que surjam outras ideias ou haja um desejo muito forte, isso parece difícil de corrigir.


Outra coisa a observar é que os bits NaN são difíceis de observar acidentalmente, portanto, essa não é uma grande preocupação de portabilidade em geral.

Alguém mais tem alguma ideia a acrescentar?

clarification floating point

Comentários muito úteis

Gostaria de quantificar os efeitos de desempenho disso antes de tomar uma decisão. Acho que nossos conjuntos de ferramentas ainda são muito imaturos para fazer boas medições de desempenho no momento (há postes mais longos no caminho deste). Dito de outra forma: eu quero evitar "a morte por mil cortes".

Todos 15 comentários

Outra coisa a notar é que os bits NaN são difíceis de observar acidentalmente

Existe alguma maneira de torná-los impossíveis de serem observados ou pelo menos a parte do sinal?

IMO, os não determinismos NaN devem ser deixados de lado, visto que é muito raro o software se preocupar com eles. Qual é o caso de sacrificar o desempenho para torná-los determinísticos?

Aqui está uma lista das maneiras pelas quais podemos observar os bits NaN:

  • reinterpret conversão
  • store para a memória linear e carregue os bits com uma interpretação diferente (ou deixe os bits serem observados externamente)
  • passe um argumento para call de uma função importada ou retorne um valor de uma função exportada
  • copysign o bit de sinal em um não-NaN

As VMs podem inserir o código de canonização antes de cada um deles; essa é a ideia de "canonizar nos pontos de escape" discutida acima. store é muito comum em caminhos ativos, então provavelmente ainda seria bastante caro.

@qwertie Eu me aqui, existem maneiras de torná-lo totalmente determinante, mesmo que não entre nas especificações.

Os benefícios do

store é muito comum em caminhos ativos, então provavelmente ainda seria bastante caro.

Neste caso, poderíamos apenas sempre especificar o valor do sinal? então apenas torná-lo 1 se for colocado em mem?

@wanderer Isso é essencialmente o que a canonização envolve: verifique se o valor é um NaN e, em caso afirmativo, aplique alguma correção. Normalmente é uma comparação extra e ramificação no caminho ativo.

Devo também acrescentar que não comparei nenhuma das opções mencionadas nesta edição; qualquer benchmarking que alguém possa adicionar aqui seria bem-vindo.

Eu me inclinaria fortemente para o nível atual de não determinismo, que favorece o desempenho.
Na verdade, fiquei um tanto surpreso que wasm define rigorosamente os bits de mantissa do NaN.
Embora isso possa ser suficiente para os processadores de hoje, quem sabe quais futuros processadores podem vir.

Se o wasm estiver sendo gerado a partir de uma linguagem de alto nível, esse tradutor pode fornecer uma opção para controlar o nível de semântica FP, semelhante a -ffast-math, por exemplo. O tradutor poderia então inserir qualquer correção de wasm extra necessária para forçar os nans a um formato desejado. Para esse fim, poderíamos fornecer os operadores isNan ou normalizeNan, embora não esteja defendendo isso agora.

Resumindo, é melhor "consertar" isso para uma ferramenta de nível superior, a IMO.

+1 @mbodart. Edit : oh, olhe, há na verdade uma coisa +1 agora :). Aliás, eu pessoalmente acho que Wasm = dominação mundial e que, portanto, os futuros processadores não irão antagonizá-lo.

Gostaria de quantificar os efeitos de desempenho disso antes de tomar uma decisão. Acho que nossos conjuntos de ferramentas ainda são muito imaturos para fazer boas medições de desempenho no momento (há postes mais longos no caminho deste). Dito de outra forma: eu quero evitar "a morte por mil cortes".

Eu concordo com @jfbastien. Os efeitos de desempenho precisam ser quantificados primeiro, antes de mexer na semântica.

Para ser claro, atualmente não estou defendendo uma mudança aqui; Estou coletando material de justificativa. O não-determinismo do bit NaN se destaca e quer explicação. E, até onde sei, ninguém quantificou os efeitos de desempenho de tornar esses bits não determinísticos.

ARMv8 nem sempre propaga o primeiro operando quando ambos os operandos são NaN. A regra é:

  • Se um operando for um NaN silencioso e o outro for um NaN de sinalização, propague o NaN de sinalização.
  • Caso contrário, propague o primeiro operando.

Enquanto estou lendo as especificações, isso se aplica aos modos Aarch32 e Aarch64 do ARMv8.

Este comportamento é diferente do SSE que propaga o primeiro operando em ambos os casos.

Ambas as arquiteturas converterão um sNaN em um qNaN definindo o bit silencioso antes de propagá-lo.

@stoklund Good spot! Eu perdi que o ARM escolhendo o primeiro NaN não acontece no caso em que o segundo NaN está sinalizando. Isso complicaria a estratégia que estabeleci para o caso NaN múltiplo acima, portanto, podemos mencionar isso na justificativa.

Agora criei # 973 para propor um texto específico resumindo o acima.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

frehberg picture frehberg  ·  6Comentários

thysultan picture thysultan  ·  4Comentários

konsoletyper picture konsoletyper  ·  6Comentários

bobOnGitHub picture bobOnGitHub  ·  6Comentários

spidoche picture spidoche  ·  4Comentários