Ir para conteúdo

Assembly nem sempre é a solução...


fredericopissarra

Posts Recomendados

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 para o comentário
Compartilhar em outros sites

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 para o comentário
Compartilhar em outros sites

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

  • Quem Está Navegando   0 membros estão online

    • Nenhum usuário registrado visualizando esta página.
×
×
  • Criar Novo...