Estou fazendo um trabalho crítico de desempenho em C ++ e atualmente estamos usando cálculos inteiros para problemas que são inerentemente de ponto flutuante porque "é mais rápido". Isso causa muitos problemas irritantes e adiciona muitos códigos irritantes.
Agora, eu me lembro de ter lido sobre como os cálculos de ponto flutuante eram tão lentos aproximadamente por volta dos 386 dias, onde eu acredito (IIRC) que havia um coprocessador opcional. Mas certamente hoje em dia com CPUs exponencialmente mais complexas e poderosas não faz diferença na "velocidade" se estiver fazendo cálculo de ponto flutuante ou inteiro? Especialmente porque o tempo de cálculo real é minúsculo em comparação a algo como causar uma paralisação no pipeline ou buscar algo na memória principal?
Eu sei que a resposta correta é fazer o benchmark no hardware de destino, qual seria uma boa maneira de testar isso? Eu escrevi dois pequenos programas C ++ e comparei seu tempo de execução com "tempo" no Linux, mas o tempo de execução real é muito variável (não ajuda, estou executando em um servidor virtual). Sem passar o dia inteiro executando centenas de benchmarks, fazendo gráficos, etc., há algo que eu possa fazer para obter um teste razoável da velocidade relativa? Alguma ideia ou pensamento? Estou completamente errado?
Os programas que usei como segue, eles não são idênticos de forma alguma:
#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>
int main( int argc, char** argv )
{
int accum = 0;
srand( time( NULL ) );
for( unsigned int i = 0; i < 100000000; ++i )
{
accum += rand( ) % 365;
}
std::cout << accum << std::endl;
return 0;
}
Programa 2:
#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>
int main( int argc, char** argv )
{
float accum = 0;
srand( time( NULL ) );
for( unsigned int i = 0; i < 100000000; ++i )
{
accum += (float)( rand( ) % 365 );
}
std::cout << accum << std::endl;
return 0;
}
Desde já, obrigado!
Edit: A plataforma que me interessa é regular x86 ou x86-64 rodando em máquinas desktop Linux e Windows.
Editar 2 (colado de um comentário abaixo): Temos uma ampla base de código atualmente. Na verdade, eu me deparei com a generalização de que "não devemos usar float, pois o cálculo de inteiro é mais rápido" - e estou procurando uma maneira (se isso for verdade) de refutar essa suposição generalizada. Sei que seria impossível prever o resultado exato para nós sem fazer todo o trabalho e traçá-lo depois.
De qualquer forma, obrigado por todas as suas excelentes respostas e ajuda. Sinta-se à vontade para adicionar qualquer outra coisa :).
fonte
addl
substituído porfadd
, por exemplo). A única maneira de realmente obter uma boa medição é obter uma parte central do seu programa real e criar perfis de versões diferentes dele. Infelizmente, isso pode ser muito difícil sem usar muito esforço. Talvez nos dizer o hardware de destino e seu compilador ajudaria as pessoas a pelo menos dar a você experiência pré-existente, etc. Sobre seu uso de inteiros, eu suspeito que você poderia fazer uma espécie defixed_point
classe de modelo que facilitaria esse trabalho tremendamente.float
obtém o aumento de velocidade, mas geralmentedouble
não.Respostas:
Infelizmente, só posso dar uma resposta "depende" ...
Pela minha experiência, existem muitas, muitas variáveis para o desempenho ... especialmente entre números inteiros e matemáticos de ponto flutuante. Ele varia fortemente de processador para processador (mesmo dentro da mesma família, como x86) porque diferentes processadores têm diferentes comprimentos de "pipeline". Além disso, algumas operações geralmente são muito simples (como adição) e têm uma rota acelerada pelo processador, e outras (como divisão) demoram muito, muito mais.
A outra grande variável é onde residem os dados. Se você tiver apenas alguns valores para adicionar, todos os dados podem residir no cache, de onde podem ser enviados rapidamente para a CPU. Uma operação de ponto flutuante muito lenta que já contém os dados no cache será muitas vezes mais rápida do que uma operação de inteiro em que um inteiro precisa ser copiado da memória do sistema.
Presumo que você esteja fazendo esta pergunta porque está trabalhando em um aplicativo de desempenho crítico. Se estiver desenvolvendo para a arquitetura x86 e precisar de desempenho extra, você pode querer usar as extensões SSE. Isso pode acelerar bastante a aritmética de ponto flutuante de precisão única, pois a mesma operação pode ser realizada em vários dados de uma vez, além de haver um * banco de registradores separado para as operações SSE. (Notei em seu segundo exemplo que você usou "float" em vez de "double", me fazendo pensar que você está usando matemática de precisão simples)
* Nota: Usar as instruções antigas da MMX na verdade tornaria os programas mais lentos, porque essas instruções antigas usavam os mesmos registros da FPU, tornando impossível usar a FPU e a MMX ao mesmo tempo.
fonte
double
-precision FP. Com apenas doisdouble
s de 64 bits por registro, a aceleração potencial é menor do quefloat
para o código que vetoriza bem. Escalarfloat
edouble
usar registros XMM em x86-64, com legado x87 usado apenas paralong double
. (Então @ Dan: não, os registros MMX não entram em conflito com os registros FPU normais, porque a FPU normal em x86-64 é a unidade SSE. MMX não faria sentido porque se você pode fazer SIMD inteiro, você quer 16 bytes emxmm0..15
vez de 8 -bytemm0..7
, e CPUs modernas têm transferência de MMX pior do que SSE.)Por exemplo (números menores são mais rápidos),
Intel Xeon X5550 de 64 bits a 2.67 GHz, gcc 4.1.2
-O3
Processador AMD Opteron (tm) de 32 bits Dual Core 265 @ 1,81 GHz, gcc 3.4.6
-O3
Como Dan apontou , mesmo depois de normalizar a frequência de clock (o que pode ser enganoso em si mesmo em projetos em pipeline), os resultados irão variar muito com base na arquitetura da CPU ( desempenho de ALU / FPU individual , bem como número real de ALUs / FPUs disponíveis por núcleo em projetos superescalares que influenciam quantas operações independentes podem executar em paralelo - o último fator não é exercido pelo código abaixo, pois todas as operações abaixo são sequencialmente dependentes.)
Referência de operação FPU / ALU do pobre:
fonte
volatile
para ter certeza. Em Win64, o FPU é não utilizado e MSVC não irá gerar código para ele, então ele compila usandomulss
edivss
instruções XMM lá, que são 25x mais rápido do que o FPU em Win32. A máquina de teste é Core i5 M 520 @ 2,40 GHzv
atingirão rapidamente 0 ou +/- inf muito rapidamente, o que pode ou não ser (teoricamente) tratado como um case / fastpatheed especial por certas implementações fpu.v
). Em designs recentes da Intel, a divisão não é canalizada (divss
/divps
tem latência de 10-14 ciclos e a mesma taxa de transferência recíproca).mulss
no entanto, é a latência de 5 ciclos, mas pode emitir um a cada ciclo. (Ou dois por ciclo em Haswell, uma vez que a porta 0 e a porta 1 têm um multiplicador para FMA).É provável que haja uma diferença significativa na velocidade do mundo real entre a matemática de ponto fixo e de ponto flutuante, mas a taxa de transferência de melhor caso teórica da ALU vs FPU é completamente irrelevante. Em vez disso, o número de registros inteiros e de ponto flutuante (registros reais, não nomes de registro) em sua arquitetura que não são usados de outra forma por sua computação (por exemplo, para controle de loop), o número de elementos de cada tipo que cabem em uma linha de cache , otimizações possíveis considerando as diferentes semânticas para matemática de inteiro vs. ponto flutuante - esses efeitos irão dominar. As dependências de dados de seu algoritmo desempenham um papel significativo aqui, de forma que nenhuma comparação geral irá prever a lacuna de desempenho em seu problema.
Por exemplo, a adição de inteiro é comutativa, então se o compilador vê um loop como você usou para um benchmark (assumindo que os dados aleatórios foram preparados com antecedência para não obscurecer os resultados), ele pode desenrolar o loop e calcular somas parciais com sem dependências, adicione-as quando o loop terminar. Mas com o ponto flutuante, o compilador tem que fazer as operações na mesma ordem que você solicitou (você tem pontos de sequência lá, então o compilador tem que garantir o mesmo resultado, o que não permite a reordenação), então há uma forte dependência de cada adição em o resultado do anterior.
Provavelmente, você também ajustará mais operandos inteiros no cache por vez. Portanto, a versão de ponto fixo pode superar a versão flutuante em uma ordem de magnitude, mesmo em uma máquina onde a FPU tem um rendimento teoricamente maior.
fonte
A adição é muito mais rápida do que
rand
, portanto, seu programa é (especialmente) inútil.Você precisa identificar pontos de acesso de desempenho e modificar gradativamente seu programa. Parece que você tem problemas com seu ambiente de desenvolvimento que precisam ser resolvidos primeiro. É impossível executar seu programa no PC para um pequeno conjunto de problemas?
Geralmente, tentar tarefas FP com aritmética de inteiros é uma receita para lentidão.
fonte
timespec_t
ou algo semelhante. Registre a hora no início e no final do loop e faça a diferença. Em seguida, mova arand
geração de dados para fora do loop. Certifique-se de que seu algoritmo obtenha todos os dados de arrays e coloque todos os dados em arrays. Isso pega seu algoritmo real por si só, e obtém configuração, malloc, impressão de resultados, tudo, exceto troca de tarefas e interrupções fora de seu loop de criação de perfil.TIL Isso varia (muito). Aqui estão alguns resultados usando o compilador GNU (aliás, eu também verifiquei compilando em máquinas, o gnu g ++ 5.4 do xenial é muito mais rápido do que o 4.6.3 do linaro na precisão)
Intel i7 4700MQ xenial
Intel i3 2370M tem resultados semelhantes
Intel (R) Celeron (R) 2955U (Acer C720 Chromebook executando xenial)
DigitalOcean 1 GB Droplet Intel (R) Xeon (R) CPU E5-2630L v2 (em execução confiável)
Processador AMD Opteron (tm) 4122 (preciso)
Este usa o código de http://pastebin.com/Kx8WGUfg como
benchmark-pc.c
Já fiz várias passagens, mas parece que os números gerais são iguais.
Uma exceção notável parece ser ALU mul vs FPU mul. Adição e subtração parecem trivialmente diferentes.
Aqui está o acima em forma de gráfico (clique para ampliar, inferior é mais rápido e preferível):
Atualização para acomodar @Peter Cordes
https://gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc
i7 4700MQ Linux Ubuntu Xenial 64 bits (todos os patches de 13/03/2018 aplicados) Processador AMD Opteron (tm) 4122 (preciso, hospedagem compartilhada DreamHost) Intel Xeon E5-2630L v2 a 2,4 GHz (Trusty 64 bits, DigitalOcean VPS)fonte
benchmark-pc
medindo alguma combinação de taxa de transferência e latência? Em seu Haswell (i7 4700MQ), a multiplicação inteira é 1 por taxa de transferência de clock, latência de 3 ciclos, mas add / sub de número inteiro é 4 por taxa de transferência de clock, latência de 1 ciclo ( agner.org/optimize ). Portanto, presumivelmente, há muito overhead de loop diluindo esses números para que add e mul cheguem tão perto (adição longa: 0,824088 vs. mul longo: 1,017164). (o padrão do gcc é não desenrolar loops, exceto para desenrolar totalmente contagens de iterações muito baixas).int
, apenasshort
elong
? No Linux x86-64,short
é de 16 bits (e, portanto, tem lentidão de registro parcial em alguns casos), enquantolong
elong long
são do tipo 64 bits. (Talvez tenha sido projetado para Windows em que o x86-64 ainda usa 32 bitslong
? Ou talvez tenha sido projetado para o modo de 32 bits.) No Linux, o x32 ABI tem 32 bitslong
no modo de 64 bits , portanto, se você tiver as bibliotecas instaladas , usegcc -mx32
para compilador para ILP32. Ou apenas use-m32
e veja oslong
números.addps
em registros xmm ao invés deaddss
, para fazer 4 FP adiciona em paralelo em uma instrução que é tão rápida quanto escalaraddss
. (Use-march=native
para permitir o uso de quaisquer conjuntos de instruções que sua CPU suporte, não apenas a linha de base SSE2 para x86-64).Dois pontos a considerar -
O hardware moderno pode sobrepor instruções, executá-las em paralelo e reorganizá-las para fazer o melhor uso do hardware. E também, qualquer programa de ponto flutuante significativo provavelmente terá trabalho inteiro significativo também, mesmo se estiver apenas calculando índices em matrizes, contador de loop etc., então mesmo se você tiver uma instrução de ponto flutuante lenta, pode muito bem estar sendo executado em um bit separado de hardware sobreposto com algum do trabalho inteiro. Meu ponto é que mesmo que as instruções de ponto flutuante sejam lentas que as inteiras, seu programa geral pode ser executado mais rápido porque pode fazer uso de mais do hardware.
Como sempre, a única maneira de ter certeza é traçar o perfil de seu programa real.
O segundo ponto é que a maioria das CPUs hoje em dia tem instruções SIMD para ponto flutuante que podem operar em vários valores de ponto flutuante ao mesmo tempo. Por exemplo, você pode carregar 4 flutuadores em um único registro SSE e realizar 4 multiplicações em todos eles em paralelo. Se você puder reescrever partes do seu código para usar as instruções SSE, parece provável que seja mais rápido do que uma versão inteira. O Visual c ++ fornece funções intrínsecas do compilador para fazer isso. Consulte http://msdn.microsoft.com/en-us/library/x5c07e2a(v=VS.80).aspx para obter algumas informações.
fonte
A versão de ponto flutuante será muito mais lenta, se não houver operação de resto. Como todas as adições são sequenciais, a CPU não será capaz de paralelizar a soma. A latência será crítica. A latência de adição de FPU é normalmente de 3 ciclos, enquanto a adição de inteiro é de 1 ciclo. No entanto, o divisor para o operador restante provavelmente será a parte crítica, já que não é totalmente pipeline nas cpus modernas. portanto, assumindo que a instrução divide / resto consumirá a maior parte do tempo, a diferença devido à latência de adição será pequena.
fonte
A menos que você esteja escrevendo um código que será chamado milhões de vezes por segundo (como, por exemplo, desenhar uma linha na tela em um aplicativo gráfico), a aritmética de número inteiro vs. ponto flutuante raramente é o gargalo.
A primeira etapa usual para as questões de eficiência é traçar o perfil de seu código para ver onde o tempo de execução é realmente gasto. O comando do Linux para isso é
gprof
.Editar:
Embora eu suponha que você sempre possa implementar o algoritmo de desenho de linha usando números inteiros e números de ponto flutuante, chame-o um grande número de vezes e veja se faz diferença:
http://en.wikipedia.org/wiki/Bresenham's_algorithm
fonte
Hoje, as operações de inteiros são geralmente um pouco mais rápidas do que as operações de ponto flutuante. Portanto, se você pode fazer um cálculo com as mesmas operações em número inteiro e ponto flutuante, use número inteiro. NO ENTANTO, você está dizendo "Isso causa muitos problemas irritantes e adiciona muitos códigos irritantes". Parece que você precisa de mais operações porque usa aritmética de inteiros em vez de ponto flutuante. Nesse caso, o ponto flutuante será executado mais rápido porque
assim que você precisar de mais operações inteiras, você provavelmente precisará de muito mais, então a ligeira vantagem de velocidade é mais do que consumida pelas operações adicionais
o código de ponto flutuante é mais simples, o que significa que é mais rápido escrever o código, o que significa que se a velocidade for crítica, você pode gastar mais tempo otimizando o código.
fonte
Fiz um teste que acabou de adicionar 1 ao número em vez de rand (). Os resultados (em um x86-64) foram:
fonte
Com base naquele "algo que eu ouvi", tão confiável, antigamente, o cálculo de inteiros era cerca de 20 a 50 vezes mais rápido que o ponto flutuante, e hoje em dia é menos que duas vezes mais rápido.
fonte