Aqui está um exemplo do mundo real: o ponto fixo se multiplica nos compiladores antigos.
Eles não são úteis apenas em dispositivos sem ponto flutuante, eles brilham quando se trata de precisão, pois oferecem 32 bits de precisão com um erro previsível (o float tem apenas 23 bits e é mais difícil prever a perda de precisão). isto é, precisão absoluta uniforme em toda a faixa, em vez de precisão relativa quase uniforme ( float
).
Os compiladores modernos otimizam esse exemplo de ponto fixo, portanto, para exemplos mais modernos que ainda precisam de código específico do compilador, consulte
C não possui um operador de multiplicação completa (resultado de 2N bits de entradas de N bits). A maneira usual de expressá-lo em C é converter as entradas para o tipo mais amplo e esperar que o compilador reconheça que os bits superiores das entradas não são interessantes:
// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
long long a_long = a; // cast to 64 bit.
long long product = a_long * b; // perform multiplication
return (int) (product >> 16); // shift by the fixed point bias
}
O problema com esse código é que fazemos algo que não pode ser expresso diretamente na linguagem C. Queremos multiplicar dois números de 32 bits e obter um resultado de 64 bits, dos quais retornamos os 32 bits do meio. No entanto, em C essa multiplicação não existe. Tudo o que você pode fazer é promover os números inteiros para 64 bits e fazer uma multiplicação de 64 * 64 = 64.
x86 (e ARM, MIPS e outros) podem, no entanto, fazer a multiplicação em uma única instrução. Alguns compiladores costumavam ignorar esse fato e gerar código que chama uma função de biblioteca de tempo de execução para fazer a multiplicação. A mudança de 16 também é frequentemente feita por uma rotina de biblioteca (também o x86 pode fazer essas mudanças).
Portanto, temos uma ou duas chamadas de biblioteca apenas para uma multiplicação. Isso tem sérias conseqüências. O turno não é apenas mais lento, os registros devem ser preservados nas chamadas de função e também não ajuda na inserção e desenrolamento de código.
Se você reescrever o mesmo código no assembler (em linha), poderá obter um aumento de velocidade significativo.
Além disso: o uso do ASM não é a melhor maneira de resolver o problema. A maioria dos compiladores permite que você use algumas instruções do assembler de forma intrínseca se não puder expressá-las em C. O compilador do VS.NET2008, por exemplo, expõe o mul 32 * 32 = 64 bits como __emul e o deslocamento de 64 bits como __ll_rshift.
Usando intrínsecos, você pode reescrever a função de uma maneira que o compilador C tenha a chance de entender o que está acontecendo. Isso permite que o código seja embutido, alocado para registro, eliminação comum de subexpressão e propagação constante também. Você obterá uma enorme melhoria de desempenho com o código do montador escrito à mão dessa maneira.
Para referência: o resultado final da multa de ponto fixo para o compilador VS.NET é:
int inline FixedPointMul (int a, int b)
{
return (int) __ll_rshift(__emul(a,b),16);
}
A diferença de desempenho das divisões de pontos fixos é ainda maior. Eu tive melhorias até o fator 10 para o código de ponto fixo pesado de divisão escrevendo algumas linhas ASM.
O uso do Visual C ++ 2013 fornece o mesmo código de montagem para os dois lados.
O gcc4.1 de 2007 também otimiza bem a versão C pura. (O Godbolt compiler explorer não possui nenhuma versão anterior do gcc instalada, mas, presumivelmente, versões mais antigas do GCC poderiam fazer isso sem intrínseca.)
Consulte source + asm para x86 (32 bits) e ARM no explorador do compilador Godbolt . (Infelizmente, ele não possui compiladores com idade suficiente para produzir código incorreto a partir da versão simples e simples de C).
CPUs modernas podem fazer coisas C não têm operadores para em tudo , como popcnt
ou bit-scan para encontrar o primeiro ou último conjunto de bits . (O POSIX tem uma ffs()
função, mas sua semântica não corresponde a x86 bsf
/ bsr
. Consulte https://en.wikipedia.org/wiki/Find_first_set ).
Às vezes, alguns compiladores podem reconhecer um loop que conta o número de bits definidos em um número inteiro e compilá-lo em uma popcnt
instrução (se ativada no momento da compilação), mas é muito mais confiável usar __builtin_popcnt
no GNU C ou no x86 se você estiver apenas segmentando hardware com SSE4.2: _mm_popcnt_u32
from<immintrin.h>
.
Ou em C ++, atribua a std::bitset<32>
e use .count()
. (Este é o caso em que o idioma encontrou uma maneira de expor portatilmente uma implementação otimizada de popcount por meio da biblioteca padrão, de uma maneira que sempre será compilada com algo correto e que possa tirar proveito do que o destino suportar.) Veja também https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .
Da mesma forma, ntohl
pode compilar até bswap
(x86 swap de bytes de 32 bits para conversão endian) em algumas implementações em C que o possuem.
Outra área importante para intrínsecas ou asm manuscritas é a vetorização manual com instruções SIMD. Compiladores não são ruins com loops simples como dst[i] += src[i] * 10.0;
, mas geralmente se saem mal ou não se auto-vectorizam quando as coisas ficam mais complicadas. Por exemplo, é improvável que você obtenha algo como Como implementar o atoi usando o SIMD? gerado automaticamente pelo compilador a partir do código escalar.