Pergunta

Há uma década ou duas, valeu a pena escrever código numérico para evitar o uso de multiplicações e dividir e usar adição e subtração. Um bom exemplo está usando diferenças de avanço Avaliar uma curva polinomial em vez de calcular diretamente o polinômio.

Ainda é esse o caso, ou as arquiteturas modernas de computadores avançam até o ponto em que *,/ não são mais muitas vezes mais lentas que +,-?

Para ser específico, estou interessado em o código C/C ++ compilado em execução em chips x86 típicos modernos com hardware de ponto flutuante a bordo, não um pequeno micro tentando fazer FP no software. Percebo que os aprimoramentos de pipelining e outras arquiteturas impedem a contagem de ciclo específica, mas eu ainda gostaria de obter uma intuição útil.

Foi útil?

Solução

Também depende do mix de instruções. Seu processador terá várias unidades de computação a qualquer momento e você obterá o máximo de rendimento se todas elas forem preenchidas o tempo todo. Portanto, a execução de um loop de MUL é tão rápido quanto a execução de um loop ou adiciona - mas o mesmo não se mantém se a expressão se tornar mais complexa.

Por exemplo, pegue este loop:

for(int j=0;j<NUMITER;j++) {
  for(int i=1;i<NUMEL;i++) {
    bla += 2.1 + arr1[i] + arr2[i] + arr3[i] + arr4[i] ;
  }
}

Para tumiter = 10^7, numel = 10^2, ambas as matrizes inicializadas em pequenos números positivos (a NAN é muito mais lenta), isso leva 6,0 segundos usando duplas em um Proc de 64 bits. Se eu substituir o loop por

bla += 2.1 * arr1[i] + arr2[i] + arr3[i] * arr4[i] ;

Leva apenas 1,7 segundos ... então, como "exageramos" as adições, os Muls eram essencialmente livres; E a redução além de acrescentar ajudou. Fica mais confuso:

bla += 2.1 + arr1[i] * arr2[i] + arr3[i] * arr4[i] ;

- A mesma distribuição do Mul/Adicionar, mas agora a constante é adicionada em vez de multiplicada- leva 3,7 segundos. Seu processador provavelmente é otimizado para executar cálculos numéricos típicos com mais eficiência; Portanto, o produto de ponto como somas de muls e somas em escala são tão boas quanto é; Adicionar constantes não é tão comum, então isso é mais lento ...

bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; /*someval == 2.1*/

novamente leva 1,7 segundos.

bla += someval + arr1[i] + arr2[i] + arr3[i] + arr4[i] ; /*someval == 2.1*/

(o mesmo que o loop inicial, mas sem adição constante cara: 2,1 segundos)

bla += someval * arr1[i] * arr2[i] * arr3[i] * arr4[i] ; /*someval == 2.1*/

(principalmente muls, mas uma adição: 1,9 segundos)

Então, basicamente; É difícil dizer o que é mais rápido, mas se você deseja evitar gargalos, mais importante é ter uma mistura sã, evitar nan ou inf, evitar adicionar constantes. Faça o que fizer, certifique -se de testar e testar várias configurações do compilador, pois muitas vezes pequenas alterações podem fazer a diferença.

Mais alguns casos:

bla *= someval; // someval very near 1.0; takes 2.1 seconds
bla *= arr1[i] ;// arr1[i] all very near 1.0; takes 66(!) seconds
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; // 1.6 seconds
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; //32-bit mode, 2.2 seconds
bla += someval + arr1[i] * arr2[i] + arr3[i] * arr4[i] ; //32-bit mode, floats 2.2 seconds
bla += someval * arr1[i]* arr2[i];// 0.9 in x64, 1.6 in x86
bla += someval * arr1[i];// 0.55 in x64, 0.8 in x86
bla += arr1[i] * arr2[i];// 0.8 in x64, 0.8 in x86, 0.95 in CLR+x64, 0.8 in CLR+x86

Outras dicas

Em teoria, a informação está aqui:

Manual de referência de otimização de arquiteturas Intel®64 e IA-32, Apêndice C Latência e taxa de transferência de instrução

Para cada processador que listam, a latência no FMUL é muito próxima da de Fadd ou FDIV. Em alguns dos processadores mais antigos, o FDIV é 2-3 mais lento do que isso, enquanto nos processadores mais recentes, é o mesmo que o FMUL.

Ressalvas:

  1. O documento que vinculei na verdade diz que você não pode confiar nesses números na vida real, pois o processador fará o que deseja tornar as coisas mais rapidamente, se estiver correto.

  2. Há uma boa chance de seu compilador decidir usar um dos muitos conjuntos de instruções mais recentes que possuem um ponto flutuante multiplicar / dividir disponíveis.

  3. Este é um documento complicado apenas para ser lido pelos escritores do compilador e eu poderia ter entendido errado. Como se não estivesse claro por que o número de latência do FDIV está ausente para algumas das CPUs.

A melhor maneira de responder a essa pergunta é realmente escrever uma referência/perfil do processamento que você precisa fazer. O empírico deve ser usado sobre teórico sempre que possível. Especialmente quando é fácil de obter.

Se você já conhece diferentes implementações da matemática que precisa fazer, pode escrever poucas transferimentos de código diferentes da matemática e ver onde seu desempenho atinge o pico. Isso permitirá que o processador/compilador gere diferentes fluxos de execução para preencher os dutos do processador e fornecer uma resposta concreta à sua resposta.

Se você se interessa especificamente no desempenho das instruções DIV/MUL/ADD/SUB TIPO, você pode até lançar em algum conjunto em linha para controlar especificamente quais variantes dessas instruções foram executadas. No entanto, você precisa ter certeza de que está mantendo as unidades de execução multilple ocupadas para ter uma boa idéia do desempenho da qual o sistema é capaz.

Também fazer algo assim permitiria comparar o desempenho em várias variações do processador, simplesmente executando o mesmo programa nelas e também pode permitir que você considere as diferenças na placa -mãe.

Editar:

A arquitetura básica de A +- é idêntica. Então eles logicamente levam o mesmo tempo para calcular. * Por outro lado, requer várias camadas, normalmente construídas a partir de "Adders completos" para concluir uma única operação. Isso é que, embora a * possa ser emitido para o pipeline a cada ciclo, ele terá uma latência mais alta que um circuito de adição/subtraia. Uma operação FP / é normalmente implementada usando um método de aproximação que converge iterativamente para a resposta correta ao longo do tempo. Esses tipos de aproximações são normalmente implementados por multiplicação. Portanto, para o ponto flutuante, você geralmente pode assumir que a divisão levará mais tempo, porque é impraticável "desenrolar" as multiplicações (que já é um grande circuito em e de si) no pipeline de uma infinidade de circuitos multiplicadores. Ainda assim, o desempenho de um determinado sistema é melhor medido via teste.

Não consigo encontrar uma referência definitiva, mas a experimentação extensa me diz que a multiplicação de flutuação hoje em dia é quase a mesma velocidade que a adição e a subtração, enquanto a divisão não é (mas não "muitas vezes" também mais lenta). Você pode obter a intuição que deseja apenas executando seus próprios experimentos - lembre -se de gerar os números aleatórios (milhões deles) com antecedência, leia -os antes de iniciar o tempo e usar os contadores de desempenho da CPU (sem outro processo em execução, como Por mais que você possa impedi -los) para uma medição precisa!

A diferença de velocidade de * / vs + - depende da sua arquitetura do processador. Em geral e com x86, em particular, a diferença de velocidade tornou -se menor com os processadores modernos. * Deve estar perto de +, em caso de dúvida: basta experimentar. Se você tiver um problema muito difícil com muitas operações de FP também consideram o uso da sua GPU (GeForce, ...), que funciona como um processador vetorial.

Provavelmente, há muito pouca diferença no tempo entre multiplicação e adição. A divisão, por outro lado, ainda é significativamente mais lenta que a multiplicação por causa de sua natureza recursiva. Sobre as instruções modernas da arquitetura X86, deve ser considerada ao fazer operação de ponto flutuante, em vez de usar a FPU. Embora um bom compilador C/C ++ deve fornecer a opção de usar SSE em vez da FPU.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top