Posso sugerir o otimizador, fornecendo o intervalo de um número inteiro?

173

Estou usando um inttipo para armazenar um valor. Pela semântica do programa, o valor sempre varia em um intervalo muito pequeno (0 - 36) e int(não a char) é usado apenas devido à eficiência da CPU.

Parece que muitas otimizações aritméticas especiais podem ser executadas em um intervalo tão pequeno de números inteiros. Muitas chamadas de função nesses números inteiros podem ser otimizadas em um pequeno conjunto de operações "mágicas", e algumas funções podem até ser otimizadas em consultas de tabela.

Portanto, é possível dizer ao compilador que isso intestá sempre nesse pequeno intervalo e é possível que o compilador faça essas otimizações?

rolevax
fonte
4
otimizações de faixa de valor existem em muitos compiladores, por exemplo. llvm, mas não conheço nenhuma dica de idioma para declará-la.
Remus Rusanu
2
Observe que, se você nunca tiver números negativos, poderá obter pequenos ganhos no uso de unsignedtipos, pois são mais fáceis de raciocinar com o compilador.
user694733
4
@RemusRusanu: Pascal permite definir tipos de subfaixas , por exemplo var value: 0..36;.
Edgar Bonet #
7
" int (não um caractere) é usado apenas porque a eficiência da CPU. " Esse velho pedaço de sabedoria convencional geralmente não é muito verdadeiro. Às vezes, tipos estreitos precisam ser estendidos de zero ou sinal para toda a largura do registro, esp. quando usado como índices de matriz, mas às vezes isso acontece de graça. Se você tiver uma matriz desse tipo, a redução no espaço ocupado pelo cache geralmente supera qualquer outra coisa.
Peter Cordes
1
Esqueceu-se de dizer: inte também unsigned intprecisa ser estendido por zero ou zero de 32 para 64 bits, na maioria dos sistemas com ponteiros de 64 bits. Observe que no x86-64, as operações nos registradores de 32 bits estendem de zero a 64 bits gratuitamente (sem estender sinal, mas o estouro assinado é um comportamento indefinido, portanto, o compilador pode usar apenas matemática assinada de 64 bits, se desejar). Portanto, você vê apenas instruções extras para estender zero argumentos de função de 32 bits, não resultados da computação. Você faria para tipos não assinados mais estreitos.
27568 Peter

Respostas:

230

Sim, é possível. Por exemplo, gccvocê pode usar __builtin_unreachablepara informar o compilador sobre condições impossíveis, como:

if (value < 0 || value > 36) __builtin_unreachable();

Podemos quebrar a condição acima em uma macro:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

E use-o assim:

assume(x >= 0 && x <= 10);

Como você pode ver , gccrealiza otimizações com base nessas informações:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

Produz:

func(int):
    mov     eax, 17
    ret

Uma desvantagem, no entanto, é que, se seu código quebrar essas suposições, você terá um comportamento indefinido .

Ele não o notifica quando isso acontece, mesmo nas versões de depuração. Para depurar / testar / capturar bugs com suposições mais facilmente, você pode usar uma macro híbrida de assumir / afirmar (créditos para @David Z), como esta:

#if defined(NDEBUG)
#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)
#else
#include <cassert>
#define assume(cond) assert(cond)
#endif

Nas compilações de depuração (com NDEBUG não definidas), ele funciona como uma assertmensagem de erro comum e abortprograma de impressão, e nas compilações de versão utiliza uma suposição, produzindo código otimizado.

Observe, no entanto, que ele não substitui os permanentes regulares assert- condnas versões de lançamento, portanto você não deve fazer algo assim assume(VeryExpensiveComputation()).

Peter Mortensen
fonte
5
@ Xofo, não entendi, no meu exemplo isso já está acontecendo, pois o return 2ramo foi eliminado do código pelo compilador.
6
No entanto, parece que o gcc não pode otimizar funções para operações mágicas ou pesquisa de tabela conforme o OP esperado.
precisa saber é o seguinte
19
@ user3528438, __builtin_expecté uma dica não estrita. __builtin_expect(e, c)deve ler como " eprovavelmente avaliará c" e pode ser útil para otimizar a previsão de ramificação, mas não se restringe ea sempre c, portanto, não permite que o otimizador jogue fora outros casos. Veja como as ramificações são organizadas em montagem .
6
Em teoria, qualquer código que cause incondicionalmente um comportamento indefinido pode ser usado no lugar de __builtin_unreachable().
CodesInChaos
14
A menos que haja alguma peculiaridade que eu não conheça e que faça disso uma péssima idéia, pode fazer sentido combinar isso com assert, por exemplo, definir assumecomo assertquando NDEBUGnão está definido e como __builtin_unreachable()quando NDEBUGestá definido. Dessa forma, você obtém o benefício da suposição no código de produção, mas em uma compilação de depuração, você ainda tem uma verificação explícita. É claro que você precisa fazer testes suficientes para garantir que a suposição será satisfeita na natureza.
David Z
61

Existe suporte padrão para isso. O que você deve fazer é incluir stdint.h( cstdint) e, em seguida, usar o tipo uint_fast8_t.

Isso informa ao compilador que você está usando apenas números entre 0 e 255, mas que é livre usar um tipo maior se isso fornecer um código mais rápido. Da mesma forma, o compilador pode assumir que a variável nunca terá um valor acima de 255 e, em seguida, fará otimizações de acordo.

Lundin
fonte
2
Esses tipos não são usados ​​quase tanto quanto deveriam (eu pessoalmente esqueço que eles existem). Eles fornecem código rápido e portátil, bastante brilhante. E eles estão por aí desde 1999.
Lundin 07/11
Essa é uma boa sugestão para o caso geral. A resposta de deniss mostra uma solução mais maleável para cenários específicos.
Lightness Races em órbita
1
O compilador apenas obtém as informações do intervalo de 0 a 255 em sistemas onde uint_fast8_té realmente um tipo de 8 bits (por exemplo unsigned char) como em x86 / ARM / MIPS / PPC ( godbolt.org/g/KNyc31 ). No início do DEC Alpha antes de 21164A , as cargas / lojas de bytes não eram suportadas, portanto qualquer implementação sã seria usada typedef uint32_t uint_fast8_t. AFAIK, não há mecanismo para um tipo ter limites de alcance extras com a maioria dos compiladores (como o gcc), por isso tenho certeza de uint_fast8_tque eles se comportariam exatamente da mesma forma unsigned intou em qualquer outro caso.
Peter Cordes
( boolé especial e tem intervalo limitado a 0 ou 1, mas é um tipo interno, não definido pelos arquivos de cabeçalho em termos de char, no gcc / clang. Como eu disse, não acho que a maioria dos compiladores tenha um mecanismo que tornaria isso possível).
Peter Cordes
1
Enfim, uint_fast8_té uma boa recomendação, pois ele usará um tipo de 8 bits em plataformas onde é tão eficiente quanto unsigned int. (Eu estou realmente não sei o que os fasttipos devem ser rápido para , e se o cache pegada tradeoff é suposto ser parte dela.). O x86 possui amplo suporte para operações de bytes, mesmo para adicionar bytes com uma fonte de memória, para que você nem precise fazer uma carga separada de extensão zero (o que também é muito barato). O gcc cria uint_fast16_tum tipo de 64 bits no x86, o que é insano para a maioria dos usos (vs. 32 bits). godbolt.org/g/Rmq5bv .
27516 Peter
8

A resposta atual é boa para o caso quando você sabe ao certo qual é o intervalo, mas se ainda deseja um comportamento correto quando o valor está fora do intervalo esperado, ele não funcionará.

Nesse caso, achei que essa técnica pode funcionar:

if (x == c)  // assume c is a constant
{
    foo(x);
}
else
{
    foo(x);
}

A idéia é uma troca de dados de código: você está movendo 1 bit de dados (se x == c) para a lógica de controle .
Isso sugere que o otimizador xé de fato uma constante conhecida c, incentivando-o a alinhar e otimizar a primeira chamada de fooseparadamente do resto, possivelmente muito.

fooEntretanto, certifique-se de fatorar o código em uma única sub-rotina - não duplique o código.

Exemplo:

Para que essa técnica funcione, você precisa ter um pouco de sorte - há casos em que o compilador decide não avaliar as coisas estaticamente, e elas são arbitrárias. Mas quando funciona, funciona bem:

#include <math.h>
#include <stdio.h>

unsigned foo(unsigned x)
{
    return x * (x + 1);
}

unsigned bar(unsigned x) { return foo(x + 1) + foo(2 * x); }

int main()
{
    unsigned x;
    scanf("%u", &x);
    unsigned r;
    if (x == 1)
    {
        r = bar(bar(x));
    }
    else if (x == 0)
    {
        r = bar(bar(x));
    }
    else
    {
        r = bar(x + 1);
    }
    printf("%#x\n", r);
}

Basta usar -O3e observar as constantes pré-avaliadas 0x20e 0x30ena saída do assembler .

user541686
fonte
Você não iria querer if (x==c) foo(c) else foo(x)? Se apenas para pegar constexprimplementações de foo?
MSalters
@MSalters: Eu sabia que alguém ia perguntar isso !! Eu vim com essa técnica antes, constexprera uma coisa e nunca me preocupei em atualizá-la depois (embora eu nunca tenha me preocupado em me preocupar com isso constexprdepois), mas a razão pela qual eu não fiz isso inicialmente foi porque eu queria facilite para o compilador classificá-los como código comum e remova a ramificação se ele decidir deixá-los como chamadas de método normais e não otimizar. Eu esperava que, se eu colocasse c, é realmente difícil para o compilador c (desculpe, piada de mau gosto) que os dois sejam do mesmo código, embora eu nunca tenha verificado isso.
user541686
4

Estou apenas dizendo que, se você quiser uma solução mais C ++ padrão, poderá usar o [[noreturn]]atributo para escrever sua própria unreachable.

Então, refiz o excelente exemplo de deniss para demonstrar:

namespace detail {
    [[noreturn]] void unreachable(){}
}

#define assume(cond) do { if (!(cond)) detail::unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

O que, como você pode ver , resulta em código quase idêntico:

detail::unreachable():
        rep ret
func(int):
        movl    $17, %eax
        ret

A desvantagem é, obviamente, que você recebe um aviso de que uma [[noreturn]]função realmente retorna.

Contador de histórias - Unslander Monica
fonte
Funciona com clang, quando minha solução original não funciona , truque tão bom e +1. Mas tudo depende muito do compilador (como Peter Cordes nos mostrou, iccpois pode piorar o desempenho), portanto ainda não é universalmente aplicável. Além disso, observação secundária: a unreachabledefinição deve estar disponível para o otimizador e incorporada para que isso funcione .