fredericopissarra Posted September 19, 2019 at 12:07 PM Share Posted September 19, 2019 at 12:07 PM 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]. Link to comment Share on other sites More sharing options...
fredericopissarra Posted September 19, 2019 at 12:13 PM Author Share Posted September 19, 2019 at 12:13 PM PS: Quem quiser, pode baixar meu header cycle_counting.h aqui: cycle_counting.h no meu sandbox, no Gitlab. Link to comment Share on other sites More sharing options...
fredericopissarra Posted September 19, 2019 at 01:36 PM Author Share Posted September 19, 2019 at 01:36 PM 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)... Link to comment Share on other sites More sharing options...
fredericopissarra Posted September 20, 2019 at 12:23 PM Author Share Posted September 20, 2019 at 12:23 PM 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! Link to comment Share on other sites More sharing options...
Recommended Posts
Archived
This topic is now archived and is closed to further replies.