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.