Jump to content
Sign in to follow this  
fredericopissarra

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

 Read 1 minute

Recommended Posts

 Read 1 minute

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 by fredericopissarra
  • Curtir 1

Share this post


Link to post
Share on other sites
 Read less than a minute

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

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...