fredericopissarra Posted September 19, 2019 Share Posted September 19, 2019 (edited) Eis um exemplo de que a ideia de que um código escrito em assembly é mais rápido do que um escrito em outra linguagem muitas vezes não é verdadeira. No exemplo abaixo em preencho um array de 1000 inteiros (32 bits) com valores aleatórios. Isso coloca 4000 bytes no cache L1d. Daí, em chamo as rotinas suma(), que é o equivalente em assembly da rotina sumc(), essa última escrita em C, medindo a quantidade de ciclos gastos na soma de todos os inteiros do array. Faço o mesmo com a rotina sumc()... Eis o resultado em uma de minhas máquinas de teste (i5-3570 @ 3.4 GHz): $ ./test sum (asm) = 1032606550591 (4556 ciclos) sum (c) = 1032606550591 (3094 ciclos) Yep... nossa rotina em C é 32% mais rápida que a rotina em assembly! Eis o código: /* test.c */ /* Compilar com: $ gcc -O2 -o test test.c */ #include <stdio.h> #include <stdlib.h> #include <time.h> #include <stdint.h> #include <inttypes.h> /* Não é absolutamente necessário, já que não escreveemos nada no array durante a medição. */ #define SYNC_MEM /* Minhas duas rotinas de contagem de ciclos */ #include "cycle_counting.h" /* Uso isso só para exportar as funções, para verificar o código gerado com objdump... */ #ifndef NDEBUG #define STATIC #else #define STATIC static #endif /* Quantidade de inteiros no array de teste */ #define MAX_ITEMS 1000 __attribute__ ( ( noinline ) ) STATIC int64_t suma ( int32_t *, size_t ); __attribute__ ( ( noinline ) ) STATIC int64_t sumc ( int32_t *, size_t ); int main ( void ) { static int32_t array[MAX_ITEMS]; int32_t *ptr; int n; int64_t sum1, sum2; counter_T c1, c2; // preenche o array com valores aleatórios. srand ( time ( NULL ) ); n = MAX_ITEMS; ptr = array; while ( n-- ) *ptr++ = rand(); c1 = BEGIN_TSC(); sum1 = suma ( array, MAX_ITEMS ); END_TSC ( &c1 ); c2 = BEGIN_TSC(); sum2 = sumc ( array, MAX_ITEMS ); END_TSC ( &c2 ); printf ( "sum (asm) = %" PRIi64 " (%" PRIu64 " ciclos)\n" "sum (c) = %" PRIi64 " (%" PRIu64 " ciclos)\n", sum1, c1, sum2, c2 ); } /* Essa rotina é, essencialmente, a mesma coisa que sumc(), mas em asm. */ int64_t suma ( int32_t *ptr, size_t size ) { long sum; __asm__ __volatile__ ( "xorl %%edx,%%edx\n" /* Acumulador é zerado */ "testq %2,%2\n\t" /* Testa se o contador é zero... */ "1:\n\t" "jz 2f\n\t" /* Sai do loop se o contador é zero */ "movslq (%1),%%rax\n\t" /* Pega o inteiro do array */ "addq %%rax,%%rdx\n\t" /* Acumula */ "addq $4,%1\n\t" /* Avança o ponteiro */ "subq $1,%2\n\t" /* Decrementa o contador */ "jmp 1b\n" /* Continua acumulando... */ "2:\n\t" "movq %%rdx,%0\n\t" : /* Copia o acumulador para 'sum'. */ "=g" ( sum ) : /* GCC alocará sum em RAX, mas não confio nisso! */ "D" ( ptr ), "S" ( size ) : /* Usei RDI e RSI por causa da convenção de chamada x86-64. */ "%rdx" ); return sum; } int64_t sumc ( int32_t *ptr, size_t size ) { long sum; sum = 0; while ( size-- ) sum += *ptr++; return sum; } Geralmente o GCC e o CLANG fazem um trabalho melhor que um que possa ser feito manualmente. Porquê? Existem vários efeitos de cache, TLBs, emparelhamento de instruções, alinhamentos, fusão de instruções (micro e macro), tamanho de microcódigo, etc, etc, etc... que o GCC toma conta pra você durante a otimização. Até mesmo manipulação de dados podem ser paralelizadas (via SSE, por exemplo) [pode ser que seja feito com as opções -ftree-vectorize ou -O3 -march=native]. Edited September 27, 2019 by fredericopissarra 1 Quote Link to comment Share on other sites More sharing options...
fredericopissarra Posted September 19, 2019 Author Share Posted September 19, 2019 PS: Quem quiser, pode baixar meu header cycle_counting.h aqui: cycle_counting.h no meu sandbox, no Gitlab. Quote Link to comment Share on other sites More sharing options...
fredericopissarra Posted September 19, 2019 Author Share Posted September 19, 2019 Bem... mas, às vezes, Assembly pode ser a solução... Eis uma rotina que é mais rápida que a rotina da glibc: /* Pela convenção de chamada SysV ABI x86-64, RDI=dptr, RSI=sptr */ char *strcpy_( char *dptr, char *sptr ) { __asm__ __volatile__ ( "movq %%rdi,%%rdx\n\t" /* salva RDI (stosb altera RDI) */ "1:\n\t" "lodsb\n\t" "stosb\n\t" "testb %%al,%%al\n\t" "jnz 1b\n\t" "movq %%rdx,%%rdi\n\t" /* Recupera RDI */ : : "D" (dptr), "S" (sptr) : "%rax", "%rdx" ); return dptr; } Comparando uma chamada a essa função contra a função strcpy() original, obtenho cerca de 80% (medido!) de ganho de performance num i5-3570 (Haswell)... Quote Link to comment Share on other sites More sharing options...
fredericopissarra Posted September 20, 2019 Author Share Posted September 20, 2019 PS: "Fred, porque você não usou MOVSB?"... É que MOVSB não afeta os registradores. e preciso verifiacar o NUL char ao copiar uma string! Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.