A multiplicação e a divisão podem ser obtidas usando operadores de bits, por exemplo
i*2 = i<<1
i*3 = (i<<1) + i;
i*10 = (i<<3) + (i<<1)
e assim por diante.
É realmente mais rápido usar o say (i<<3)+(i<<1)
para multiplicar por 10 do que usar i*10
diretamente? Existe algum tipo de entrada que não possa ser multiplicada ou dividida dessa maneira?
Respostas:
Resposta curta: improvável.
Resposta longa: Seu compilador possui um otimizador que sabe como multiplicar tão rapidamente quanto a arquitetura do processador de destino. Sua melhor aposta é informar claramente ao seu compilador sua intenção (ou seja, i * 2 em vez de i << 1) e deixar que ele decida qual é a sequência de código de montagem / máquina mais rápida. É até possível que o próprio processador tenha implementado a instrução de multiplicação como uma sequência de turnos e acréscimos no microcódigo.
Conclusão - não gaste muito tempo se preocupando com isso. Se você quer mudar, mude. Se você deseja multiplicar, multiplique. Faça o que é semanticamente mais claro - seus colegas de trabalho agradecerão mais tarde. Ou, mais provavelmente, amaldiçoá-lo mais tarde, se você fizer o contrário.
fonte
gcc -O3
no x86 doreturn i*10
que na versão shift . Como alguém que olha muito para o compilador (veja muitas das minhas respostas asm / otimização), não estou surpreso. Há momentos em que pode ajudar a segurar o compilador manualmente em uma maneira de fazer as coisas , mas essa não é uma delas. O gcc é bom em matemática de números inteiros, porque é importante.millis() >> 2
; Teria sido pedir demais para apenas dividir?i / 32
vsi >> 5
ei / 4
vsi >> 2
no gcc para o córtex-a9 (que não possui divisão de hardware) com otimização -O3 e o conjunto resultante foi exatamente o mesmo. Eu não gostei de usar divisões primeiro, mas ele descreve minha intenção e a saída é a mesma.Apenas um ponto de medida concreto: muitos anos atrás, eu comparei duas versões do meu algoritmo de hash:
e
Em todas as máquinas em que eu comparava, a primeira era pelo menos tão rápida quanto a segunda. Surpreendentemente, às vezes era mais rápido (por exemplo, em um Sun Sparc). Quando o hardware não suportava multiplicação rápida (e a maioria não era na época), o compilador convertia a multiplicação nas combinações apropriadas de turnos e add / sub. E porque sabia o objetivo final, às vezes podia fazê-lo em menos instruções do que quando você escrevia explicitamente os turnos e os add / subs.
Observe que isso foi algo como 15 anos atrás. Felizmente, os compiladores só ficaram melhores desde então, então você pode contar com o compilador fazendo a coisa certa, provavelmente melhor do que você poderia. (Além disso, a razão pela qual o código parece tão C'ish é porque era há mais de 15 anos. Eu obviamente usaria
std::string
e iteradores hoje.)fonte
Além de todas as outras boas respostas aqui, deixe-me apontar outro motivo para não usar turno quando você quer dizer dividir ou multiplicar. Nunca vi alguém introduzir um bug esquecendo a relativa precedência da multiplicação e adição. Vi bugs introduzidos quando os programadores de manutenção esqueceram que "multiplicar" por meio de um turno é logicamente uma multiplicação, mas não sintaticamente da mesma precedência que a multiplicação.
x * 2 + z
ex << 1 + z
são muito diferentes!Se você estiver trabalhando em números , use operadores aritméticos como
+ - * / %
. Se você estiver trabalhando em matrizes de bits, use operadores de ajuste de bits como& ^ | >>
. Não os misture; uma expressão que tanto modifica como aritmética é um bug que está esperando para acontecer.fonte
Isso depende do processador e do compilador. Alguns compiladores já otimizam o código dessa maneira, outros não. Portanto, você precisa verificar sempre que seu código precisar ser otimizado dessa maneira.
A menos que você precise desesperadamente otimizar, eu não codificaria meu código-fonte apenas para salvar uma instrução de montagem ou um ciclo do processador.
fonte
>>
operador é mais rápido que/
, e se os valores assinados podem ser negativos, também é semanticamente superior. Se alguém precisa do valor quex>>4
produziria, isso é muito mais claro quex < 0 ? -((-1-x)/16)-1 : x/16;
, e não consigo imaginar como um compilador poderia otimizar essa última expressão para algo agradável.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 )
(de alguns anúncios da Intel)
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)
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 valoresx
,y
ez
, 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 osint
valores. Por exemplo, considere sua pergunta:Você ilustra a multiplicação, mas e a divisão?
De acordo com o C ++ Standard 5.8:
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".Sim ... como mencionado acima, os números negativos têm um comportamento definido pela implementação quando "dividido" pela troca de bits.
fonte
intVal>>1
terá a mesma semântica que difere da deintVal/2
uma maneira que às vezes é útil. Se for necessário calcular de maneira portátil o valor que as arquiteturas comuns renderiamintVal >> 1
, a expressão precisaria ser um pouco mais complicada e difícil de ler, e provavelmente geraria um código substancialmente inferior ao produzidointVal >> 1
.Apenas tentei na minha máquina compilar isso:
Ao desmontar, produz saída:
Esta versão é mais rápida que o seu código otimizado para as mãos, com pura mudança e adição.
Você realmente nunca sabe o que o compilador criará, então é melhor simplesmente escrever uma multiplicação normal e deixá-lo otimizar da maneira que ele deseja, exceto em casos muito precisos em que você sabe que o compilador não pode otimizar.
fonte
vector<T>::size()
. Meu compilador era bastante antigo! :)Mudar é geralmente muito mais rápido do que multiplicar em um nível de instrução, mas você pode estar perdendo seu tempo fazendo otimizações prematuras. O compilador pode muito bem executar essas otimizações em tempo de compilação. Fazer você mesmo afetará a legibilidade e possivelmente não terá efeito no desempenho. Provavelmente, só vale a pena fazer coisas assim se você criar um perfil e achar que isso é um gargalo.
Na verdade, o truque da divisão, conhecido como 'divisão mágica', pode realmente render grandes recompensas. Novamente, você deve criar um perfil primeiro para ver se é necessário. Mas se você o usar, existem programas úteis para ajudá-lo a descobrir quais instruções são necessárias para a mesma semântica de divisão. Aqui está um exemplo: http://www.masm32.com/board/index.php?topic=12421.0
Um exemplo que tirei do thread do OP no MASM32:
Geraria:
fonte
As instruções de mudança e multiplicação de números inteiros têm desempenho semelhante na maioria das CPUs modernas - as instruções de multiplicação de números inteiros eram relativamente lentas nos anos 80, mas, em geral, isso não é mais verdade. Instruções de multiplicação de número inteiro podem ter maior latência ; portanto, ainda pode haver casos em que uma mudança é preferível. O mesmo vale para os casos em que você pode manter mais unidades de execução ocupadas (embora isso possa ocorrer nos dois sentidos).
A divisão de números inteiros ainda é relativamente lenta, portanto, usar uma mudança em vez de divisão por uma potência de 2 ainda é uma vitória, e a maioria dos compiladores implementará isso como uma otimização. Observe, no entanto, que para que essa otimização seja válida, o dividendo precisa ser não assinado ou deve ser positivo. Para um dividendo negativo, o deslocamento e a divisão não são equivalentes!
Resultado:
Portanto, se você deseja ajudar o compilador, verifique se a variável ou expressão no dividendo está explicitamente sem sinal.
fonte
Depende completamente do dispositivo de destino, idioma, finalidade etc.
Trituração de pixels em um driver de placa de vídeo? Muito provavelmente sim!
Aplicativo de negócios .NET para o seu departamento? Absolutamente nenhuma razão para sequer olhar para ele.
Para um jogo de alto desempenho para um dispositivo móvel, pode valer a pena investigar, mas somente após a otimização mais fácil.
fonte
Não faça isso, a menos que seja absolutamente necessário e sua intenção de código exija mudança em vez de multiplicação / divisão.
Em um dia típico - você pode economizar potencialmente poucos ciclos de máquina (ou menos, já que o compilador sabe melhor o que otimizar), mas o custo não vale a pena - você gasta tempo com pequenos detalhes em vez de trabalhos reais, mantendo o código mais difícil e seus colegas de trabalho o amaldiçoarão.
Pode ser necessário fazer isso para cálculos de alta carga, em que cada ciclo salvo significa minutos de tempo de execução. Porém, você deve otimizar um local de cada vez e fazer testes de desempenho a cada vez para ver se realmente tornou mais rápido ou quebrou a lógica dos compiladores.
fonte
Tanto quanto sei em algumas máquinas, a multiplicação pode precisar de 16 a 32 ciclos de máquina. Portanto , sim , dependendo do tipo de máquina, os operadores de deslocamento de bits são mais rápidos que a multiplicação / divisão.
No entanto, certas máquinas possuem seu processador matemático, que contém instruções especiais para multiplicação / divisão.
fonte
Concordo com a resposta marcada por Drew Hall. A resposta pode usar algumas notas adicionais.
Para a grande maioria dos desenvolvedores de software, o processador e o compilador não são mais relevantes para a questão. A maioria de nós está muito além do 8088 e do MS-DOS. Talvez seja relevante apenas para aqueles que ainda estão desenvolvendo para processadores embarcados ...
Na minha empresa de software, Math (add / sub / mul / div) deve ser usado para toda a matemática. Enquanto Shift deve ser usado ao converter entre tipos de dados, por exemplo. ushort para byte como n >> 8 e não n / 256.
fonte
No caso de números inteiros assinados e deslocamento à direita x divisão, isso pode fazer a diferença. Para números negativos, o turno arredonda para o infinito negativo, enquanto a divisão arredonda para o zero. É claro que o compilador mudará a divisão para algo mais barato, mas geralmente mudará para algo que tenha o mesmo comportamento de arredondamento da divisão, porque é incapaz de provar que a variável não será negativa ou simplesmente não Cuidado. Portanto, se você puder provar que um número não será negativo ou se não se importar com a maneira como ele arredondará, poderá fazer essa otimização de uma maneira que seja mais provável que faça a diferença.
fonte
unsigned
Teste Python executando a mesma multiplicação 100 milhões de vezes contra os mesmos números aleatórios.
Portanto, ao fazer uma mudança em vez de multiplicar / divisão por uma potência de dois em python, há uma ligeira melhora (~ 10% para divisão; ~ 1% para multiplicação). Se não for dois, provavelmente haverá uma desaceleração considerável.
Novamente, esses #s mudam dependendo do seu processador, compilador (ou intérprete - feito em python por simplicidade).
Como com todos os outros, não otimize prematuramente. Escreva um código muito legível, crie um perfil, se não for rápido o suficiente, e tente otimizar as partes lentas. Lembre-se de que seu compilador é muito melhor em otimização do que você.
fonte
Existem otimizações que o compilador não pode fazer porque elas funcionam apenas para um conjunto reduzido de entradas.
Abaixo, há um código de exemplo c ++ que pode fazer uma divisão mais rápida executando uma "Multiplicação pelo inverso" de 64 bits. O numerador e o denominador devem estar abaixo de determinado limite. Observe que ele deve ser compilado para usar instruções de 64 bits para ser realmente mais rápido que a divisão normal.
fonte
Eu acho que, no caso em que você deseja multiplicar ou dividir por uma potência de dois, você não pode errar ao usar operadores de deslocamento de bits, mesmo que o compilador os converta em um MUL / DIV, porque alguns processadores microcódigo (na verdade, um macro) de qualquer maneira, portanto, nesses casos, você obterá uma melhoria, especialmente se o deslocamento for maior que 1. Ou mais explicitamente, se a CPU não tiver operadores de deslocamento de bits, será um MUL / DIV de qualquer maneira, mas se a CPU tiver operadores de deslocamento de bits, você evita uma ramificação de microcódigo e estas são algumas instruções a menos.
Estou escrevendo algum código agora que requer muitas operações de duplicação / redução pela metade porque está trabalhando em uma árvore binária densa, e há mais uma operação que eu suspeito que possa ser mais ideal do que uma adição - uma esquerda (potência de duas multiplique ) mudar com uma adição. Isso pode ser substituído por um deslocamento à esquerda e um xor se o deslocamento for maior que o número de bits que você deseja adicionar; o exemplo fácil é (i << 1) ^ 1, que adiciona um a um valor dobrado. Obviamente, isso não se aplica a um deslocamento à direita (potência de duas divisões) porque apenas um deslocamento à esquerda (little endian) preenche a lacuna com zeros.
No meu código, essas multiplicações / divisões por duas e potências de duas operações são usadas intensivamente e, como as fórmulas já são bastante curtas, cada instrução que pode ser eliminada pode ser um ganho substancial. Se o processador não suportar esses operadores de deslocamento de bits, nenhum ganho ocorrerá, mas também não haverá perda.
Além disso, nos algoritmos que estou escrevendo, eles representam visualmente os movimentos que ocorrem e, nesse sentido, são de fato mais claros. O lado esquerdo de uma árvore binária é maior e o direito é menor. Além disso, no meu código, os números pares e ímpares têm um significado especial, e todos os filhos da esquerda na árvore são ímpares e todos os filhos da mão direita e a raiz são pares. Em alguns casos, que eu ainda não encontrei, mas talvez não tenha pensado nisso, x & 1 pode ser uma operação mais ideal em comparação com x% 2. x e 1 em um número par produzirão zero, mas produzirão 1 para um número ímpar.
Indo um pouco além da identificação ímpar / par, se eu receber zero para x e 3, sei que 4 é um fator do nosso número e o mesmo para x% 7 para 8 e assim por diante. Sei que esses casos provavelmente têm utilidade limitada, mas é bom saber que você pode evitar uma operação de módulo e usar uma operação lógica bit a bit, porque as operações bit a bit são quase sempre as mais rápidas e com menor probabilidade de serem ambíguas para o compilador.
Estou inventando praticamente o campo de árvores binárias densas, por isso espero que as pessoas não compreendam o valor desse comentário, pois muito raramente as pessoas querem apenas executar fatorações com apenas potências de dois ou apenas multiplicar / dividir potências de duas.
fonte
Se é realmente mais rápido depende do hardware e compilador realmente usado.
fonte
Se você comparar a saída para a sintaxe x + x, x * 2 e x << 1 em um compilador gcc, obterá o mesmo resultado no assembly x86: https://godbolt.org/z/JLpp0j
Portanto, você pode considerar o gcc suficientemente inteligente para determinar sua melhor solução independentemente do que você digitou.
fonte
Eu também queria ver se eu poderia bater a casa. este é um bit a bit mais geral para qualquer número por qualquer multiplicação de números. as macros que fiz são cerca de 25% a mais que o dobro da multiplicação normal *. como dito por outros, se for próximo de um múltiplo de 2 ou composto de poucos múltiplos de 2, você poderá ganhar. como X * 23 composto por (X << 4) + (X << 2) + (X << 1) + X será mais lento que X * 65 composto por (X << 6) + X.
fonte