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:
- Dados estáticos, alocados no segmento de dados padrão.
- Dados de pilha, alocados na pilha do programa da CPU.
- Dados com coleta de lixo, alocados dinâmicamente na pilha do coletor de lixo.
- Copy-on-Write (Cópia na Escrita) de Strings (e Array)
- Tempo Real
- Operação Suave
- Listas Livres
- Contagem de Referência
- Alocação de Instancia de Classe Explícita
- Marque/Solte
- RAII (Resource Acquisition Is Initialization)
- Alocar Instâncias de Classe na Pilha
- Alocar Arrays Não-Inicializados na Pilha
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)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.
{
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;
}
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)Cópia na escrita é o protocolo implementado pelas funções de processamento de arrays na biblioteca padrão D Phobos.
{
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;
}
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: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 FooTais aproximações de listas livres podem ter desempenho muito alto.
{
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);
}
- Se usadas por múltiplos threads, as funções allocate() e deallocate() precisam ser sincronizadas.
- O construtor de Foo não é re-executado por allocate() quando alocar da lista livre, então o alocador pode precisar reinicializar alguns dos membros.
- Não é necessário praticar RAII com isso, já que se quaisquer objetos não são passados para deallocate() quando terminados, por causa de uma exceção lançada, serão eventualmente escolhidos pelo gc de qualquer modo.
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;As características criticas de new() são:
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);
}
}
}
- new() não tem um tipo de retorno especificado, mas é definido para ser void*. new() deve retornar um void*.
- Se new() não pode alocar memória, ela não deve retornar nulo, mas deve disparar uma exceção.
- O ponteiro retornado por new() deve ser para memória alinhada pelo alinhamento padrão. Isso é 8 em sistemas win32.
- O parâmetro tamanho é necessário no caso de o alocador ser chamado de uma classe derivada de Foo e for de um tamanho maior que Foo.
- Um null não é retornado se armazenamento não pode ser alocado. Ao invés disso, uma exceção é disparada. Que exceção será disparada é dada pelo programador, nesse caso, é OutOfMemory().
- Quando varrer a memória por ponteiros raíz na pilha do coletor de lixo, o segmento de dados estático e a pilha são varridas automaticamente. A pilha de C não é. Conseqüentemente, se Foo ou qualquer classe derivada de Foo o alocador contem qualquer referência para dados alocados pelo coletor de lixo, o gc deve ser notificado. Isso é feito com o método gc.addRange().
- Nenuma inicialização de memória é necessária, como o código é automaticamente inserido após a chamada à new() para ajustar os membros da instancia da classe para seus padrões e então o construtor (se algum) ser executado.
- O destrutor (se algum) já foi chamado no argumento p, então os dados que ele aponta deveriam ser assumidos como sendo lixo.
- O ponteiro p pode ser nulo.
- Se o gc foi notificado com gc.addRange(), uma chamada correspondente à gc.removeRange() deve ocorrer no desalocador.
- Se há um delete(), deveria haver um new() correspondente.
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
}
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;
...
}
- Não há necessidade de chacar por uma falha de alloca() e lançar uma exceção, já que por definição alloca() irá gerar uma exceção de transbordamento de pilha se ela transbordar.
- Não há necessidadede uma chamada a gc.addRange() ou gc.removeRange() já que gc varre a pilha de qualquer forma.
- A boba função delete() é para assegurar que nenhuma tentativa de deletar um objeto baseado na pilha será feita.
Alocar Arrays Não-Inicializados na Pilha
Arrays são sempre inicializados em D. Então, a seguinte declaração:void foo()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:
{ byte[1024] buffer;
fillBuffer(buffer);
...
}
void foo()Dados não-inicializados na pilha vem com alguns caveats que precisam ser cuidadosamente avaliados antes de usar:
{ byte[1024] buffer = void;
fillBuffer(buffer);
...
}
- Os dados não-inicializados que estão na pilha serão varridos pelo coletor de lixo procurando por quaisquer referências para memória alocada. Já que dados não-inicializados consistem de antigos quadros da pilha de D, é altamente provável que algo daquele lixo irá parecer com referências na pilha do gc, e a memória do gc não será liberada. Esse problema realmente acontece, e pode ser bem frustrante consertar.
- É possível para uma função passar uma referência para dados naquele quadro de pilha da função. Mas então alocar um novo quadro de pilha sobre os dados antigos, e não inicializar, a referência para os dados antigos pode parecer ser válida. O programa irá então se comportar incorretamente. Inicializando todos os dados no quadro da pilha irá aumentar a probabilidade de forçar esse bug de forma repetível.
- Dados não inicializados podem ser uma fonte de bugs e problemas, mesmo quando usado corretamente. Uma meta do projeto de D é melhorar confiança e portabilidade eliminando fontes de comportamento indefinido, e dados não inicializados são uma enorme fonte de comportamentos indefinidos, importáveis, irrados e imprevisíveis. Conseqüentemente esse idioma deveria ser usado somente após outras oportunidades por otimização de velocidade serem exaustas e comparações mostram que realmente aceleram a execução global.