Cuidado com uso de ponteiros

Este post mostra um exemplo da importância de se conhecer sobre hardware para desenvolver bons softwares, mostrando um problema que se tem com ponteiros quando necessitamos do melhor desempenho possível.

Vamos analisar um trecho do código postado em Threads (parte 2). Para você não precisar ler aquele post para entender este, vou resumir: naquele código, precisávamos fazer o seguinte processo:

resultado = √1 – √2 + √3 – … + √999999999

Porém, a função em que esse trecho era executado não podia ser uma função qualquer. Ela tinha obrigatoriamente que seguir o protótipo “void* nomedafuncao(void* parametro)”. Isso me levou a inicialmente desenvolvê-la da seguinte maneira:

void* thread_call(void* param)
{
  // Utilizamos a variável teste apenas para simplificar o código 
  thread_param_t* teste = (thread_param_t*)param;
  int i;

  teste->value = 0;
  for (i = teste->i_min; i<i_max; i++)
  {
    if (i%2 == 1) // Se o resto de i/2 for 1 (ou seja, i é impar)
      teste->value += sqrt(i); // Soma raiz de i
    else
      teste->value -= sqrt(i); // Subtrai raiz de i
  }
}

Já que o parâmetro é um ponteiro, faz sentido que trabalhamos diretamente com ponteiro (substituindo o “.” por “->”). Ainda mais pois trabalhar com ponteiros é muito comum em C++, e é necessário na maioria das linguagens gerenciadas, como Java, C#, etc. Porém, quando focamos no desempenho, precisamos reconsiderar essas “manias”. Por que?

As nossas variáveis podem ser guardadas em vários tipos de memórias. Por ordem de velocidade, são basicamente: registradores, cache, RAM e HD. Porém, tenha uma coisa em mente: nós não temos como criar um ponteiro para um registrador. Portanto, sempre que criamos uma variável, e depois acessamos o endereço dessa variável de alguma forma, o compilador simplesmente não tem como utilizar essa variável como um registrador. Ele vai colocá-la na memória RAM. E se você faz 1000000000 de acessos a essa variável, tenha certeza que seu programa será muito mais lento se essa variável estiver na memória RAM.
A resposta para isso é criar uma variável local, no começo da função copiar o conteúdo do ponteiro para a local, mexer apenas na local e só no fim da função é que copiamos o resultado para o ponteiro.

void* thread_call(void* param)
{
  // Copiamos o parâmetro para uma variável local (não-ponteiro)
  thread_param_t teste = *(thread_param_t*)param;
  int i;

  // Realizamos o mesmo algoritmo que tínhamos feito no 1º programa
  teste.value = 0;
  for (i = teste.i_min; i < teste.i_max; i++)
  {
    if (i%2 == 1) // Se o resto de i/2 for 1 (ou seja, i é impar)
      teste.value += sqrt(i); // Soma raiz de i
    else
      teste.value -= sqrt(i); // Subtrai raiz de i
  }
  
  // Copiamos o resultado no parâmetro original
  ((thread_param_t*)param)->value = teste.value;
}

Vejam o que a diferença é muito pequena. Foi só trocar um asterisco de lugar na declaração da variável, substituir os “->” por ponto, e adicionar uma linha que copia o valor obtido para o ponteiro. É uma diferença que em uma rápida olhada poderíamos achar até que piora o desempenho, mas que na verdade traz uma melhora grande.

Diferença nos tempos de execução no meu PC (os tempos aqui mostrados são cada um os melhores resultados de 5 execuções seguidas)

Usando ponteiro, sem otimizações: 10.137s
Usando ponteiro, com otimizações -O3 -O2: 7.178s
Usando não-ponteiro, com otimizações: 4.969s
Usando não-ponteiro, sem otimizações: 4.997s (diferença pouco significativa)

Conclusões: A melhora de desempenho comparando com o código usando ponteiro com otimizações foi de 44%. Isso pois provavelmente meu PC conseguiu colocar o ponteiro na memória cache. No PC do meu trabalho demorava 16 segundos usando não ponteiro e single-threaded (usando 1 núcleo), e quando tentei fazer multi-threaded passou a demorar mais de 1 minuto (com ponteiro, provavelmente sendo alocado na RAM). Demorei um tempo para pensar nisso, e com essa pequena modificação passou a fazer em apenas 4 segundos (multi-threaded). Isso me motivou a fazer estes 3 posts mais recentes.

O programa completo, caso você queira testar no seu computador:

#include <stdio.h>
#include <math.h>
#include <pthread.h>

/* Precimos definir uma struct, porque a função
* thread_call só pode aceitar 1 apontador como
* parametro, e precisamos passar 3 valores
*/
typedef struct
{
	int i_min;    // a partir de onde fazer o cálculo
	int i_max;    // até onde fazer o cálculo
	double value; // valor do cálculo parcial
} thread_param_t;

/*  Esta função será chamada cada vez que uma Thread for criada
*/
void* thread_call(void* param)
{
  // Copiamos o parâmetro para uma variável local
  thread_param_t teste = *(thread_param_t*)param;
  int i;

  // Realizamos o mesmo algoritmo que tínhamos feito no 1º programa
  teste.value = 0;
  for (i = teste.i_min; i < teste.i_max; i++)
  {
    if (i%2 == 1) // Se o resto de i/2 for 1 (ou seja, i é impar)
      teste.value += sqrt(i); // Soma raiz de i
    else
      teste.value -= sqrt(i); // Subtrai raiz de i
  }
  
  // Copiamos o resultado no parâmetro original
  ((thread_param_t*)param)->value = teste.value;
}

int main()
{
  // Estas variáveis serão a identificação de cada Thread
  pthread_t thread1, thread2, thread3;
  // Estas serão responsáveis por distribuir o processamento
  thread_param_t param0, param1, param2, param3;
  
  param0.i_min =          1;
  param0.i_max =  250000000;
  param1.i_min =  250000000;
  param1.i_max =  500000000;
  param2.i_min =  500000000;
  param2.i_max =  750000000;
  param3.i_min =  750000000;
  param3.i_max = 1000000000;
  
  // Criamos as threads
  pthread_create(&thread1, NULL, thread_call, (void*)&param1);
  pthread_create(&thread2, NULL, thread_call, (void*)&param2);
  pthread_create(&thread3, NULL, thread_call, (void*)&param3);
  // E por fim, a Thread padrão chama a função como normalmente
  thread_call((void*)&param0);

  // Aguardamos que as outras threads terminem
  pthread_join(thread1, NULL);
  pthread_join(thread2, NULL);
  pthread_join(thread3, NULL);

  // Somamos as parcelas
  param0.value += param1.value + param2.value + param3.value;
  printf("%f\n", param0.value); // Imprime o resultado
  return 0;
}

Publicado em Computação | 2 Comentários

Threads em C (parte 2)

Na parte 1 deste tutorial, vimos o problema de fazer um programa com cálculo demorado em um programa comum (sem Threads), e uma breve introdução de como as Threads funcionam. Agora vamos analisar esse programa melhorado de forma a torná-lo multi-thread, e ver as diferenças de desempenho.

Vamos utilizar as seguintes funções:

int pthread_create(pthread_t* thread, pthread_attr_t * attr,
                   void *(*start_routine)(void*),
                   void* arg); // Cria uma Thread (equivalente
                               // ao start_thread que eu havia comentado)

int pthread_join(pthread_t thread,
                 void **value_ptr);  // Espera que uma thread termine
                                     // (veremos sua utilidade)

As especificações: http://linux.die.net/man/3/pthread_create

Sigam os comentários do código-fonte. Notem como ficou absurdamente maior.

#include <stdio.h>
#include <math.h>
#include <pthread.h>

/* Precimos definir uma struct, porque a função
* thread_call só pode aceitar 1 apontador como
* parametro, e precisamos passar 3 valores
*/
typedef struct
{
	int i_min;    // a partir de onde fazer o cálculo
	int i_max;    // até onde fazer o cálculo
	double value; // valor do cálculo parcial
} thread_param_t;

/*  Esta função será chamada cada vez que uma Thread for criada
*/
void* thread_call(void* param)
{
  // Copiamos o parâmetro para uma variável local
  thread_param_t teste = *(thread_param_t*)param;
  int i;

  // Realizamos o mesmo algoritmo que tínhamos feito no 1º programa
  teste.value = 0;
  for (i = teste.i_min; i < teste.i_max; i++)
  {
    if (i%2 == 1) // Se o resto de i/2 for 1 (ou seja, i é impar)
      teste.value += sqrt(i); // Soma raiz de i
    else
      teste.value -= sqrt(i); // Subtrai raiz de i
  }
  
  // Copiamos o resultado no parâmetro original
  ((thread_param_t*)param)->value = teste.value;
}

int main()
{
  // Estas variáveis serão a identificação de cada Thread
  pthread_t thread1, thread2, thread3;
  // Estas serão responsáveis por distribuir o processamento
  thread_param_t param0, param1, param2, param3;
  
  param0.i_min =          1;
  param0.i_max =  250000000;
  param1.i_min =  250000000;
  param1.i_max =  500000000;
  param2.i_min =  500000000;
  param2.i_max =  750000000;
  param3.i_min =  750000000;
  param3.i_max = 1000000000;
  
  // Criamos as threads
  pthread_create(&thread1, NULL, thread_call, (void*)&param1);
  pthread_create(&thread2, NULL, thread_call, (void*)&param2);
  pthread_create(&thread3, NULL, thread_call, (void*)&param3);
  // E por fim, a Thread padrão chama a função como normalmente
  thread_call((void*)&param0);

  // Aguardamos que as outras threads terminem
  pthread_join(thread1, NULL);
  pthread_join(thread2, NULL);
  pthread_join(thread3, NULL);

  // Somamos as parcelas
  param0.value += param1.value + param2.value + param3.value;
  printf("%f\n", param0.value); // Imprime o resultado
  return 0;
}

Resultados no meu PC:

/*15811.768402
real    0m5.038s
user    0m18.860s
sys     0m0.020s*/

Lembrando que anteriormente havíamos conseguido 12.242s. Portanto, tivemos uma melhora de 143%! O programa ficou mais complexo, mas sabemos que há certos casos que essa melhora é muito importante.
Reparem também que o tempo “máximo” de programa, somando as parcelas que foram processadas de forma paralela, foi de 18.860s, que foi resultado do aumento do programa para adicionar as Threads. Ou seja, se o programa fosse executado por um só núcleo, ele demoraria 18.86s.

E o gráfico do processamento, dessa vez usando todos os núcleos:

Espero que esse post tenha sido interessante, e que esse conhecimento seja útil para você de alguma forma.
Devemos lembrar também que não se usa Thread apenas para utilizar melhor os recursos, mas também para “paralelizar” um software. Imagine por exemplo um sistema de um veículo, que possui um processador não necessariamente de vários núcleos, mas que precisa controlar a velocidade do carro integrado com o sistema de navegação (GPS). Vamos supor que o software do GPS simplesmente chame a subrotina de controle de velocidade, pedindo para ajustar em 80km/h. A subrotina de controle fica em loop até o carro chegar em 80km/h +- 2km/h. Essa velocidade naturalmente pode demorar para ser alcançada, e enquanto isso a interface do GPS precisa continuar funcionando. Nesse caso, o controle de velocidade precisa estar em uma thread separada.

Publicado em Computação | Deixe um comentário

Threads em C (parte 1)

Quase todo mundo hoje em dia possui um computador com processador multi-core. Alguns Core i5 possuem 4 núcleos, e há alguns Core i7 com 8! E apesar da maior parte dos programas usarem apenas 1 núcleo, o computador consegue distribuir os vários programas sendo executados entre todos os outros núcleos. Até aí, está tudo perfeito. Porém, e se o seu programa exige um cálculo avançado? O computador não sabe como distribuir um só programa, e como resultado você terá 1 núcleo trabalhando 100% e os outros próximo de 0%.

A resposta mais simples para esse problema é o uso de Threads. Uma Thread é um recurso utilizado para dividir um programa em 2 processos, mas que conseguem acessar as variáveis um do outro. Em quase todas as linguagens de programação ele funciona de maneira muito parecida. Existe uma função do tipo start_thread, que tem como parâmetro uma outra função (esta é você quem deverá definir), e quando start_thread é chamada, um núcleo vai para a próxima instrução, como se nada tivesse acontecido, e o outro começa a executar a função que você passou como parâmetro. Confuso? Vamos ver como acontece na prática.

A linguagem de programação que vamos utilizar é C. Primeiro, vamos fazer um programa que faça um cálculo demorado. Arbitrariamente, eu escolhi fazer o seguinte cálculo:

resultado = √1 – √2 + √3 – … + √999999999
Obs.: Esse cálculo é inútil, mas imagine que poderia ser um cálculo de um jogo que simula a
física de forma apurada, e que deve ser feito a uma taxa de 60 frames por segundo para que o jogador não experiencie aquelas chatas “travadas”.

#include
#include

int main()
{
    double teste = 0;
    int i;
    for (i = 1; i < 1000000000; i++)
    {
        if (i%2 == 1) // Se o resto de i/2 for 1 (ou seja, i é impar)
            teste += sqrt(i); // Soma raiz de i
        else
            teste -= sqrt(i); // Subtrai raiz de i
    }
    printf("%f\n", teste); // Imprime o resultado
    return 0;
}

Para compilar:    gcc codigo1.c -lm -o codigo1

Pra quem usa Linux, saiba o tempo de execução do seu programa assim:    time ./codigo1

Resultados no meu computador:

15811.768402
real 0m12.240s
user 0m12.200s
sys  0m0.000s

real -> tempo calculado como se vc tivesse cronometrado a execução
user -> tempo que seu código realmente demorou (tirando overhead e etc, mas somando as parcelas executadas em cada núcleo). Se este tempo é menor do que o real, isso indica que seu programa usou apenas 1 núcleo.

Observem agora o gráfico do processamento. Reparem que quando o programa está rodando, apenas 1 core está 100%.

 

No próximo post, vamos fazer esse mesmo programa utilizar os 4 núcleos, ver a diferença de tempo e do gráfico, e tentarei fazer uma boa explicação de como funcionam as chamadas de função.

Editado: Veja o post da 2ª parte

Abraços

Publicado em Computação | Deixe um comentário

Baby Citroen toca Secret Agent Man

Eu sou esse da guitarra vermelha. hehehe

Publicado em Música | 1 Comentário

What is Rally

Este vídeo vale mais do que 1000 palavras:

O Campeonato Mundial de Rally (World Rally Championship) é uma das únicas categorias de automobilismo hoje em dia onde os pilotos não conseguem chegar no limite do carro nem no limite da pista, mas sim no limite deles próprios. Nessa categoria, não vence o melhor carro, nem a equipe com mais dinheiro, e sim o melhor piloto e o melhor trabalho em equipe.

O piloto que arriscar de menos não ganha, pois vence o mais rápido. O piloto que arriscar demais capota. Isso sim é que é corrida! hahaha

Abraços!

Publicado em Automobilismo | Deixe um comentário

Limp Bizkit – Take a Look Around

Vou ser sincero.. não gosto muito de Limp Bizkit não. Não faz meu estilo.

Mas temos q admitir… eles são mto bons! Tava vendo um vídeo deles tocando ao vivo a música “Take a Look Around”, famosa por ser tema do filme “Missão Impossível II”, e achei legal compartilhar aqui. Os efeitos especiais desse show são excelentes tb.

Admito que levei um susto nos 3:26. hauahua

Publicado em Música | Deixe um comentário

Jamiroquai – Runaway

Já que eles vão tocar mês que vem, aí vai um vídeo excelente deles tocando no Live from Abbey Road.

Abraços!

Publicado em Música | Deixe um comentário