Eu estava trabalhando recentemente em um projeto pessoal quando me deparei com uma questão estranha.
Em um loop muito fechado, tenho um número inteiro com um valor entre 0 e 15. Preciso obter -1 para os valores 0, 1, 8 e 9 e 1 e para os valores 4, 5, 12 e 13.
Eu me virei para o godbolt para verificar algumas opções e fiquei surpreso ao parecer que o compilador não poderia otimizar uma declaração de switch da mesma maneira que uma cadeia if.
O link está aqui: https://godbolt.org/z/WYVBFl
O código é:
const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
int a(int num) {
return lookup[num & 0xF];
}
int b(int num) {
num &= 0xF;
if (num == 0 || num == 1 || num == 8 || num == 9)
return -1;
if (num == 4 || num == 5 || num == 12 || num == 13)
return 1;
return 0;
}
int c(int num) {
num &= 0xF;
switch (num) {
case 0: case 1: case 8: case 9:
return -1;
case 4: case 5: case 12: case 13:
return 1;
default:
return 0;
}
}
Eu pensaria que bec produziriam os mesmos resultados, e esperava poder ler os bit-hacks para obter uma implementação eficiente, já que minha solução (a instrução switch - de outra forma) era bastante lenta.
Estranhamente, b
compilado para hacks de bits, enquanto c
praticamente não era otimizado ou reduzido a um caso diferente de a
depender do hardware de destino.
Alguém pode explicar por que existe essa discrepância? Qual é a maneira 'correta' de otimizar esta consulta?
EDITAR:
Esclarecimento
Eu quero a solução chave para ser o mais rápido, ou uma solução semelhante "limpa". No entanto, quando compilado com otimizações na minha máquina, a solução if é significativamente mais rápida.
Eu escrevi um programa rápido para demonstrar e o TIO tem os mesmos resultados que encontro localmente: Experimente online!
Com static inline
a tabela de pesquisa acelera um pouco: Experimente online!
fonte
-O3
, e ele compilouc
para algo provavelmente pior quea
oub
(c
teve dois saltos condicionais mais algumas manipulações de bits, versus apenas um salto condicional e manipulação de bits mais simplesb
), mas ainda assim item melhor do que ingênuo por testes de itens. Não tenho certeza do que você realmente está pedindo aqui; o simples fato é que um compilador otimizador pode transformar qualquer um desses itens em qualquer um dos outros, se assim o desejar, e não há regras rígidas para o que ele fará ou não fará.if
ainda bateswitch
(estranhamente pesquisa torna-se ainda mais rápido) [TIO seguir]Respostas:
Se você enumerar explicitamente todos os casos, o gcc é muito eficiente:
é apenas compilado em um simples ramo indexado:
Observe que, se não
default:
for comentado, o gcc retornará à sua versão de ramificação aninhada.fonte
pslld
/psrad
ou seus equivalentes em AVX2 em 8 vias. Depende muito das outras particularidades do seu código.Os compiladores C têm casos especiais
switch
, porque esperam que os programadores entendam o idiomaswitch
e o explorem.Código como:
não passaria na revisão por codificadores C competentes; três ou quatro revisores simultaneamente exclamariam "isso deve ser um
switch
!"Não vale a pena que os compiladores C analisem a estrutura de
if
instruções para conversão em uma tabela de salto. As condições para isso precisam ser corretas e a quantidade de variação possível em váriasif
declarações é astronômica. A análise é ao mesmo tempo complicada e provavelmente negativa (como em: "não, não podemos converter essesif
s em aswitch
").fonte
if
se possível.static
e use os inicializadores designados C99 se quiser deixar um pouco mais claro o que você está atribuindo, e está claramente perfeitamente bem.if
(veja editar). @R .. Eu trabalhei com a solução bit a bit completa para o compilador, que é o que estou usando no momento. Infelizmente, no meu caso, esses sãoenum
valores, não números inteiros nus; portanto, os hacks bit a bit não são muito sustentáveis.O código a seguir calculará sua pesquisa sem ramificação, sem LUT, em ~ 3 ciclos de relógio, ~ 4 instruções úteis e ~ 13 bytes de
inline
código de máquina x86 altamente ativável.Depende da representação inteira do complemento de 2.
Você deve, no entanto, garantir que o
u32
es32
typedefs realmente aponte para tipos inteiros não assinados e assinados de 32 bits.stdint.h
tiposuint32_t
eint32_t
teria sido adequado, mas não tenho idéia se o cabeçalho está disponível para você.Veja você mesmo aqui: https://godbolt.org/z/AcJWWf
Na seleção da constante
Sua pesquisa é para 16 constantes muito pequenas entre -1 e +1, inclusive. Cada um se encaixa dentro de 2 bits e há 16 deles, que podemos apresentar da seguinte maneira:
Ao colocá-los com o índice 0 mais próximo do bit mais significativo, um único deslocamento de
2*num
colocará o bit de sinal do seu número de 2 bits no bit de sinal do registrador. Mudar para a direita o número de 2 bits por 32-2 = sinal de 30 bits o estende ao máximoint
, completando o truque.fonte
magic
comentário explicando como regenerá-la. Você poderia explicar como surgiu?!!(12336 & (1<<x))-!!(771 & (1<<x));
Você pode criar o mesmo efeito usando apenas aritmética:
Mesmo assim, tecnicamente, essa ainda é uma pesquisa (bit a bit).
Se o exposto acima parecer muito misterioso, você também pode:
fonte