c++ - resumo - rpubs python




Como forçar o GCC a assumir que uma expressão de ponto flutuante não é negativa? (3)

Após cerca de uma semana, perguntei sobre o GCC Bugzilla e eles forneceram uma solução mais próxima do que eu tinha em mente

float test (float x)
{
    float y = x*x;
    if (std::isless(y, 0.f))
        __builtin_unreachable();
    return std::sqrt(y);
}

que é compiles no seguinte assembly:

test(float):
    mulss   xmm0, xmm0
    sqrtss  xmm0, xmm0
    ret

Ainda não tenho muita certeza do que exatamente acontece aqui.

Há casos em que você sabe que uma determinada expressão de ponto flutuante sempre será não negativa. Por exemplo, ao calcular o comprimento de um vetor, faz-se sqrt(a[0]*a[0] + ... + a[N-1]*a[N-1]) (NB: Estou ciente de std::hypot , isso não é relevante para a questão), e a expressão sob a raiz quadrada é claramente não negativa. No entanto, o GCC outputs o seguinte assembly para sqrt(x*x) :

        mulss   xmm0, xmm0
        pxor    xmm1, xmm1
        ucomiss xmm1, xmm0
        ja      .L10
        sqrtss  xmm0, xmm0
        ret
.L10:
        jmp     sqrtf

Ou seja, ele compara o resultado de x*x a zero e, se o resultado não for negativo, ele executa a instrução sqrtss , caso contrário, chama sqrtf .

Então, minha pergunta é: como forçar o GCC a assumir que x*x sempre é negativo, para que ignore a comparação e a chamada sqrtf , sem escrever montagem embutida?

Desejo enfatizar que estou interessado em uma solução local e não fazer coisas como -ffast-math , -fno-math-errno ou -ffinite-math-only (embora elas realmente resolvam o problema, graças a ks1322, harold e Eric Postpischil nos comentários).

Além disso, "forçar o GCC a assumir que x*x não é negativo" deve ser interpretado como assert(x*x >= 0.f) ; portanto, isso também exclui o caso de x*x ser NaN.

Estou bem com soluções específicas do compilador, específicas da plataforma, específicas da CPU, etc.


Passe a opção -fno-math-errno para o gcc. Isso corrige o problema sem tornar seu código não portável ou sair do âmbito da ISO / IEC 9899: 2011 (C11).

O que essa opção faz não está tentando definir errno quando uma função da biblioteca de matemática falha:

       -fno-math-errno
           Do not set "errno" after calling math functions that are executed
           with a single instruction, e.g., "sqrt".  A program that relies on
           IEEE exceptions for math error handling may want to use this flag
           for speed while maintaining IEEE arithmetic compatibility.

           This option is not turned on by any -O option since it can result
           in incorrect output for programs that depend on an exact
           implementation of IEEE or ISO rules/specifications for math
           functions. It may, however, yield faster code for programs that do
           not require the guarantees of these specifications.

           The default is -fmath-errno.

           On Darwin systems, the math library never sets "errno".  There is
           therefore no reason for the compiler to consider the possibility
           that it might, and -fno-math-errno is the default.

Dado que você não parece estar particularmente interessado em rotinas matemáticas definindo errno , esta parece ser uma boa solução.


Você pode escrever assert(x*x >= 0.f) como uma promessa em tempo de compilação, em vez de uma verificação de tempo de execução, como segue no GNU C:

#include <cmath>

float test1 (float x)
{
    float tmp = x*x;
    if (!(tmp >= 0.0f)) 
        __builtin_unreachable();    
    return std::sqrt(tmp);
}

(relacionado: Quais otimizações o __builtin_unreachable facilita? Você também pode if(!x)__builtin_unreachable() em uma macro e chamá-lo de promise() ou algo assim.)

Mas o gcc não sabe como tirar proveito dessa promessa de que tmp não é NaN e não é negativo. Ainda obtemos ( Godbolt ) a mesma sequência ASM enlatada que verifica x>=0 e chama o sqrtf para definir errno . Presumivelmente, a expansão para uma comparação e ramificação ocorre após outras aprovações de otimização, portanto, não ajuda o compilador a saber mais.

Essa é uma otimização perdida na lógica que especula especifica o sqrt quando -fmath-errno está ativado (ativado por padrão, infelizmente).

O que você deseja é -fno-math-errno , que é seguro globalmente

Isso é 100% seguro se você não confiar nas funções matemáticas que definem errno . Ninguém quer isso, é para isso que servem a propagação de NaN e / ou sinalizadores que registram exceções de FP mascaradas. por exemplo, acesso fenv C99 / C ++ 11 via #pragma STDC FENV_ACCESS ON e, em seguida, funciona como fetestexcept() . Veja o exemplo em feclearexcept que mostra como usá-lo para detectar a divisão por zero.

O ambiente FP faz parte do contexto do encadeamento, enquanto errno é global.

O suporte para esse recurso obsoleto não é gratuito; você deve desativá-lo, a menos que tenha um código antigo que foi escrito para usá-lo. Não use em novo código: use fenv . O suporte ideal para -fmath-errno seria o mais barato possível, mas a raridade de quem realmente usa __builtin_unreachable() ou outras coisas para descartar uma entrada NaN presumivelmente não fez valer o tempo do desenvolvedor para implementar a otimização. Ainda assim, você pode relatar um erro de otimização perdida, se desejar.

Na verdade, o hardware da FPU do mundo real possui esses sinalizadores fixos que permanecem definidos até serem limpos, por exemplo, o mxcsr status / controle mxcsr do x86 para matemática SSE / AVX ou FPUs de hardware em outros ISAs. No hardware em que a FPU pode detectar exceções, uma implementação de C ++ de qualidade oferecerá suporte a coisas como fetestexcept() . E se não, então mathrrno provavelmente também não funciona.

errno para matemática era um projeto obsoleto antigo com o qual o C / C ++ ainda está preso por padrão e agora é amplamente considerado uma má idéia. Isso torna mais difícil para os compiladores incorporar funções matemáticas eficientemente. Ou talvez não estejamos tão presos quanto pensei: por que errno não está definido como EDOM, mesmo o sqrt retira argumentos de domínio? explica que a definição de errno nas funções matemáticas é opcional na ISO C11, e uma implementação pode indicar se o fazem ou não. Presumivelmente em C ++ também.

É um grande erro -fno-math-errno com otimizações de alteração de valor, como -ffast-math ou -ffinite-math-only . Você deve considerar ativá-lo globalmente, ou pelo menos para todo o arquivo que contém essa função.

float test2 (float x)
{
    return std::sqrt(x*x);
}
# g++ -fno-math-errno -std=gnu++17 -O3
test2(float):   # and test1 is the same
        mulss   xmm0, xmm0
        sqrtss  xmm0, xmm0
        ret

Você também pode usar -fno-trapping-math , se nunca desmascarar nenhuma exceção de FP com feenableexcept() . (Embora essa opção não seja necessária para essa otimização, é apenas a porcaria de definir erros que é um problema aqui.).

-fno-trapping-math não assume nenhum NaN ou qualquer coisa, apenas pressupõe que exceções de FP como Invalid ou Inexact nunca realmente invocam um manipulador de sinal em vez de produzir NaN ou um resultado arredondado. -ftrapping-math é o padrão, mas está quebrado e "nunca funcionou", de acordo com Marc Glisse, desenvolvedor do GCC . (Mesmo assim, o GCC faz algumas otimizações que podem alterar o número de exceções que seriam aumentadas de zero para diferente de zero ou vice-versa. E bloqueia algumas otimizações seguras). Mas, infelizmente, https://gcc.gnu.org/bugzilla/show_bug.cgi?id=54192 ( https://gcc.gnu.org/bugzilla/show_bug.cgi?id=54192 -lo por padrão) ainda está aberto.

Se você realmente desmascarou exceções, talvez seja melhor -ftrapping-math , mas, novamente, é muito raro que você deseje isso em vez de apenas verificar sinalizadores após algumas operações matemáticas ou verificar o NaN. E, de fato, não preserva a semântica exata das exceções.

Consulte SIMD para operação de limite de flutuação para um caso em que -fno-trapping-math bloqueia incorretamente uma otimização segura. (Mesmo depois de içar uma operação potencialmente interceptadora para que o C o faça incondicionalmente, o gcc cria um asm não vetorizado que o faz condicionalmente! Portanto, não apenas bloqueia a vetorização, mas também altera a semântica da exceção em relação à máquina abstrata do C.)







micro-optimization