Eu acho que seria mais útil para o questionador ter uma resposta mais diferenciada, porque vejo várias suposições não examinadas nas perguntas e em algumas das respostas ou comentários.
O tempo de execução relativo resultante de deslocamento e multiplicação não tem nada a ver com C. Quando digo C, não me refiro à instância de uma implementação específica, como aquela ou aquela versão do GCC, mas a linguagem. Não pretendo tomar esse ad absurdum, mas usar um exemplo extremo para ilustração: você pode implementar um compilador C completamente compatível com os padrões e fazer com que a multiplicação demore uma hora, enquanto a mudança leva milissegundos - ou o contrário. Não conheço nenhuma restrição de desempenho em C ou C ++.
Você pode não se importar com esse tecnicismo na argumentação. Sua intenção era provavelmente apenas testar o desempenho relativo de turnos versus multiplicações e você escolheu C, porque geralmente é percebida como uma linguagem de programação de baixo nível; portanto, pode-se esperar que seu código-fonte seja traduzido em instruções correspondentes mais diretamente. Tais perguntas são muito comuns e acho que uma boa resposta deve apontar que, mesmo em C, seu código-fonte não se traduz em instruções tão diretamente quanto você pensa em um determinado caso. Dei a você alguns resultados possíveis de compilação abaixo.
É aqui que entram os comentários que questionam a utilidade de substituir essa equivalência no software do mundo real. Você pode ver alguns nos comentários da sua pergunta, como o de Eric Lippert. Está de acordo com a reação que você geralmente obtém de engenheiros mais experientes em resposta a essas otimizações. Se você usar mudanças binárias no código de produção como um meio geral de multiplicar e dividir, as pessoas provavelmente se assustarão com o seu código e terão algum grau de reação emocional ("eu ouvi essa afirmação absurda feita sobre o JavaScript, pelo amor de Deus"). isso pode não fazer sentido para programadores iniciantes, a menos que eles entendam melhor os motivos dessas reações.
Esses motivos são principalmente uma combinação da diminuição da legibilidade e da futilidade dessa otimização, como você já deve ter descoberto ao comparar seu desempenho relativo. No entanto, não acho que as pessoas teriam uma reação tão forte se a substituição do turno pela multiplicação fosse o único exemplo dessas otimizações. Perguntas como a sua costumam aparecer de várias formas e em vários contextos. Acho que o que mais engenheiros seniores realmente reagem tão fortemente, pelo menos às vezes, é que há potencial para uma gama muito maior de danos quando as pessoas empregam essas micro otimizações liberalmente em toda a base de código. Se você trabalha em uma empresa como a Microsoft em uma grande base de códigos, passa muito tempo lendo o código-fonte de outros engenheiros ou tenta localizar determinado código nele. Pode até ser o seu próprio código que você tentará entender daqui a alguns anos, particularmente em alguns dos momentos mais inoportunos, como quando você precisa corrigir uma interrupção da produção após uma chamada que você recebeu no pager dever em uma noite de sexta-feira, prestes a sair para uma noite de diversão com os amigos ... Se você gastar tanto tempo lendo código, apreciará que seja o mais legível possível. Imagine ler seu romance favorito, mas a editora decidiu lançar uma nova edição em que eles usam a abrev. tudo sobre plc bcs thy thnk svs spc. Isso é semelhante às reações que outros engenheiros podem ter ao seu código, se você as derramar com essas otimizações. Como outras respostas apontaram, é melhor indicar claramente o que você quer dizer,
Mesmo nesses ambientes, você pode resolver uma questão de entrevista em que espera conhecer essa ou alguma outra equivalência. Conhecê-los não é ruim e um bom engenheiro estaria ciente do efeito aritmético da mudança binária. Note que eu não disse que isso é um bom engenheiro, mas que um bom engenheiro saberia, na minha opinião. Em particular, você ainda pode encontrar algum gerente, geralmente no final do ciclo de entrevistas, que sorrirá amplamente para você, antecipando o prazer de revelar esse "truque" inteligente de engenharia em uma questão de codificação e provar que ele / ela também costumava ser ou é um dos engenheiros mais experientes e não "apenas" um gerente. Nessas situações, tente parecer impressionado e agradeça pela entrevista esclarecedora.
Por que você não viu uma diferença de velocidade em C? A resposta mais provável é que ambos resultaram no mesmo código de montagem:
int shift(int i) { return i << 2; }
int multiply(int i) { return i * 2; }
Os dois podem compilar em
shift(int):
lea eax, [0+rdi*4]
ret
No GCC sem otimizações, ou seja, usando o sinalizador "-O0", você pode obter o seguinte:
shift(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
sal eax, 2
pop rbp
ret
multiply(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
add eax, eax
pop rbp
ret
Como você pode ver, passar "-O0" para o GCC não significa que não será um pouco inteligente sobre o tipo de código que produz. Em particular, observe que, mesmo nesse caso, o compilador evitou o uso de uma instrução de multiplicação. Você pode repetir o mesmo experimento com turnos de outros números e até multiplicações de números que não são potências de dois. Provavelmente, em sua plataforma, você verá uma combinação de turnos e acréscimos, mas sem multiplicações. Parece uma coincidência para o compilador aparentemente evitar o uso de multiplicações em todos esses casos, se multiplicações e trocas realmente tiverem o mesmo custo, não é? Mas eu não pretendo fornecer suposição para prova, então vamos seguir em frente.
Você pode executar novamente seu teste com o código acima e ver se percebe uma diferença de velocidade agora. Mesmo assim, você não está testando shift versus multiplicar, como é possível ver pela ausência de multiplicação, mas o código que foi gerado com um determinado conjunto de sinalizadores pelo GCC para as operações C de shift e multiplicar em uma instância específica . Portanto, em outro teste, você pode editar o código do assembly manualmente e, em vez disso, usar uma instrução "imul" no código para o método "multiplicar".
Se você quiser derrotar alguns desses pontos inteligentes do compilador, defina um método de mudança e multiplicação mais geral e acabará com algo assim:
int shift(int i, int j) { return i << j; }
int multiply(int i, int j) { return i * j; }
O que pode gerar o seguinte código de montagem:
shift(int, int):
mov eax, edi
mov ecx, esi
sal eax, cl
ret
multiply(int, int):
mov eax, edi
imul eax, esi
ret
Finalmente, temos finalmente, mesmo no nível mais alto de otimização do GCC 4.9, a expressão nas instruções de montagem que você pode esperar quando iniciou o teste. Eu acho que por si só pode ser uma lição importante na otimização de desempenho. Podemos ver a diferença que ele fez para substituir constantes concretas pelas variáveis em nosso código, em termos de inteligência que o compilador é capaz de aplicar. Micro-otimizações, como a substituição de multiplicação por turno, são algumas otimizações de nível muito baixo que um compilador geralmente pode fazer sozinho. Outras otimizações que são muito mais impactantes no desempenho exigem uma compreensão da intenção do códigoisso geralmente não é acessível pelo compilador ou pode ser adivinhado apenas por algumas heurísticas. É aí que você entra como engenheiro de software e, certamente, normalmente não envolve substituir multiplicações por turnos. Envolve fatores como evitar uma chamada redundante para um serviço que produz E / S e pode bloquear um processo. Se você for ao seu disco rígido ou, se Deus permitir, a um banco de dados remoto para obter alguns dados extras que você poderia ter derivado do que você já tem na memória, o tempo gasto em espera será superior à execução de um milhão de instruções. Agora, acho que nos afastamos um pouco da sua pergunta original, mas acho que isso é apontado para um interlocutor, especialmente se supusermos que alguém está começando a entender a tradução e a execução do código,
Então, qual será mais rápido? Eu acho que é uma boa abordagem que você escolheu para testar a diferença de desempenho. Em geral, é fácil se surpreender com o desempenho em tempo de execução de algumas alterações no código. Existem muitas técnicas que os processadores modernos empregam e a interação entre softwares também pode ser complexa. Mesmo se você obtiver resultados benéficos de desempenho para uma determinada mudança em uma situação, acho perigoso concluir que esse tipo de alteração sempre trará benefícios de desempenho. Eu acho que é perigoso executar esses testes uma vez, diga "Ok, agora eu sei qual é o mais rápido!" e aplique indiscriminadamente essa mesma otimização ao código de produção sem repetir suas medições.
E daí se a mudança for mais rápida que a multiplicação? Certamente há indicações de por que isso seria verdade. O GCC, como você pode ver acima, parece pensar (mesmo sem otimização) que evitar a multiplicação direta em favor de outras instruções é uma boa idéia. O Manual de referência da otimização de arquiteturas Intel 64 e IA-32 fornecerá uma idéia do custo relativo das instruções da CPU. Outro recurso, mais focado na latência e no rendimento das instruções, é http://www.agner.org/optimize/instruction_tables.pdf. Observe que eles não são um bom predicador de tempo de execução absoluto, mas do desempenho de instruções relativas uma à outra. Em um loop restrito, enquanto seu teste está simulando, a métrica de "taxa de transferência" deve ser mais relevante. É o número de ciclos pelos quais uma unidade de execução normalmente estará vinculada ao executar uma determinada instrução.
E daí que a mudança NÃO é mais rápida que a multiplicação? Como eu disse acima, arquiteturas modernas podem ser bastante complexas e coisas como unidades de previsão de ramificação, armazenamento em cache, pipelining e execução paralela podem dificultar a previsão do desempenho relativo de duas partes de código logicamente equivalentes às vezes. Eu realmente quero enfatizar isso, porque é aqui que não estou feliz com a maioria das respostas para perguntas como estas e com o grupo de pessoas dizendo que simplesmente não é verdade (mais) que a mudança é mais rápida que a multiplicação.
Não, até onde sei, não inventamos um molho secreto de engenharia nos anos 70 ou quando anulamos repentinamente a diferença de custo de uma unidade de multiplicação e um pouco de shifter. Uma multiplicação geral, em termos de portas lógicas, e certamente em termos de operações lógicas, ainda é mais complexa do que uma mudança com um deslocador de barril em muitos cenários, em muitas arquiteturas. Como isso se traduz no tempo de execução geral em um computador desktop pode ser um pouco opaco. Não sei ao certo como eles são implementados em processadores específicos, mas aqui está uma explicação de uma multiplicação: A multiplicação inteira é realmente a mesma velocidade da adição na CPU moderna
Enquanto aqui está uma explicação de um deslocador de tambor . Os documentos que mencionei no parágrafo anterior oferecem outra visão sobre o custo relativo das operações, por proxy das instruções da CPU. Os engenheiros da Intel frequentemente parecem ter perguntas semelhantes: os fóruns da zona de desenvolvedor da intel criam ciclos de multiplicação de números inteiros e adição no processador core 2 duo
Sim, na maioria dos cenários da vida real, e quase certamente em JavaScript, tentar explorar essa equivalência pelo desempenho é provavelmente uma tarefa fútil. No entanto, mesmo se forçarmos o uso de instruções de multiplicação e não vermos diferença no tempo de execução, isso se deve mais à natureza da métrica de custo que usamos, para ser mais preciso, e não porque não há diferença de custo. O tempo de execução de ponta a ponta é uma métrica e, se é a única com a qual nos preocupamos, tudo está bem. Mas isso não significa que todas as diferenças de custo entre multiplicação e deslocamento simplesmente desapareceram. E acho que certamente não é uma boa ideia transmitir essa ideia a um questionador, por implicação ou não, que obviamente está apenas começando a ter uma idéia dos fatores envolvidos no tempo de execução e no custo do código moderno. A engenharia é sempre sobre compensações. A investigação e a explicação sobre quais compensações os processadores modernos fizeram para exibir o tempo de execução que nós, como usuários acabamos vendo, pode produzir uma resposta mais diferenciada. E acho que uma resposta mais diferenciada do que "isso simplesmente não é mais verdade" é justificada se quisermos ver menos engenheiros verificando o código micro-otimizado obliterando a legibilidade, porque é necessário um entendimento mais geral da natureza dessas "otimizações" para identifique suas várias encarnações diversas do que simplesmente se referir a alguns casos específicos como desatualizados.