www.digitalmars.com
Last update Wed May 31 17:11:58 2006

Gerenciamento de Memória

Qualquer programa não-trivial precisa alocar e liberar memória. Técnicas de gerenciamento de memória se tornam mais e mais importantes como programas aumentam em complexidade, tamanho e desempenho. D oferece muitas opções para gerenciar memória.

Os três métodos primários para alocar memória em D são:

  1. Dados estáticos, alocados no segmento de dados padrão.
  2. Dados de pilha, alocados na pilha do programa da CPU.
  3. Dados com coleta de lixo, alocados dinâmicamente na pilha do coletor de lixo.
Este capítulo descreve técnicas para usá-los, bem como algumas alternativas avançadas:

Copy-on-Write (Cópia na Escrita) de Strings (e Array)

Considere o cado de passar um array para uma função, possivelmente modificando os conteúdos do array, e retornando o array modificado. Já que arrays são passados por referência, não por valor, um assunto crucial é quem possui os conteúdos do array? Por exemplo, uma função para converter um array de caracteres para letras maiúsculas:
char[] toupper(char[] s)
{
int i;

for (i = 0; i < s.length; i++)
{
char c = s[i];
if ('a' <= c && c <= 'z')
s[i] = c - (cast(char)'a' - 'A');
}
return s;
}
Note que a versão chamadora de s[] também é modificada. Isso pode não ser o que foi pretendido, ou pior, s[] pode ser uma fatia para uma seção de memória somente leitura.

Se uma cópia de s[] for sempre feita por toupper(), então isso irá desnecessáriamente consumir tempo e memória para strings que já estão totalmente e maiúsculo.

A solução é implementar cópia na escrita, que quer dizer que uma cópia é feita somente se a string precisa ser modificada. Algumas linguagens de processamento de strings fazem isso como comportamento padrão, mas há um alto custo para isso. A string "abcdeF" enrolará sendo copiada 5 vezes pela função. Para ter a eficiência máxima usando o protocolo, ele deve ser feito explicitamente no código. Aqui  toupper() reescrita para implementar cópia na escrita de maneira eficiênte:

char[] toupper(char[] s)
{
int changed;
int i;

changed = 0;
for (i = 0; i < s.length; i++)
{
char c = s[i];
if ('a' <= c && c <= 'z')
{
if (!changed)
{ char[] r = new char[s.length];
r[] = s;
s = r;
changed = 1;
}
s[i] = c - (cast(char)'a' - 'A');
}
}
return s;
}
Cópia na escrita é o protocolo implementado pelas funções de processamento de arrays na biblioteca padrão D Phobos.

Tempo Real

Programação em Tempo Real significa que um programa deve ser capaz de garantir latência máxima, ou tempo para completar a operação. Com a maioria dos esquemas de alocação de memória, incluindo malloc/free e coleta de lixo, a latência é teoricamente ilimitada. O jeito mais confiável para garantir latência é pré-alocar todos os dados que serão necessários pela porção de tempo crítica. Se nenhuma chamada para alocar memória for feita, o gc não será executado e então não fará a latência máxima ser excedida.

Operação Suave

Related to real time programming is the need for a program to operate smoothly, without arbitrary pauses while the garbage collector stops everything to run a collection. An example of such a program would be an interactive shooter type game. Having the game play pause erratically, while not fatal to the program, can be annoying to the user. There are several techniques to eliminate or mitigate the effect:
  • Preallocate all data needed before the part of the code that needs to be smooth is run.
  • Manually run a gc collection cycle at points in program execution where it is already paused. An example of such a place would be where the program has just displayed a prompt for user input and the user has not responded yet. This reduces the odds that a collection cycle will be needed during the smooth code.
  • Call gc.disable() before the smooth code is run, and gc.enable() afterwards. This will cause the gc to favor allocating more memory instead of running a collection pass.

    Listas Livres

    Listas Livres são um grande jeito de acelerar acesso à tipos freqüentemente alocados e discartados. A idéia é simples - ao invés de desalocar um objeto quando terminar com ele, ponhá-o em uma lista livre. Quando alocar, puxe alguém da lista livre primeiro.
    class Foo
    {
    static Foo freelist; // começo da lista livre

    static Foo allocate()
    { Foo f;

    if (freelist)
    { f = freelist;
    freelist = f.next;
    }
    else
    f = new Foo();
    return f;
    }

    static void deallocate(Foo f)
    {
    f.next = freelist;
    freelist = f;
    }

    Foo next; // para uso por FooFreeList
    ...
    }

    void test()
    {
    Foo f = Foo.allocate();
    ...
    Foo.deallocate(f);
    }
    Tais aproximações de listas livres podem ter desempenho muito alto.

    Contagem de Referência

    A idéia por trás da contagem de referências é incluir um campo contador no objeto. Incremente para cada referência adicional para ele, e decremente sempre que uma referência cessar. Quando o contador contas0, o objeto pode ser deletado.

    D não provê qualquer processo automática para contagem de referências, ela deve ser feita explicitamente.

    Programação COM Win32 usa os membros AddRef() e Release() para manter contagem de referências.

    Alocação de Instancia de Classe Explícita

    D provê um significado de criar alocadores e dasalocadores customizados para instancias de classes. Normalmente, estas seriam alocados na pilha do coletor de lixo, e desalocadas quando o coletor decidir correr. Para propósitos especializados, isso pode ser controlado criando NewDeclarations e DeleteDeclarations. Por exemplo, para alocar usando malloc e free da biblioteca de C:
    import std.c.stdlib;
    import std.outofmemory;
    import std.gc;

    class Foo
    {
    new(size_t sz)
    {
    void* p;

    p = std.c.stdlib.malloc(sz);
    if (!p)
    throw new OutOfMemoryException();
    std.gc.addRange(p, p + sz);
    return p;
    }

    delete(void* p)
    {
    if (p)
    { gc.removeRange(p);
    std.c.stdlib.free(p);
    }
    }
    }
    As características criticas de new() são: As características críticas de delete() são: Se memória é alocada usando alocadores e desalocadores específicos de classe, praticas de codificação cuidadosas devem ser seguidas para evitar escapes de memória e referências oscilando. Na presença de exceções, isso é particularmente importante para praticar RAII para previnir escapes de memória.

    Marque/Solte

    Marque/solte é equivalente à um método da pilha de alocar e liberar memória. Uma 'pilha' é criada na memória. Objetos são alocados simplesmente movendo um ponteiro abaixo na pilha. Vários ponteiros são 'marcados', e então seções inteiras de memória são soltas simplesmente reajustando o ponteiro da pilha devolta para um ponto marcado.
    import std.c.stdlib;
    import std.outofmemory;

    class Foo
    {
    static void[] buffer;
    static int bufindex;
    static const int bufsize = 100;

    static this()
    { void *p;

    p = malloc(bufsize);
    if (!p)
    throw new OutOfMemory;
    gc.addRange(p, p + bufsize);
    buffer = p[0 .. bufsize];
    }

    static ~this()
    {
    if (buffer.length)
    {
    gc.removeRange(buffer);
    free(buffer);
    buffer = null;
    }
    }

    new(size_t sz)
    { void *p;

    p = &buffer[bufindex];
    bufindex += sz;
    if (bufindex > buffer.length)
    throw new OutOfMemory;
    return p;
    }

    delete(void* p)
    {
    assert(0);
    }

    static int mark()
    {
    return bufindex;
    }

    static void release(int i)
    {
    bufindex = i;
    }
    }

    void test()
    {
    int m = Foo.mark();
    Foo f1 = new Foo; // alocado
    Foo f2 = new Foo; // alocado
    ...
    Foo.release(m); // desaloca f1 e f2
    }
  • A alocação de buffer[] propriamente dita é adicionada como uma região para o gc, então não há necessidade para uma chamada separada dentro de Foo.new() para fazer isso.

    RAII (Resource Acquisition Is Initialization)

    Técnicas RAII  podem ser úteis em evitar transbordamentos de memória quando usando alocadores e desalocadores explícitos. Adicionar o atributo auto à tais classes pode ajudar.

    Alocar Instâncias de Classe na Pilha

    Alocar instancias de classe na pilha é util para objetos temporários que são automaticamente desalocados quando a função é deixada. Nenhum controle especial é necessário para a terminação da função via desenrolamento da pilha de uma exceção. Para trabalhar, elas não devem ter destrutores, já que tal destrutor nunca seria chamado.

    Para ter uma classe alocada na pilha que tenha um destrutor, é o mesmo que uma declaração com o atributo auto. Embora a implementação atual não ponha tais objetos na pilha, futuramente poderá.

    import std.c.stdlib;

    class Foo
    {
    new(size_t sz, void *p)
    {
    return p;
    }

    delete(void* p)
    {
    assert(0);
    }
    }

    void test()
    {
    Foo f = new(std.c.stdlib.alloca(Foo.classinfo.init.length)) Foo;
    ...
    }

    Alocar Arrays Não-Inicializados na Pilha

    Arrays são sempre inicializados em D. Então, a seguinte declaração:
    void foo()
    { byte[1024] buffer;

    fillBuffer(buffer);
    ...
    }
    não será tão rápida quanto poderia ser já que o conteúdo de buffer[] são sempre inicializados. e cuidadoso perfilamento do programa mostra que essa inicialização é um problema de velocidade, isso pode ser eliminado usando um VoidInitializer:
    void foo()
    { byte[1024] buffer = void;

    fillBuffer(buffer);
    ...
    }
    Dados não-inicializados na pilha vem com alguns caveats que precisam ser cuidadosamente avaliados antes de usar: