Archive for the ‘Implementação do C++’ Category.

Gerência de memória lenta em C++? Redefinido os operadores new e delete

Programa lento? Já viu se é a gerência de memória?

Redefinindo os operadores new e delete

É fato conhecido entre os programadores C++ que a gerência de memória implementada na biblioteca padrão do C/C++ é ineficiente para a alocação/liberação de muitos (mas muuuuuitos mesmo!) blocos de memória de tamanhos distintos. Programas que alocam e liberam objetos freneticamente pagam um preço muito alto se utilizarem os operadores new e delete padrão. O problema ocorre porque as rotinas que manipulam os blocos de memória devem lidar com blocos de tamanhos diferentes, e isso significa trabalhar com listas encadeadas, encontrar blocos de tamanho adequado, fragmentação etc. Nesse link há uma pequena introdução sobre o assunto.

O programa a seguir aloca e desaloca objetos freneticamente. Não quero discutir o que o programa faz, na verdade ele foi copiado daqui (uma comparação de desempenho entre Java, C++ e outras linguagens…) com duas pequenas alterações: 1) eliminei a chamada a gettimeofday e meço o tempo pelo SO; 2) o programa original tinha um leak de memória. Quero apenas mostrar como esse programa roda usando o new e delete padrão, e como uma abordagem não intrusiva para redefinir esses operadores consegue fazer com que esse programa rode 10x mais rápido…

Código original:

#include <stdio.h>
#include <iostream>

using namespace std;

class Person {
  public:
    Person( int id, int count ): _next(NULL), _prev(NULL), _count(count), _id(id) {}

    int shout( int shout, int nth ) {
      if( shout < nth )
        return shout + 1;

      _prev->_next = _next;
      _next->_prev = _prev;

      return 1;
    }

    int count() const { return _count; }
    Person* next() const { return _next; }
    Person* prev() const { return _prev; }

    void next(Person* person) { this->_next = person ; }
    void prev(Person* person) { this->_prev = person; }

   private:
     int _count;
     int _id;
     Person* _next;
     Person* _prev;
};

class Chain {
  public:
    Chain(const int id, const int size) : _first(NULL), _id(id) {
      Person* current = NULL;
      Person* last = NULL;

      for( int i = 0 ; i < size ; i++ )
      {
        current = new Person( id + i, i );

        if( _first == NULL )
          _first = current;

        if( last != NULL ) {
          last->next(current);
          current->prev(last);
        }

        last = current;
      }

      _first->prev(last);
      last->next(_first);
    }

    Person* kill( const int nth ) {
      Person* current = _first;
      int shout = 1;

      while( current->next() != current )
      {
        Person* tmp = current;

        shout = current->shout( shout,nth );
        current = current->next();

        if( shout == 1 )
          delete tmp;
      }

      _first = current;

      return current;
    }

    Person* first() const  { return _first; }

    ~Chain() { delete _first; }

  private:
      Person* _first;
      int _id;
};

int main( int argc, char* argv[] )
{
  const int ITER = 10000000;

  for( int i = 0 ; i < ITER ; ++i ) {
    Chain* chain = new Chain(i, 40);

    chain->kill(3);
    delete chain;
  }

  return 0;
}

Ok, o código acima é meio doido. É sobre a história de Flávio Josefo, por vezes designado como problema de Josefo ou Roleta Romana. Mas o interessante é que ele aloca e libera 410 milhões de pequenos objetos com dois tamanhos diferentes e de forma intercalada, gerando um estresse nas rotinas de gerência de memória. Apesar desse número, o consumo de memória do programa é mínimo: a cada instante, o número máximo de objetos alocados é 41. De certa forma, esse programa mimetiza o comportamento de um programa que executa sequencialmente milhares de pequenas tarefas por minuto em transações curtas: para cada tarefa é alocado um pequeno número de objetos de tamanhos distintos e após a transação estes objetos são liberados.  O tempo de execução e opções de compilação são mostrados a seguir:

Máquina          : Intel(R) Core(TM) i7 CPU 920 @ 2.67GHz
Sistema          : Windows XP SP2
Compilador       : gcc version 4.4.0 (mingw32)
Linha de comando : g++ -O3 -funroll-loops -fprefetch-loop-arrays -march=core2 -msse4 ChainOriginal.cpp -o ChainOriginal.exe
Tempo de execução: 71.88 segundos

Pois bem, o que há de errado com esse código? Nada, exceto que os operadores new e delete fazem uma série de operações internas sobre listas encadeadas de espaços de memória livre que os tornam um pouco lentos. A gerência da memória dinâmica é um tópico complexo de estudo. Há vários substitutos específicos das rotinas padrão do C++ que gerenciam memória e que são mais eficientes para determinadas situações do que as rotinas genéricas. E há algumas técnicas para diminuir o tempo gasto com essa gerência – aqui vou mostrar uma variação bem simples de uma técnica conhecida como slab allocation e que é utilizada por exemplo pelo kernel do Linux. Essa técnica funciona com buffers ou caches de áreas de memória, uma para cada determinado tipo de objeto, e se adequa bem para a situação acima. Ela é muito boa quando o programa possui um número qualquer de objetos de classes distintas sendo alocados e liberados frequentemente. A implementação a seguir funciona apenas quando a não há hierarquia de classes – ou seja, não há subclasses. Para funcionar com subclasses é necessária uma outra implementação, um pouco menos eficiente pois terá de lidar com destrutores virtuais e sobrecarga de operadores. Mas vamos por partes.

Non-Virtual Slab Allocation:

#include <stdio.h>
#include <iostream>
#include <vector>

using namespace std;

class Cache {
  private:
    vector<void*> cache;

  public:
    enum Config { Max = 100 }; // Número máximo de objetos mantido no cache.
                               // Quando esse número é atingido, as chamadas
                               // a "free" realmente liberam os objetos.

    Cache(): cache() {
      cache.reserve( Max );
    }

    void *alloc( size_t size ) {
      if( cache.size() > 0 ) {
        void* aux = cache.back();

        cache.pop_back();
        return aux;
      }
      else
        return new char[size];
    }

    void free( void* ptr ) {
      if( cache.size() < Max )
        cache.push_back( ptr );
      else
        delete[] (char *) ptr;
    }
};

template <class C>
class NonVirtualSlab {
  private:
    static Cache cache;

  public:
    void* operator new ( size_t size ) {
      return cache.alloc( size );
    }

    void operator delete ( void* ptr, size_t ) {
      cache.free( ptr );
    }
};

template <class C>
Cache NonVirtualSlab<C>::cache;

class Person: public NonVirtualSlab<Person> {
  public:
    Person( int id, int count ): _next(NULL), _prev(NULL), _count(count), _id(id) {}

    int shout( int shout, int nth ) {
      if( shout < nth )
        return shout + 1;

      _prev->_next = _next;
      _next->_prev = _prev;

      return 1;
    }

    int count() const { return _count; }
    Person* next() const { return _next; }
    Person* prev() const { return _prev; }

    void next(Person* person) { this->_next = person ; }
    void prev(Person* person) { this->_prev = person; }

   private:
     int _count;
     int _id;
     Person* _next;
     Person* _prev;
};

class Chain: public NonVirtualSlab<Chain> {
  public:
    Chain(const int id, const int size) : _first(NULL), _id(id) {
      Person* current = NULL;
      Person* last = NULL;

      for( int i = 0 ; i < size ; i++ )
      {
        current = new Person( id + i, i );

        if( _first == NULL )
          _first = current;

        if( last != NULL ) {
          last->next(current);
          current->prev(last);
        }

        last = current;
      }

      _first->prev(last);
      last->next(_first);
    }

    Person* kill( const int nth ) {
      Person* current = _first;
      int shout = 1;

      while( current->next() != current )
      {
        Person* tmp = current;

        shout = current->shout( shout,nth );
        current = current->next();

        if( shout == 1 )
          delete tmp;
      }

      _first = current;

      return current;
    }

    Person* first() const  { return _first; }

    ~Chain() { delete _first; }

  private:
      Person* _first;
      int _id;
};

int main( int argc, char* argv[] )
{
  const int ITER = 10000000;

  for( int i = 0 ; i < ITER ; ++i ) {
    Chain* chain = new Chain(i, 40);

    chain->kill(3);
    delete chain;
  }

  return 0;
}

Tempo total de execução com as mesmas opções do compilador: 7.67 segundos! Aproximadamente 9.4 vezes mais rápido! E a única modificação a ser feita no fonte são nas linhas 57 e 85, onde é acrescentado a herança para NonVirtualSlab<>.

Bom, para herança e métodos virtuais é necessário uma outra implementação. Em breve posto ela aqui.

“Is this the real life? Is this just fantasy?”

Virtual Method Tables em C++

VMTs

Uma VMT (virtual method table) é um mecanismo utilizado em diversas linguagens de programação para permitir a associação em tempo de execução de uma implementação de um método a uma chamada desse método. É o mecanismo que permite o polimorfismo.

Implementação de VMTs

Uma VMT nada mais é do que um array de endereços de métodos. Nada melhor do que um exemplo para explicar como as coisas funcionam. Observe o seguinte código em C++.  Na classe Derivada, apenas o método abre foi redefinido:

#include <cstdlib>
#include <iostream>

using namespace std;

class Base {
  public:
    int x;

  virtual void abre( int n ) {
    x = n;
    cout << "Base::abre " << this << " " << x << endl;
  } 

  virtual void fecha() {
    x = 0;
    cout << "Base::fecha " << this << " " << x << endl;
  }
};

class Derivada: public Base {
  public:
  double y;

  virtual void abre( int n ) {
    x = n;
    y = n*n;
    cout << "Derivada::abre " << this << " " << x << " " << y << endl;
  }
};

int main(int argc, char *argv[])
{
  Base* b[2] = { new Base, new Derivada };

  for( int i = 0; i < 2; ++i ) {
    cout << "b[" << i << "]: " << b[i] << endl;
    b[i]->abre( 3 + i );
    b[i]->fecha();
  }

  delete b[0];
  delete b[1];

  system("PAUSE");
  return EXIT_SUCCESS;
}

Saída:

b[0]: 0x3d2490
Base::abre 0x3d2490 3
Base::fecha 0x3d2490 0
b[1]: 0x3d24f8
Derivada::abre 0x3d24f8 4 16
Base::fecha 0x3d24f8 0

Detalhes de Implementação:

Na chamada de método b[i]->abre( 3 + i ) com i valendo 1 o compilador deve chamar o método Derivada::abre, pois embora o ponteiro seja da classe Base, o objeto para o qual ele aponta é da classe Derivada. Este é um exemplo de polimorfismo. Mas como o compilador pode saber em tempo de execução qual o tipo do objeto b[i] para chamar o método correto? Note que em uma iteração ele chama Base::abre e na outra Derivada::abre.

Penso eu que mostrar um programa equivalente ao acima em C é uma das melhores formas de explicar como a VMT é implementada. Cada classe que possui ou herda métodos virtuais possui um ponteiro para uma VMT que contém os endereços dos métodos. Um método, virtual ou não, nada mais é que uma função normal do C com um parâmetro extra, implícito e automaticamente passado pelo compilador chamado this. Esse parêmetro é sempre do tipo ponteiro para o objeto que está recebendo a mensagem. E uma classe nada mais é do que uma struct do C.  Dentro de um método a resolução de escopo do compilador funciona da seguinte forma: se uma variável x é mencionada pelo programador, o compilador procura por alguma variável local ou parâmetro com esse nome. Se não encontra, procura por this->x. Se encontrar, ou seja, se x for um atributo da classe (struct), o compilador o usará. Se novamente não encontrar, ele irá procurar por uma variável x de escopo maior, global por exemplo. Note que tudo isto é feito em tempo de compilação, não existe esta busca em tempo de execução.

Dessa forma, os métodos Base::abre, Base::fecha e Derivada::abre poderiam ser traduzidos para C da seguinte forma:

void Base_abre( Base* _this, int n ) {
  _this->x = n;
  cout << "Base::abre " << _this << " " << _this->x << endl;
}

void Base_fecha( Base* _this ) {
  _this->x = 0;
  cout << "Base::fecha " << _this << " " << _this->x << endl;
}

void Derivada_abre( Derivada* _this, int n ) {
  _this->x = n;
  _this->y = n*n;
  cout << "Derivada::abre " << _this << " " << _this->x << " " << _this->y << endl;
}

Assim, cada classe possui a sua própria VMT com os endereços dos métodos que devem ser chamados durante a execução. O compilador normalmente coloca na VMT apenas os endereços dos métodos virtuais, e em geral na ordem em que são declarados. As VMTs das classes Base e Derivada poderiam ser traduzidas da seguinte forma, utilizando variáveis globais:

void* vmt_Base[2] = { (void*) &Base_abre, (void*) &Base_fecha };
void* vmt_Derivada[2] = { (void*) &Derivada_abre, (void*) &Base_fecha };

A VMT da classe Base possui os endereços de seus métodos Base_abre e Base_fecha. Por usa vez, a VMT da classe Derivada possui os endereços de Derivada_abre e Base_fecha, já que apenas o método abre foi redefinido. Embora os elementos da VMT sejam endereços de funções, para o compilador são apenas endereços, ou seja, void*.

Uma observação: em C padrão não seria necessário o typecast para void*, mas como estou compilando com o g++ tenho de colocá-los. É importante observar que a verificação de tipos é feita durante a compilação, então não é possível ocorrer o caso de uma interpretação errada (salvo bugs no compilador ou se o próprio programador C++ fizer algum typecast errôneo).

Podemos agora ver como as classes Base e Derivada poderiam ser traduzidas para C:

struct Base {
 void **vmt;
 int x;
};

struct Derivada {
 void **vmt;
 int x;
 double y;
};

O compilador, de posse da informação de que a classe Base possui métodos virtuais acrescenta um ponteiro para a VMT. Esse ponteiro, que é implícito, é na verdade um ponteiro para um array de endereços, logo é um ponteiro duplo (void**). A classe Derivada herda os atributos da classe Base e possui métodos virtuais, logo precisa ter também um ponteiro para uma VMT. O layout das estruturas devem ser coerentes, ou seja, Derivada deve ter os atributos de Base na mesma posição relativa, de modo que um ponteiro para uma struct Derivada possa ser convertido (sem causar estragos) para um ponteiro para uma struct Base. Isto é necessário porque nos métodos que não forem redefindos na classe  Derivada serão chamados com o parâmetro this do tipo Base* (é o caso do método Base_fecha).

Assim, finalmente podemos ver como uma chamada de um método pode ser traduzida para C:

  // Em C++: b[i]->abre( i + 3 );
  ((void (*)(Base*, int)) b[i]->vmt[0])( b[i], i + 3 ); 

  // Em c++: b[i]->fecha();
  ((void (*)(Base*)) b[i]->vmt[1])( b[i] );

b[i]->vmt é o ponteiro para a VMT do objeto para o qual b[i] aponta. Zero é a posição do endereço do método abre. Novamente é necessário um typecast, agora de void* para endereço de função:  (void (*)(Base*, int)). No problem, o compilador sabe o que está fazendo… :)

Repare que na chamada da função o parâmetro extra this recebe como argumento b[i]. Isso é feito automaticamante pelo compilador.

As VMTs das duas classes também devem ser coerentes, ou seja, os endereços dos métodos devem estar na mesma ordem. Se a classe Derivada possuir outros métodos virtuais que não existam na classe Base, a sua VMT será maior e eles terão de estar após os métodos herdados e/ou redefinidos – tudo para que um ponteiro para uma classe Derivada possa ser convertido sem problemas para um ponteiro da classe Base e a VMT continuar funcionando.

Está explicado então porque uma chamada a um método virtual é mais lenta que uma chamada a um método não virtual: é necessário um acesso extra (VMT) para descobrir o endereço do método virtual, ao passo que o endereço de um método não virtual (ou uma função comum) é sabido durante a compilação. Mas o compilador otimiza o código. Ele somente irá usar a VMT do objeto para obter o endereço do método a ser chamado quando não for possível descobrir isso a tempo de compilação – ou seja, quando estamos utilizando ponteiros ou referências para objetos.

Quando um objeto é criado a sua VMT deve ser inicializada corretamente. O código para essa inicialização é colocado nos construtores da classe. Quando uma classe não possui construtor o compilador gera um construtor default, de modo que nesse construtor default é colocado o código de inicialização da VMT. Esse é um dos motivos pelo qual não se deve alocar objetos em C++ usando malloc: quando se usa malloc, o construtor não é chamado, e não há como chamar um construtor manualmente. Daí a VMT não inicializada do objeto estará apontando para qualquer coisa – só com muita sorte estará apontando para o lugar correto. O problema não é tão ruim se o objeto não possui métodos virtuais, mas de qualquer forma esse é um estilo de programação condenável. Já o operador new irá chamar algum construtor, inicializando corretamente a VMT se for o caso. Como nesse exemplo nenhuma das classes possui construtor, assumi que o compilador irá expandir o código de inicialização da VMT diretamente, como se fosse inline. Note que a VMT deve ser inicializada para cada objeto do array.

Para quem quiser executar, segue o código completo – só copiar e colar no Dev-C++.

Por fim, essa técnica de implementação de VMTs precisa ser adaptada para  herança múltipla e herança virtual. Em outro post explico isso…

[]s!

Código Final em C:

#include <cstdlib>
#include <iostream>

using namespace std;

struct Base {
  void **vmt;
  int x;
};

void Base_abre( Base* _this, int n ) {
  _this->x = n;
  cout << "Base::abre " << _this << " " << _this->x << endl;
}

void Base_fecha( Base* _this ) {
  _this->x = 0;
  cout << "Base::fecha " << _this << " " << _this->x << endl;
}

struct Derivada {
  void **vmt;
  int x;
  double y;
};

void Derivada_abre( Derivada* _this, int n ) {
  _this->x = n;
  _this->y = n*n;
  cout << "Derivada::abre " << _this << " " << _this->x << " " << _this->y << endl;
}

void* vmt_Base[2] = { (void*) &Base_abre, (void*) &Base_fecha };
void* vmt_Derivada[2] = { (void*) &Derivada_abre, (void*) &Base_fecha };

int main(int argc, char *argv[])
{
  Base* b[2] = { (Base *) malloc( sizeof( Base ) ), (Base *) malloc( sizeof( Derivada ) ) };
  b[0]->vmt = vmt_Base;
  b[1]->vmt = vmt_Derivada;

  for( int i = 0; i < 2; ++i ) {
    cout << "b[" << i << "]: " << b[i] << endl;
    ((void (*)(Base*, int)) b[i]->vmt[0])( b[i], i + 3 );
    ((void (*)(Base*)) b[i]->vmt[1])( b[i] );
  } 

  free( b[0] );
  free( b[1] );

  system("PAUSE");
  return EXIT_SUCCESS;
}