É realmente mais rápido usar say (i << 3) + (i << 1) para multiplicar por 10 do que usar i * 10 diretamente?
Pode ou não estar em sua máquina - se você se importa, meça seu uso no mundo real.
Um estudo de caso - do 486 ao core i7
O benchmarking é muito difícil de ser feito de maneira significativa, mas podemos observar alguns fatos. Em http://www.penguin.cz/~literakl/intel/s.html#SAL e http://www.penguin.cz/~literakl/intel/i.html#IMUL, temos uma idéia dos ciclos do relógio x86 necessário para mudança aritmética e multiplicação. Digamos que aderimos ao "486" (o mais novo listado), registradores e imediatos de 32 bits, o IMUL leva 13-42 ciclos e o IDIV 44. Cada SAL pega 2 e adiciona 1, portanto, mesmo com alguns desses que mudam superficialmente a aparência como um vencedor.
Hoje em dia, com o core i7:
(em http://software.intel.com/en-us/forums/showthread.php?t=61481 )
A latência é de 1 ciclo para uma adição inteira e 3 ciclos para uma multiplicação inteira . Você pode encontrar as latências e a capacidade de processamento no Apêndice C do "Manual de referência da otimização de arquiteturas Intel® 64 e IA-32", localizado em http://www.intel.com/products/processor/manuals/ .
(de alguns anúncios da Intel)
Usando o SSE, o Core i7 pode emitir instruções simultâneas de adição e multiplicação, resultando em uma taxa de pico de 8 operações de ponto flutuante (FLOP) por ciclo de clock
Isso dá uma idéia de quão longe as coisas chegaram. A trivialidade da otimização - como deslocamento de bits versus*
- que foi levada a sério até os anos 90 é obsoleta agora. A troca de bits ainda é mais rápida, mas, para uma potência de dois mul / div, no momento em que você faz todos os seus turnos e adiciona os resultados, é mais lento novamente. Então, mais instruções significam mais falhas de cache, mais problemas em potencial no pipelining, mais uso de registros temporários podem significar mais economia e restauração do conteúdo do registro da pilha ... rapidamente fica muito complicado quantificar definitivamente todos os impactos, mas eles são predominantemente negativo.
funcionalidade no código fonte versus implementação
De maneira mais geral, sua pergunta é marcada como C e C ++. Como linguagens de terceira geração, eles são projetados especificamente para ocultar os detalhes do conjunto de instruções da CPU subjacente. Para satisfazer seus padrões de idioma, eles devem oferecer suporte a operações de multiplicação e deslocamento (e muitas outras), mesmo que o hardware subjacente não o faça . Nesses casos, eles devem sintetizar o resultado necessário usando muitas outras instruções. Da mesma forma, eles devem fornecer suporte de software para operações de ponto flutuante se a CPU não tiver e não houver FPU. CPUs modernas suportam*
e<<
, portanto, isso pode parecer absurdamente teórico e histórico, mas o importante é que a liberdade de escolher a implementação seja nos dois sentidos: mesmo que a CPU possua uma instrução que implemente a operação solicitada no código-fonte no caso geral, o compilador estará livre para escolha outra coisa que prefira, porque é melhor para o caso específico que o compilador enfrenta.
Exemplos (com uma linguagem de montagem hipotética)
source literal approach optimised approach
#define N 0
int x; .word x xor registerA, registerA
x *= N; move x -> registerA
move x -> registerB
A = B * immediate(0)
store registerA -> x
...............do something more with x...............
Instruções como exclusive ou ( xor
) não têm relação com o código-fonte, mas armazenar qualquer coisa por si só limpa todos os bits; portanto, pode ser usado para definir algo como 0. O código-fonte que implica endereços de memória pode não implicar o uso.
Esses tipos de hacks são utilizados há tanto tempo quanto os computadores existem. Nos primeiros dias do 3GLs, para garantir a aceitação do desenvolvedor, a saída do compilador tinha que satisfazer o desenvolvedor de linguagem assembly otimizado para mão, já existente. comunidade que o código produzido não era mais lento, mais detalhado ou pior. Os compiladores adotaram rapidamente muitas ótimas otimizações - eles se tornaram um repositório centralizado melhor do que qualquer programador de linguagem assembly poderia ser, embora sempre haja a chance de que eles percam uma otimização específica que é crucial em um caso específico - às vezes os humanos podem enlouqueça e procure algo melhor, enquanto os compiladores fazem o que lhes foi dito até que alguém os devolva a experiência.
Portanto, mesmo que a troca e a adição ainda sejam mais rápidas em algum hardware específico, é provável que o gravador do compilador tenha funcionado exatamente quando é seguro e benéfico.
Manutenção
Se o seu hardware mudar, você poderá recompilar e ele olhará para a CPU de destino e fará outra melhor escolha, enquanto é improvável que você queira revisitar suas "otimizações" ou listar quais ambientes de compilação devem usar multiplicação e quais devem mudar. Pense em todas as "otimizações" sem deslocamento de dois bits, escritas há mais de 10 anos, que agora estão diminuindo o código em que estão, enquanto são executadas nos processadores modernos ...!
Felizmente, bons compiladores como o GCC podem substituir uma série de turnos de bits e aritmética por uma multiplicação direta quando qualquer otimização é ativada (por exemplo, ...main(...) { return (argc << 4) + (argc << 2) + argc; }
-> imull $21, 8(%ebp), %eax
), para que uma recompilação possa ajudar mesmo sem corrigir o código, mas isso não é garantido.
Código estranho de mudança de bits que implementa multiplicação ou divisão é muito menos expressivo do que você estava tentando alcançar conceitualmente; portanto, outros desenvolvedores ficarão confusos com isso, e um programador confuso terá mais chances de introduzir bugs ou remover algo essencial em um esforço para restaurar a aparente sanidade. Se você só faz coisas não óbvias quando elas são realmente benéficas tangíveis e depois as documenta bem (mas não documenta outras coisas que são intuitivas de qualquer maneira), todos ficarão mais felizes.
Soluções gerais versus soluções parciais
Se você tem algum conhecimento extra, tal como a sua int
vontade realmente apenas ser armazenar valores x
, y
e z
, em seguida, você pode ser capaz de trabalhar para fora algumas instruções que o trabalho para esses valores e que você obtenha o seu resultado mais rapidamente do que quando o compilador de não ter esse insight e precisa de uma implementação que funcione para todos os int
valores. Por exemplo, considere sua pergunta:
A multiplicação e a divisão podem ser obtidas usando operadores de bits ...
Você ilustra a multiplicação, mas e a divisão?
int x;
x >> 1; // divide by 2?
De acordo com o C ++ Standard 5.8:
-3- O valor de E1 >> E2 é E1 com posições de bits E2 deslocadas à direita. Se E1 tiver um tipo não assinado ou E1 tiver um tipo assinado e um valor não negativo, o valor do resultado será a parte integrante do quociente de E1 dividido pela quantidade 2 elevada à potência E2. Se E1 tiver um tipo assinado e um valor negativo, o valor resultante será definido pela implementação.
Portanto, seu deslocamento de bits tem um resultado definido de implementação quando x
é negativo: pode não funcionar da mesma maneira em máquinas diferentes. Mas, /
funciona muito mais previsivelmente. (Também pode não ser perfeitamente consistente, pois máquinas diferentes podem ter representações diferentes de números negativos e, portanto, intervalos diferentes, mesmo quando há o mesmo número de bits que compõem a representação.)
Você pode dizer "eu não ligo ... isso int
é armazenar a idade do funcionário, nunca pode ser negativo". Se você tiver esse tipo de insight especial, sim - sua >>
otimização segura pode ser ignorada pelo compilador, a menos que você faça isso explicitamente em seu código. Porém, é arriscado e raramente útil, na maioria das vezes você não terá esse tipo de insight, e outros programadores trabalhando no mesmo código não saberão que você apostou em algumas expectativas incomuns dos dados que você ' vou lidar com ... o que parece uma mudança totalmente segura para eles pode sair pela culatra por causa da sua "otimização".
Existe algum tipo de entrada que não possa ser multiplicada ou dividida dessa maneira?
Sim ... como mencionado acima, os números negativos têm um comportamento definido pela implementação quando "dividido" pela troca de bits.