Qual é a sua técnica bit-wise favorita? [fechadas]


14

Alguns dias atrás, o membro do StackExchange Anto perguntou sobre usos válidos para operadores em bits. Afirmei que o deslocamento era mais rápido do que multiplicar e dividir números inteiros por potências de dois. O membro do StackExchange Daemin rebateu afirmando que a mudança à direita apresentava problemas com números negativos.

Naquele momento, eu nunca havia pensado em usar os operadores shift com números inteiros assinados. Eu usei essa técnica principalmente no desenvolvimento de software de baixo nível; portanto, sempre usei números inteiros não assinados. C executa mudanças lógicas em números inteiros não assinados. Nenhuma atenção é dada ao bit de sinal ao executar uma mudança lógica à direita. Os bits desocupados são preenchidos com zeros. No entanto, C executa uma operação de deslocamento aritmético ao deslocar um número inteiro assinado para a direita. Os bits desocupados são preenchidos com o bit de sinal. Essa diferença faz com que um valor negativo seja arredondado para o infinito, em vez de ser truncado para zero, que é um comportamento diferente da divisão inteira assinada.

Alguns minutos de reflexão resultaram em uma solução de primeira ordem. A solução converte condicionalmente valores negativos em valores positivos antes de mudar. Um valor é convertido condicionalmente de volta à sua forma negativa após a execução da operação de mudança.

int a = -5;
int n = 1;

int negative = q < 0; 

a = negative ? -a : a; 
a >>= n; 
a = negative ? -a : a; 

O problema com esta solução é que as instruções de atribuição condicional geralmente são convertidas para pelo menos uma instrução de salto, e as instruções de salto podem ser caras em processadores que não decodificam os dois caminhos de instrução. Ter que refazer o pipeline de instruções duas vezes prejudica qualquer ganho de desempenho obtido pela troca de divisão.

Com o exposto, acordei no sábado com a resposta para o problema de atribuição condicional. O problema de arredondamento que experimentamos ao executar uma operação de mudança aritmética ocorre apenas ao trabalhar com a representação de complemento de dois. Isso não ocorre com a representação de complemento de alguém. A solução para o problema envolve a conversão do valor do complemento de dois para o valor de complemento de alguém antes de executar a operação de turno. Em seguida, temos que converter o valor do complemento de uma pessoa em um valor de complemento de dois. Surpreendentemente, podemos executar esse conjunto de operações sem converter valores negativos condicionalmente antes de executar a operação de mudança.

int a = -5;
int n = 1;

register int sign = (a >> INT_SIZE_MINUS_1) & 1

a = (a - sign) >> n + sign;   

O valor negativo do complemento de dois é convertido no valor negativo do complemento de alguém subtraindo um. Por outro lado, o valor negativo do complemento de uma pessoa é convertido no valor negativo do complemento de dois adicionando um. O código listado acima funciona porque o bit de sinal é usado para converter o complemento de dois em complemento de um e vice-versa . Somente valores negativos terão seus bits de sinal definidos; portanto, o sinal da variável será igual a zero quando a for positivo.

Com o exposto acima, você pode pensar em outros hacks pouco inteligentes, como o descrito acima, que fizeram parte da sua mochila de truques? Qual é o seu truque bit-wise favorito? Estou sempre procurando por novos hacks bit a bit orientados para o desempenho.


3
Esta pergunta e o nome da sua conta - o mundo faz sentido novamente ...
JK

+1 Pergunta interessante como um acompanhamento para o meu e de outra forma bem;)
Anto

Também fiz alguns cálculos rápidos de paridade uma vez. Paridade é um pouco dolorosa, porque tradicionalmente envolve loops e contando se um pouco é definido, o que exige muitos saltos. A paridade pode ser calculada usando shift e XOR; em seguida, um monte daqueles executados um após o outro evita os loops e saltos.
quickly_now

2
Você está ciente de que há um livro inteiro sobre essas técnicas? O que outras pessoas estão dizendoDiscover
share the most beautiful images from around the

Sim, também há um site dedicado a operações de bits. Esqueço o URL, mas o Google aumentará em breve.
quickly_now

Respostas:


23

Eu amo o hack de Gosper (HAKMEM # 175), uma maneira muito esperta de pegar um número e obter o próximo número com o mesmo número de bits definido. É útil, por exemplo, na geração de combinações de kitens a partir de n:

int set = (1 << k) - 1;
int limit = (1 << n);
while (set < limit) {
    doStuff(set);

    // Gosper's hack:
    int c = set & -set;
    int r = set + c;
    set = (((r^set) >>> 2) / c) | r;
}

7
+1. Mas, a partir de agora, terei pesadelos em encontrar este durante uma sessão de depuração sem o comentário.
Nikie

@nikie, muahahahaha! (Eu costumo usar isso para problemas do Project Euler - meu trabalho diário não envolve muita combinação).
Peter

7

O método Raiz quadrada inversa rápida usa as técnicas de nível de bits mais bizarras para calcular o inverso de uma raiz quadrada que eu já vi:

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = * ( long * ) &y;                       // evil floating point bit level hacking [sic]
    i  = 0x5f3759df - ( i >> 1 );               // what the fuck? [sic]
    y  = * ( float * ) &i;
    y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
    //    y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

    return y;
}

O sqrt rápido também é incrível. Carmack parece ser um dos maiores codificadores.
benjaminb

A Wikipedia possui fontes ainda mais antigas, por exemplo, beyond3d.com/content/articles/15
MSalters

0

Divisão por 3 - sem recorrer a uma chamada de biblioteca em tempo de execução.

Acontece que a divisão por 3 (graças a uma dica sobre Stack Overflow) pode ser aproximada como:

X / 3 = [(x / 4) + (x / 12)]

E X / 12 é (x / 4) / 3. Há um elemento de recursão aparecendo subitamente aqui.

Acontece também que, se você limitar o intervalo dos números em que está reproduzindo, poderá limitar o número de iterações necessárias.

E assim, para números inteiros não assinados <2000, o seguinte é um algoritmo / 3 rápido e simples. (Para números maiores, basta adicionar mais etapas). Os compiladores otimizam tudo isso para que ele seja rápido e pequeno:

FastDivide3 curto não assinado estático (argumento curto não assinado const)
{
  RunningSum curto não assinado;
  FractionalTwelth curto não assinado;

  RunningSum = arg >> 2;

  FractionalTwelth = RunningSum >> 2;
  RunningSum + = FractionalTwelth;

  FractionalTwelth >> = 2;
  RunningSum + = FractionalTwelth;

  FractionalTwelth >> = 2;
  RunningSum + = FractionalTwelth;

  FractionalTwelth >> = 2;
  RunningSum + = FractionalTwelth;

  // Mais repetições das 2 linhas acima para mais precisão

  retornar RunningSum;
}

1
Obviamente, isso é relevante apenas em microcontroladores muito obscuros. Qualquer CPU real fabricada nas últimas duas décadas não precisa de uma biblioteca de tempo de execução para divisão inteira.
MSalters

1
Ah, claro, mas pequenos micros sem um multiplicador de hardware são realmente muito comuns. E se você trabalha em terrenos incorporados e deseja economizar US $ 0,10 em cada um milhão de produtos vendidos, é melhor conhecer alguns truques sujos. Esse dinheiro economizado = lucro extra que deixa seu chefe muito feliz.
quickly_now

Bem, sujo ... está apenas multiplicando por .0101010101(aprox. 1/3). Ponta Pro: você também pode multiplicar por .000100010001e 101(que leva apenas 3 bitshifts, ainda tem a melhor aproximação.010101010101
MSalters

Como posso fazer isso apenas com números inteiros e sem ponto flutuante?
quickly_now

1
Bit a bit, x * 101 = x + x << 2. Do mesmo modo, X * é ,000100010001 x >> 4 + x >> 8 + x >> 12.
MSalters

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.