static_cast<int>(double_var);

Pois então, continuando o assunto do assembly do post anterior…

Segundo lia em vários lugares, truncar um double para um int é uma operação lenta na arquitetura x86, apesar de ser uma operação básica das linguagens C e C++. O interessante é que arredondar um número é mais rápido porque as flags do processador ficam por default no modo de arredondamento (depois de muito pesquisar, parece que esse default é pra obter mais precisão nas operações em geral, pois não é só a conversão pra int que faz uso dessa flag).

Então achei que esse seria um bom motivo pra aprender um pouco mais de assembly, já que o ganho de desempenho parecia ser significativo e alternativas usando construções de alto nível são esparsas: tem o lrint do C99, tem bibliotecas externas com uma função round, como o cvRound da OpenCV que usam assembly, mas nada suficientemente bom (você instalaria uma biblioteca só pra ter uma função round? é melhor copiar a função direto). E faria as medidas com outra instrução assembly, a rdtsc.

As funções que eu usei são as seguintes (se acharem algum erro, me avisem; assembly não é minha especialidade, hehe):

Pra obter o tempo atual:

/* O VC++ 2002 não aceita long long, vai __int64 mesmo... */
__declspec(naked) unsigned __int64 get_rdtsc()
{

__asm
{

push ebx;
xor eax, eax;
cpuid;
rdtsc;
pop ebx;
ret;

}

}

Pra fazer 400 conversões de double pra int em assembly, com fld seguido de fistp:

__declspec(naked)
unsigned __int64 asm_double_to_int(...)
{

__asm
{

/* Preferi fazer toda a manipulação da pilha à mão (por isso o __declspec(naked)) */
push ebp;
mov ebp, esp;
sub esp, 12;

call get_rdtsc;
mov dword ptr [ebp-8], eax;
mov dword ptr [ebp-4], edx;

mov ecx, 100; // Loop counter
loop_label:
fld qword ptr [ebp+8];
fistp dword ptr [ebp-12];

fld qword ptr [ebp+8];
fistp dword ptr [ebp-12];

fld qword ptr [ebp+8];
fistp dword ptr [ebp-12];

fld qword ptr [ebp+8];
fistp dword ptr [ebp-12];

sub ecx, 1;
test ecx, ecx;
jnz loop_label;

call get_rdtsc;

/*
A subtração de valores 64 bits eu tirei daqui:
win32-assembly-cheat-sheet
*/

sub eax, dword ptr [ebp-8];
sbb edx, dword ptr [ebp-4];

mov esp, ebp;
pop ebp;
ret;

}

}

Pra fazer 400 casts de double pra int em C++, com o mesmo tipo de loop feito:

/* Com as variáveis volatile o otimizador não remove código inútil... */
unsigned __int64 cast_double_to_int(volatile double a)
{

int i = 100;
unsigned __int64 t1 = get_rdtsc();
volatile int j;
do{

j = static_cast<int>(a);
j = static_cast<int>(a);
j = static_cast<int>(a);
j = static_cast<int>(a);

}while(--i);
unsigned __int64 t2 = get_rdtsc();
return t2 - t1;

}

O código do main está abaixo. Testo o arredondamento de 2 números (5.5 e 1.125) repetindo cada teste 8 vezes.

int main(int argc, char ** argv)
{

const int reps = 8;
unsigned __int64 tempo;
cout << "Assembly (default rounding):\nTest 1:\n";
for(int i = 0; i < reps; ++i){

tempo =
asm_double_to_int(5.5);
cout << tempo << '\n';

}
cout << "Test 2:\n";
for(int i = 0; i < reps; ++i){

tempo =
asm_double_to_int(1.125);
cout << tempo << '\n';

}
cout << "---------------------------------\nC++ cast (truncating):\nTest 1:\n";
for(int i = 0; i < reps; ++i){

tempo =
cast_double_to_int(5.5);
cout << tempo << '\n';

}
cout << "Test 2:\n";
for(int i = 0; i < reps; ++i){

tempo =
cast_double_to_int(1.125);
cout << tempo << '\n';

}
return 0;

}

Então vamos aos resultados. Fiz os testes no Visual C++ .NET 2002 e no Visual C++ .NET 2005 Express. Não testei com o GCC porque a sintaxe de inline assembly é meio esquisita e eu não aprendi direito. Testei em duas máquinas. Uma é um Intel Pentium 4 Northwood 2.4 GHz e o outro é um AMD Sempron Mobile 3000. Os resultados são em número de ciclos, não em segundos. Quanto mais ciclos, mais lento, mas só se a comparação for feita com outra execução no mesmo processador. A compilação é em modo Release (isto é, com otimizações).

VC++ 2002, Pentium 4 2.4GHz:


Assembly (default rounding):
Test 1:
2440
1540
1424
1424
1460
1424
1424
1460
Test 2:
1460
1460
1424
1424
1460
1424
1424
1424
---------------------------------
C++ cast (truncating):
Test 1:
26440
26236
25904
25904
26440
26440
25472
26440
Test 2:
26440
26452
25904
26440
25472
26808
25580
25904

========================

VC++ 2005, Pentium 4 2.4 GHz:


Assembly (default rounding):
Test 1:
2460
1412
1428
1412
1420
1412
1420
1420
Test 2:
1412
1420
1412
1424
1412
1412
1420
1412
---------------------------------
C++ cast (truncating):
Test 1:
8760
6896
6904
6900
6904
6904
7560
6892
Test 2:
6900
7248
6900
6904
6904
6904
6904
6904

========================
========================

VC++2002, Sempron Mobile 3000:


Assembly (default rounding):
Test 1:
992
877
877
877
877
877
877
877
Test 2:
877
877
877
877
877
877
877
877
---------------------------------
C++ cast (truncating):
Test 1:
8281
7366
7330
7294
7294
7294
7294
7294
Test 2:
7294
7294
7294
7294
7294
7294
7294
7294

========================

VC++2005, Sempron Mobile 3000:


Assembly (default rounding):
Test 1:
773
708
708
689
689
689
689
689
Test 2:
716
689
689
689
689
689
689
689
---------------------------------
C++ cast (truncating):
Test 1:
4902
4678
4678
4647
4922
4922
4922
4922
Test 2:
4695
4922
4922
4922
4922
4922
4922
4922

Conclusões:

1 – Dá pra ver que o Pentium 4 precisa de mais ciclos pra fazer a mesma coisa, por isso os Pentium 4 tinham freqüências maiores, pra compensar isso. (eu disse “tinham” porque a geração do Pentium 4 já passou, hehe)

2 – O VC++2002 executa o truncamento chamando uma função que arredonda e, se necessário faz uma subtração pra transformar o arredondamento em truncamento. O VC++2005 chama uma função que trunca usando uma instrução SSE2 (cvttsd2si) do processador, que é bem mais rápida, como dá pra ver. O programa decide dinamicamente se o SSE2 está disponível e usa a instrução em caso afirmativo. Nos testes, os 2 processadores tinham SSE2.

3 – Não importa qual seja a técnica usada pelo compilador, nem o processador, arredondar em assembly sempre resulta em menos instruções e maior desempenho do que truncar com um cast. Isso é interessante de ser feito quando você precisa de um int a partir de um double e não importa se vai truncar ou arredondar. Por exemplo, quando coordenadas em double precisam ser convertidas pra coordenadas de pixels, a diferença é minúscula, ainda mais se for uma animação. Outro caso é a linguagem Lua, que usa bastante a conversão de double pra int (pois todas as variáveis numéricas em Lua são double) e por isso usa este tipo de truque pra fazer a conversão, já que tanto truncar quanto arredondar servem.

Se você tem um loop interno fazendo muito dessas conversões de double pra int, convém considerar usar uma funçãozinha em assembly com fld e fistp como esta:

inline int double_to_int(double val)
{

int t;
__asm
{

fld val;
fistp t;

}
return t;

}

4 – Naturalmente, o assunto não está esgotado, pois existem muitos outros tipos de processador pra comparar, compiladores (que resultados será que o GCC e o compilador Intel gerariam?) e maneiras de truncar doubles, como por exemplo: fazer como o VC++2002 (com arredondamento e alguns testes); como VC++2005 (com SSE2); setando as flags do processador pra truncar (ruim porque exige esvaziar o pipeline, se os compiladores não geraram isso, deve ser lento, hehe); ou usar a instrução fisttp do SSE3 (sei que o meu Pentium 4 não a suporta; não tenho muita idéia do seu desempenho). Pra arredondar também deve haver mais algumas alternativas… Além de tudo isso poderia fazer experimentos setando as opções do compilador pra usar “fast math” e ver que código o compilador gera, mas daí eu não ia terminar nunca este post!!

Anúncios

11 pensamentos sobre “static_cast<int>(double_var);

  1. Bah, cansei de escrever formatar este post! Tive que usar blockquote pra indentar o código, demorei a descobrir como rodar um programa feito em VC++2005 num computador sem o Visual C++, e é claro, tive que fazer todos os testes… Isso me lembra o mestrado! hahaha.

  2. Haha!! Voltou a agir como um mestre =P

    Eu gostei dos resultados… vi que tenho que estudar assembly =), e vou adicionar agora essa função pra minha fast_math, depois comento o que ganhei de desempenho.

  3. O que eu não entendi foi como o mesmo código assembly teve desempenhos diferentes no VisualC++ 2002 e no 2005 (executando no Sempron). Se é assembly, não devia dar no mesmo?

    Obs.: Ainda estou consertando a formatação do código…

  4. > depois comento o que ganhei de desempenho.

    Com isso acho que tu vai conseguir colocar pelo menos mais 1 boneco na simulação =D hahahaha. Bom, tomara que seja mais do que isso! =)

    Também tem que ver quanto de diferença isso dá em código “real”, porque repetir o mesmo cast várias vezes sobre as mesmas variáveis é muito falso e nenhum programa faz isso, hehehehehe.

  5. Mais uma observação: O ponto-e-vírgula depois de cada instrução assembly na verdade introduz um comentário até o fim da linha, mas eu usei assim mesmo porque o ponto-e-vírgula faz o Visual Studio indentar certo a próxima linha. :-)

  6. Tu disse que teve desempenhos diferentes no vc2002 e no 2005. Eu não terminei de ler o texto, mas cheguei a ver que tu usou versão relese… Tu usou otimização? os compiladores podem ter alterado algo…
    Se falei merda.. desconsidere.

  7. Pois é, mas o que o compilador poderia otimizar numa função escrita inteiramente em assembly? O máximo que eu imaginei foi alguma espécie de alinhamento de memória do código ou da pilha (ou algo bem simples semelhante a isso) que não interferisse nas instruções em assembly… Sei lá.

    Ah, só um outro ponto a considerar, que não custa mencionar: Usando assembly você perde muita portabilidade (vejam que não pude compilar esse código no gcc, ainda que fosse para o mesmo sistema operacional). Portanto, é sempre bom ter uma função de reserva que seja portável (ainda que mais lenta) para o caso de precisar compilar o código em outro compilador ou em outro sistema.

  8. Marcus, have you tried truncation tricks by Agner Fog?

    http://agner.org/optimize/#manuals , file optimizing_assembly.pdf, chapter 17.5 “Converting from floating point to integer”.

    He proposes a branchless truncation method, so you can keep compatibility with C/C++ and get a faster code.

    (I’m sorry that I’m commenting in English; unfortunately, I don’t speak Portuguese.)

  9. Hi Peter, thanks for your comment (i don’t mind it is in English!)

    I am aware of Agner Fog’s tutorials, but I need more time to fully understand all the techniques he presents. I will study it again the next time i try to benchmark something :-)

  10. The sad part is when you really want rounding, but cannot use it portably because most standard libraries do not support it (lrint is C99-only). If you use a trick like “int a = f + 0.5” to use truncation as rounding, you are losing performace unnecessarily.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s