Cálculos de ponto flutuante vs inteiro em hardware moderno

100

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 :).

maxpinguim
fonte
8
O que você tem como teste agora é trivial. Provavelmente também há muito pouca diferença na montagem ( addlsubstituído por fadd, 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 de fixed_pointclasse de modelo que facilitaria esse trabalho tremendamente.
GManNickG
1
Ainda há muitas arquiteturas por aí que não têm hardware de ponto flutuante dedicado - algumas tags que explicam os sistemas de seu interesse o ajudarão a obter respostas melhores.
Carl Norum,
3
Eu acredito que o hardware do meu HTC Hero (android) não tem FPU, mas o hardware do Google NexusOne (android) tem. qual é o seu alvo? PCs de desktop / servidor? netbooks (possível arm + linux)? telefones?
SteelBytes
5
Se você deseja FP rápido em x86, tente compilar com otimização e geração de código SSE. SSE (qualquer versão) pode fazer pelo menos flutuação, adição, subtração e multiplicação em um único ciclo. As funções de divisão, modificação e superiores sempre serão lentas. Observe também que floatobtém o aumento de velocidade, mas geralmente doublenão.
Mike D.
1
O número inteiro de ponto fixo aproxima o FP usando várias operações de número inteiro para evitar que os resultados transbordem. Isso quase sempre é mais lento do que usar os FPUs extremamente capazes encontrados nas CPUs de desktop modernas. por exemplo, MAD, o decodificador de ponto fixo de mp3, é mais lento que libmpg123 e, embora seja de boa qualidade para um decodificador de ponto fixo, libmpg123 ainda tem menos erros de arredondamento. wezm.net/technical/2008/04/mp3-decoder-libraries-compared para benchmarks em um PPC G5.
Peter Cordes

Respostas:

35

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.

Dan
fonte
8
E em alguns processadores a matemática FP pode ser mais rápida do que a matemática inteira. O processador Alpha tinha uma instrução de divisão de FP, mas não de inteiros, então a divisão de inteiros tinha que ser feita no software.
Gabe
O SSEx também irá acelerar a aritmética de dupla precisão? Sinto muito, não estou muito familiarizado com SSE
Johannes Schaub - litb
1
@ JohannesSchaub-litb: SSE2 (linha de base para x86-64) contém double-precision FP. Com apenas dois doubles de 64 bits por registro, a aceleração potencial é menor do que floatpara o código que vetoriza bem. Escalar floate doubleusar registros XMM em x86-64, com legado x87 usado apenas para long 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 em xmm0..15vez de 8 -byte mm0..7, e CPUs modernas têm transferência de MMX pior do que SSE.)
Peter Cordes
1
Mas as instruções inteiras MMX e SSE * / AVX2 competem pelas mesmas unidades de execução, portanto, usar ambas ao mesmo tempo quase nunca é útil. Basta usar as versões XMM / YMM mais amplas para realizar mais trabalhos. Usar SIMD inteiro e FP ao mesmo tempo compete pelos mesmos registradores, mas x86-64 tem 16 deles. Mas os limites de rendimento total significam que você não pode realizar o dobro do trabalho usando unidades de execução de inteiros e FP em paralelo.
Peter Cordes
49

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

short add/sub: 1.005460 [0]
short mul/div: 3.926543 [0]
long add/sub: 0.000000 [0]
long mul/div: 7.378581 [0]
long long add/sub: 0.000000 [0]
long long mul/div: 7.378593 [0]
float add/sub: 0.993583 [0]
float mul/div: 1.821565 [0]
double add/sub: 0.993884 [0]
double mul/div: 1.988664 [0]

Processador AMD Opteron (tm) de 32 bits Dual Core 265 @ 1,81 GHz, gcc 3.4.6 -O3

short add/sub: 0.553863 [0]
short mul/div: 12.509163 [0]
long add/sub: 0.556912 [0]
long mul/div: 12.748019 [0]
long long add/sub: 5.298999 [0]
long long mul/div: 20.461186 [0]
float add/sub: 2.688253 [0]
float mul/div: 4.683886 [0]
double add/sub: 2.700834 [0]
double mul/div: 4.646755 [0]

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:

#include <stdio.h>
#ifdef _WIN32
#include <sys/timeb.h>
#else
#include <sys/time.h>
#endif
#include <time.h>
#include <cstdlib>

double
mygettime(void) {
# ifdef _WIN32
  struct _timeb tb;
  _ftime(&tb);
  return (double)tb.time + (0.001 * (double)tb.millitm);
# else
  struct timeval tv;
  if(gettimeofday(&tv, 0) < 0) {
    perror("oops");
  }
  return (double)tv.tv_sec + (0.000001 * (double)tv.tv_usec);
# endif
}

template< typename Type >
void my_test(const char* name) {
  Type v  = 0;
  // Do not use constants or repeating values
  //  to avoid loop unroll optimizations.
  // All values >0 to avoid division by 0
  // Perform ten ops/iteration to reduce
  //  impact of ++i below on measurements
  Type v0 = (Type)(rand() % 256)/16 + 1;
  Type v1 = (Type)(rand() % 256)/16 + 1;
  Type v2 = (Type)(rand() % 256)/16 + 1;
  Type v3 = (Type)(rand() % 256)/16 + 1;
  Type v4 = (Type)(rand() % 256)/16 + 1;
  Type v5 = (Type)(rand() % 256)/16 + 1;
  Type v6 = (Type)(rand() % 256)/16 + 1;
  Type v7 = (Type)(rand() % 256)/16 + 1;
  Type v8 = (Type)(rand() % 256)/16 + 1;
  Type v9 = (Type)(rand() % 256)/16 + 1;

  double t1 = mygettime();
  for (size_t i = 0; i < 100000000; ++i) {
    v += v0;
    v -= v1;
    v += v2;
    v -= v3;
    v += v4;
    v -= v5;
    v += v6;
    v -= v7;
    v += v8;
    v -= v9;
  }
  // Pretend we make use of v so compiler doesn't optimize out
  //  the loop completely
  printf("%s add/sub: %f [%d]\n", name, mygettime() - t1, (int)v&1);
  t1 = mygettime();
  for (size_t i = 0; i < 100000000; ++i) {
    v /= v0;
    v *= v1;
    v /= v2;
    v *= v3;
    v /= v4;
    v *= v5;
    v /= v6;
    v *= v7;
    v /= v8;
    v *= v9;
  }
  // Pretend we make use of v so compiler doesn't optimize out
  //  the loop completely
  printf("%s mul/div: %f [%d]\n", name, mygettime() - t1, (int)v&1);
}

int main() {
  my_test< short >("short");
  my_test< long >("long");
  my_test< long long >("long long");
  my_test< float >("float");
  my_test< double >("double");

  return 0;
}
vladr
fonte
8
por que você misturou mult e div? Não deveria ser interessante se mult é talvez (ou esperado?) Muito mais rápido que div?
Kyss Tao
13
A multiplicação é muito mais rápida do que a divisão em casos de inteiros e de ponto flutuante. O desempenho da divisão depende também do tamanho dos números. Eu geralmente suponho que a divisão seja aproximadamente 15 vezes mais lenta.
Sogartar
4
pastebin.com/Kx8WGUfg Peguei seu benchmark e separei cada operação em seu próprio loop e adicionei volatilepara ter certeza. Em Win64, o FPU é não utilizado e MSVC não irá gerar código para ele, então ele compila usando mulsse divssinstruçõ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 GHz
James Dunne
4
@JamesDunne apenas tenha cuidado, pois os ops fp vatingirã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.
vladr
3
Este "benchmark" não possui paralelismo de dados para execução fora de ordem, pois todas as operações são feitas com o mesmo acumulador ( v). Em designs recentes da Intel, a divisão não é canalizada ( divss/ divpstem latência de 10-14 ciclos e a mesma taxa de transferência recíproca). mulssno 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).
Peter Cordes
23

É 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.

Ben Voigt
fonte
4
+1 para apontar como benchmarks ingênuos podem produzir loops de tempo 0 por causa de operações inteiras constantes desenroladas. Além disso, o compilador pode descartar completamente o loop (inteiro ou FP) se o resultado não for realmente usado.
vladr
A conclusão disso é: deve-se chamar uma função tendo a variável de loop como argumento. Uma vez que acho que nenhum compilador seria capaz de ver que a função não faz nada e que a chamada pode ser ignorada. Como há uma sobrecarga de chamada, apenas as diferenças de tempo == (tempo flutuante - tempo inteiro) serão significativas.
GameAlchemist
@GameAlchemist: Muitos compiladores eliminam chamadas para funções vazias, como um efeito colateral do inlining. Você tem que fazer um esforço para evitar isso.
Ben Voigt,
O OP parecia que ele estava falando sobre o uso de inteiros para coisas em que FP seria um ajuste mais natural, então seria necessário mais código de inteiros para obter o mesmo resultado que o código de FP. Nesse caso, basta usar FP. Por exemplo, em hardware com FPU (por exemplo, uma CPU de desktop), os decodificadores MP3 inteiros de ponto fixo são mais lentos (e um pouco mais erros de arredondamento) do que os decodificadores de ponto flutuante. Implementações de ponto fixo de codecs existem principalmente para rodar em CPUs ARM reduzidas sem hardware FP, apenas FP emulado lento.
Peter Cordes
um exemplo para o primeiro ponto: em x86-64 com AVX-512, há apenas 16 registros GP, mas 32 registros zmm, então a matemática escalar de ponto flutuante pode ser mais rápida
phuclv
18

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.

Potatoswatter
fonte
Sim, assim como a conversão de um inteiro rand para um float na versão de ponto flutuante. Alguma idéia de uma maneira melhor de testar isso?
maxpenguin
1
Se você está tentando traçar um perfil de velocidade, veja o POSIX timespec_tou algo semelhante. Registre a hora no início e no final do loop e faça a diferença. Em seguida, mova a randgeraçã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.
Mike D.
3
@maxpenguin: a questão é o que você está testando. Artem presumiu que você está fazendo gráficos, Carl considerou se você está em uma plataforma embarcada sem FP, suponho que você está codificando ciência para um servidor. Você não pode generalizar ou "escrever" benchmarks. Os benchmarks são uma amostra do trabalho real que seu programa faz. Uma coisa que posso dizer é que não permanecerá "essencialmente a mesma velocidade" se você tocar no elemento crítico de desempenho em seu programa, seja ele qual for.
Potatoswatter
bom ponto e boa resposta. Temos uma extensa base de código atualmente. Na verdade, me deparei com a generalização de que "não devemos usar float, pois o cálculo de inteiros é 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 pela sua ajuda.
maxpenguin
18

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

short add: 0.822491
short sub: 0.832757
short mul: 1.007533
short div: 3.459642
long add: 0.824088
long sub: 0.867495
long mul: 1.017164
long div: 5.662498
long long add: 0.873705
long long sub: 0.873177
long long mul: 1.019648
long long div: 5.657374
float add: 1.137084
float sub: 1.140690
float mul: 1.410767
float div: 2.093982
double add: 1.139156
double sub: 1.146221
double mul: 1.405541
double div: 2.093173

Intel i3 2370M tem resultados semelhantes

short add: 1.369983
short sub: 1.235122
short mul: 1.345993
short div: 4.198790
long add: 1.224552
long sub: 1.223314
long mul: 1.346309
long div: 7.275912
long long add: 1.235526
long long sub: 1.223865
long long mul: 1.346409
long long div: 7.271491
float add: 1.507352
float sub: 1.506573
float mul: 2.006751
float div: 2.762262
double add: 1.507561
double sub: 1.506817
double mul: 1.843164
double div: 2.877484

Intel (R) Celeron (R) 2955U (Acer C720 Chromebook executando xenial)

short add: 1.999639
short sub: 1.919501
short mul: 2.292759
short div: 7.801453
long add: 1.987842
long sub: 1.933746
long mul: 2.292715
long div: 12.797286
long long add: 1.920429
long long sub: 1.987339
long long mul: 2.292952
long long div: 12.795385
float add: 2.580141
float sub: 2.579344
float mul: 3.152459
float div: 4.716983
double add: 2.579279
double sub: 2.579290
double mul: 3.152649
double div: 4.691226

DigitalOcean 1 GB Droplet Intel (R) Xeon (R) CPU E5-2630L v2 (em execução confiável)

short add: 1.094323
short sub: 1.095886
short mul: 1.356369
short div: 4.256722
long add: 1.111328
long sub: 1.079420
long mul: 1.356105
long div: 7.422517
long long add: 1.057854
long long sub: 1.099414
long long mul: 1.368913
long long div: 7.424180
float add: 1.516550
float sub: 1.544005
float mul: 1.879592
float div: 2.798318
double add: 1.534624
double sub: 1.533405
double mul: 1.866442
double div: 2.777649

Processador AMD Opteron (tm) 4122 (preciso)

short add: 3.396932
short sub: 3.530665
short mul: 3.524118
short div: 15.226630
long add: 3.522978
long sub: 3.439746
long mul: 5.051004
long div: 15.125845
long long add: 4.008773
long long sub: 4.138124
long long mul: 5.090263
long long div: 14.769520
float add: 6.357209
float sub: 6.393084
float mul: 6.303037
float div: 17.541792
double add: 6.415921
double sub: 6.342832
double mul: 6.321899
double div: 15.362536

Este usa o código de http://pastebin.com/Kx8WGUfg comobenchmark-pc.c

g++ -fpermissive -O3 -o benchmark-pc 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):

Gráfico dos dados acima

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)
    short add: 0.773049
    short sub: 0.789793
    short mul: 0.960152
    short div: 3.273668
      int add: 0.837695
      int sub: 0.804066
      int mul: 0.960840
      int div: 3.281113
     long add: 0.829946
     long sub: 0.829168
     long mul: 0.960717
     long div: 5.363420
long long add: 0.828654
long long sub: 0.805897
long long mul: 0.964164
long long div: 5.359342
    float add: 1.081649
    float sub: 1.080351
    float mul: 1.323401
    float div: 1.984582
   double add: 1.081079
   double sub: 1.082572
   double mul: 1.323857
   double div: 1.968488
Processador AMD Opteron (tm) 4122 (preciso, hospedagem compartilhada DreamHost)
    short add: 1.235603
    short sub: 1.235017
    short mul: 1.280661
    short div: 5.535520
      int add: 1.233110
      int sub: 1.232561
      int mul: 1.280593
      int div: 5.350998
     long add: 1.281022
     long sub: 1.251045
     long mul: 1.834241
     long div: 5.350325
long long add: 1.279738
long long sub: 1.249189
long long mul: 1.841852
long long div: 5.351960
    float add: 2.307852
    float sub: 2.305122
    float mul: 2.298346
    float div: 4.833562
   double add: 2.305454
   double sub: 2.307195
   double mul: 2.302797
   double div: 5.485736
Intel Xeon E5-2630L v2 a 2,4 GHz (Trusty 64 bits, DigitalOcean VPS)
    short add: 1.040745
    short sub: 0.998255
    short mul: 1.240751
    short div: 3.900671
      int add: 1.054430
      int sub: 1.000328
      int mul: 1.250496
      int div: 3.904415
     long add: 0.995786
     long sub: 1.021743
     long mul: 1.335557
     long div: 7.693886
long long add: 1.139643
long long sub: 1.103039
long long mul: 1.409939
long long div: 7.652080
    float add: 1.572640
    float sub: 1.532714
    float mul: 1.864489
    float div: 2.825330
   double add: 1.535827
   double sub: 1.535055
   double mul: 1.881584
   double div: 2.777245
MrMesees
fonte
gcc5 talvez auto-vetorize algo que o gcc4.6 não fez? Está benchmark-pcmedindo 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).
Peter Cordes
E BTW, por que não testa int, apenas shorte long? No Linux x86-64, shorté de 16 bits (e, portanto, tem lentidão de registro parcial em alguns casos), enquanto longe long longsão do tipo 64 bits. (Talvez tenha sido projetado para Windows em que o x86-64 ainda usa 32 bits long? Ou talvez tenha sido projetado para o modo de 32 bits.) No Linux, o x32 ABI tem 32 bits longno modo de 64 bits , portanto, se você tiver as bibliotecas instaladas , use gcc -mx32para compilador para ILP32. Ou apenas use -m32e veja os longnúmeros.
Peter Cordes
E você realmente deve verificar se o seu compilador vetorizou algo automaticamente. por exemplo, usando addpsem registros xmm ao invés de addss, para fazer 4 FP adiciona em paralelo em uma instrução que é tão rápida quanto escalar addss. (Use -march=nativepara permitir o uso de quaisquer conjuntos de instruções que sua CPU suporte, não apenas a linha de base SSE2 para x86-64).
Peter Cordes
@cincodenada, por favor, deixe os gráficos mostrando os 15 completos ao lado, pois é uma ilustração do desempenho.
MrMesees
@PeterCordes Vou tentar olhar amanhã, obrigado pelo seu empenho.
MrMesees
7

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.

jcoder
fonte
Deve-se observar que no Win64 as instruções FPU não são mais geradas pelo compilador MSVC. O ponto flutuante está sempre usando instruções SIMD lá. Isso cria uma grande discrepância de velocidade entre Win32 e Win64 em relação aos flops.
James Dunne
5

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.

Goran D
fonte
4

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

Artem Sokolov
fonte
2
Aplicações científicas usam FP. A única vantagem do FP é que a precisão é invariável em escala. É como notação científica. Se você já conhece a escala dos números (por exemplo, que o comprimento da linha é um número de pixels), FP é evitado. Mas antes de traçar a linha, isso não é verdade.
Potatoswatter
4

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.

gnasher729
fonte
Há muita especulação selvagem aqui, sem levar em conta nenhum dos efeitos secundários presentes no hardware, que geralmente dominam o tempo de computação. Não é um ponto de partida ruim, mas precisa ser verificado em cada aplicativo específico por meio de criação de perfil, e não ensinado como evangelho.
Ben Voigt
3

Fiz um teste que acabou de adicionar 1 ao número em vez de rand (). Os resultados (em um x86-64) foram:

  • curto: 4.260s
  • int: 4.020s
  • long long: 3.350s
  • float: 7.330s
  • duplo: 7,210s
dan04
fonte
1
Fonte, opções de compilação e método de tempo? Estou um pouco surpreso com os resultados.
GManNickG
Mesmo loop que OP com "rand ()% 365" substituído por "1". Sem otimização. Tempo do usuário a partir do comando "tempo".
dan04
13
"Sem otimização" é a chave. Você nunca perfila com a otimização desligada, sempre perfila no modo "release".
Dean Harding,
2
Nesse caso, porém, a otimização fora força a operação a ocorrer, e é feita deliberadamente - o loop existe para dilatar o tempo para uma escala razoável de medição. Usar a constante 1 remove o custo de rand (). Um compilador de otimização suficientemente inteligente veria 1 adicionado 100 milhões de vezes sem saída do loop e simplesmente adicionaria 100000000 em uma única operação. Isso meio que contorna todo o propósito, não é?
Stan Rogers,
7
@Stan, torne a variável volátil. Até mesmo um compilador de otimização inteligente deve honrar as operações múltiplas.
vladr
0

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.

James Curran
fonte
1
Por favor, considere olhar para isso novamente oferecendo mais do que opinião (especialmente considerando que a opinião parece ir contra os fatos coletados)
MrMesees
1
@MrMesees Embora essa resposta não seja muito útil, eu diria que é consistente com os testes que você fez. E as curiosidades históricas provavelmente também estão bem.
Jonatan Öström
Como alguém que trabalhou com 286s naquela época, posso confirmar; "Sim, eles eram!"
David H Parry