Memória
Contagem de referências, weak, ligações dono/filho (Ref), listas intrusivas (ChainRoot), ponteiros, absolute e atômicos.
Objetos com contagem de referências#
Instâncias de classe são tipos gerenciados: o compilador gera AddRef/Release nas atribuições e na saída de escopo, e o objeto morre quando a última referência some. Não há coletor de lixo e não há pausa.
program Memory;
type
TNode = class(TObject)
private
FName: UString;
public
constructor Create(const AName: UString);
destructor Destroy; override;
end class;
TChild = class(TObject)
private
weak FParent: TNode; // weak: a ligação não conta referência
public
constructor Create(AParent: TNode);
end class;
constructor TNode.Create(const AName: UString);
begin
FName := AName;
end constructor;
destructor TNode.Destroy;
begin
WriteLn(' destruído: ', FName);
inherited Destroy;
end destructor;
constructor TChild.Create(AParent: TNode);
begin
FParent := AParent;
end constructor;
procedure Scope;
var
A, B: TNode;
Child: TChild;
begin
A := TNode.Create('nó A');
B := A; // duas referências ao mesmo objeto
Child := TChild.Create(A); // weak: não segura o pai vivo
A := nil;
WriteLn(' A := nil, mas B ainda segura o objeto');
end procedure; // B sai do escopo: o objeto morre
begin
WriteLn('entrando:');
Scope;
WriteLn('depois do escopo');
end.entrando:
A := nil, mas B ainda segura o objeto
destruído: nó A
depois do escopo- A destruição acontece no fim do escopo da última referência, de forma determinística: o destrutor roda ali, não "mais tarde".
- Cadeias, vetores dinâmicos, interfaces e records gerenciados seguem o mesmo princípio.
weakna frente de um campo ou variável desliga a contagem: a ligação existe, mas não segura o objeto vivo. É o que quebra ciclos (o filho aponta o pai comoweak).- Uma região
{$code unsafe}desliga o rastreio de tudo o que está dentro dela: é o modo de escrever código de baixo nível (ponteiros, buffers) sem pagar a contagem, e é por isso que a RTL a usa em vários módulos. O contrário é{$code safe}.
Para achar vazamentos, o programa pode ligar o relatório de saída (ReportMemoryLeaksOnShutdown := True) e a configuração allocsite registra o arquivo e a linha de cada alocação; veja Diagnósticos.
Ligação dono/filho: Ref e Refs#
Ref(TFilho, FCampoDoPai) declara uma ligação de mão dupla: o dono aponta o filho, o filho aponta de volta, e os dois lados se acertam sozinhos.
program Refs;
type
TCustomer = class;
TAddress = class;
TCustomer = class(TObject)
private
// ligação de mão dupla: o dono aponta o filho e o filho aponta de volta
FAddress: Ref(TAddress, FCustomer);
public
property Address: TAddress read FAddress write FAddress;
end class;
TAddress = class(TObject)
private
weak FCustomer: TCustomer; // pelo menos um dos lados é weak
public
property Customer: TCustomer read FCustomer;
end class;
var
C: TCustomer;
A, B: TAddress;
begin
C := TCustomer.Create;
A := TAddress.Create;
B := TAddress.Create;
C.Address := A; // liga os dois lados de uma vez
WriteLn('A.Customer = C? ', A.Customer = C);
C.Address := B; // desliga A e liga B
WriteLn('A.Customer = nil? ', A.Customer = nil);
WriteLn('B.Customer = C? ', B.Customer = C);
C.Address := nil; // atribuir nil desliga
WriteLn('B.Customer = nil? ', B.Customer = nil);
end.A.Customer = C? TRUE
A.Customer = nil? TRUE
B.Customer = C? TRUE
B.Customer = nil? TRUEO campo tem o tipo do filho: uma referência de classe já é um ponteiro de 8 bytes. Ler chama _Ref_GetValue; escrever chama _Ref_SetValue, que desliga o filho anterior dos dois lados e liga o novo; atribuir nil desliga; os destrutores desfazem a ligação dos dois lados.
Duas regras que o compilador recusa se você quebrar:
- Pelo menos um dos dois lados é
weak. Duas ligações fortes fariam um ciclo que nenhum dos dois solta. Oweakna frente do campoRefenfraquece a ligação para o filho; no campo de volta, enfraquece a do pai. - Um filho, um dono: um segundo
Refpara o mesmo campo de volta é erro.
Refs(TFilho, FCampoDoPai) é o irmão plural, com as mesmas regras. O campo não tem o tipo do filho (guarda uma lista) e responde por cinco membros: Count, GetItem(I), Add(X), Remove(X) e Clear. Destruir o dono chama Clear; destruir um filho o tira da lista do dono.
Listas intrusivas: ChainRoot e Chain#
Uma lista encadeada que não aloca nada: os ponteiros de ligação moram dentro do próprio elemento. A raiz mora na classe dona; o elo, na classe do elemento.
program Chains;
type
TParcel = class;
TInvoice = class(TObject)
private
// lista intrusiva: não aloca nada, os elos moram dentro de cada TParcel
FParcels: ChainRoot(TParcel, FChainInInvoice);
public
procedure Add(const AParcel: TParcel);
function Total: Int32;
function Count: NativeInt;
end class;
TParcel = class(TObject)
public
Value: Int32;
FChainInInvoice: Chain;
constructor Create(const AValue: Int32);
end class;
constructor TParcel.Create(const AValue: Int32);
begin
Value := AValue;
end constructor;
procedure TInvoice.Add(const AParcel: TParcel);
begin
FParcels.AddLast(AParcel); // a cadeia retém o elemento
end procedure;
function TInvoice.Total: Int32;
var
P: TParcel;
begin
Result := 0;
for P in FParcels do // do primeiro ao último
Inc(Result, P.Value);
end function;
function TInvoice.Count: NativeInt;
begin
Exit(FParcels.GetCount);
end function;
var
Invoice: TInvoice;
begin
Invoice := TInvoice.Create;
Invoice.Add(TParcel.Create(100));
Invoice.Add(TParcel.Create(250));
Invoice.Add(TParcel.Create(50));
WriteLn(Invoice.Count, ' parcelas, total ', Invoice.Total);
end.3 parcelas, total 400Por omissão, a cadeia retém os elementos: adicionar dá AddRef, remover dá Release. Com weak na raiz (weak FParcels: ChainRoot(...)) ela apenas aponta, o que serve para uma lista de observadores.
- Na raiz:
AddFirst,AddLast,AddAfter,AddBefore,Contains,GetCount,GetItem,IsEmpty,IsValidChain,Remove,Sort,TryExchange,TryExtractFirst,TryExtractLast,TryMoveToNext,TryMoveToPrevious,GetFirst,GetLast,Clear. - No elo:
GetPreviouseGetNext. - Um terceiro parâmetro (
ChainRoot(TParcel, FElo, FDocumento)) nomeia o campo do filho que aponta para o dono. Com ele, entrar e sair passam por uma reserva atômica desse campo: o mesmo objeto não entra em duas cadeias. - As posses são independentes: reter o filho (a cadeia) e reter o pai (o campo do dono) são decisões separadas, e cada
weakresponde por uma. As duas fortes são erro (ciclo). - Os destrutores são automáticos: o pai destruído esvazia a cadeia; o filho destruído se desliga do dono.
A varredura tem forma própria. O próximo elemento é lido antes do corpo, então tirar o corrente da cadeia lá dentro continua válido:
for P in FParcels do // do primeiro ao último
for P downin FParcels do // do último ao primeiro
// e sobre uma cadeia que o próprio objeto oferece, pelas properties Next/Previous:
for N in chain(AFirst) do
for N downin chain(ALast) doEventos que se desligam sozinhos: Delegate#
Um campo of object cru fica apontando para memória morta se o alvo for destruído. Delegate(T) guarda o método na cadeia do objeto-alvo, e o destrutor de TObject zera o código de todos os que apontavam para ele. O weak diz se a ligação conta: um evento normalmente não deve manter vivo quem o escuta.
program Delegates;
type
TNotifyEvent = procedure(const ASender: TObject) of object;
TButton = class(TObject)
public
// Delegate: o evento que se desliga sozinho quando o alvo morre
weak FOnClick: Delegate(TNotifyEvent);
procedure Click;
end class;
TTarget = class(TObject)
public
procedure Pressed(const ASender: TObject);
destructor Destroy; override;
end class;
procedure TButton.Click;
begin
FOnClick!(Self);
end procedure;
procedure TTarget.Pressed(const ASender: TObject);
begin
WriteLn(' Pressed');
end procedure;
destructor TTarget.Destroy;
begin
WriteLn(' alvo destruído');
inherited Destroy;
end destructor;
var
Button: TButton;
Target: TTarget;
begin
Button := TButton.Create;
Target := TTarget.Create;
Button.FOnClick := Target.Pressed;
WriteLn('com o alvo vivo:');
Button.Click;
Target := nil; // weak: a ligação não segura o alvo, que morre
WriteLn('depois que o alvo morreu:');
Button.Click; // nada acontece, e nenhuma memória morta é tocada
end.com o alvo vivo:
Pressed
alvo destruído
depois que o alvo morreu:Num Delegate de função, desligado não chama e devolve o valor zero do tipo; quem precisa distinguir "ninguém ligado" de "respondeu zero" testa o campo contra nil antes.
Ponteiros#
Veja também Tipos. New/Dispose alocam um tipo, GetMem/FreeMem/ReallocMem blocos, e a aritmética de ponteiro está disponível.
Sobreposição: absolute#
A variável declarada com absolute não tem armazenamento próprio: é uma vista sobre o de outra. Não aloca, não inicializa e não finaliza; quem tem o valor é a base.
program AbsoluteDemo;
procedure Demo;
var
P: UInt64;
X: UInt64 absolute P; // o mesmo lugar que P
B: UInt8 absolute P; // o byte de baixo
begin
P := $1122334455667788;
WriteLn('B = ', B); // $88 = 136
X := 7; // escrever pela vista muda P
WriteLn('P = ', P);
B := 1;
WriteLn('P = ', P); // só o byte de baixo mudou
end procedure;
begin
Demo;
end.B = 136
P = 7
P = 1Em código seguro a sobreposição exige que nenhum dos dois lados seja gerenciado (senão seriam duas contagens de referência que não se conhecem) e que a vista não seja maior que a base. Dentro de {$code unsafe} nada disso é cobrado. absolute não é palavra reservada: naquela posição nenhum identificador seria válido, então ela segue servindo de nome.
Os atômicos#
São formas da linguagem: o compilador emite atomicrmw e cmpxchg, que no x86-64 viram lock xchg, lock cmpxchg, lock add e lock sub. Uma instrução cada, sem chamada.
program Atomics;
var
Counter: Int32;
Old: Int32;
begin
// cada uma vira uma única instrução com "lock" (sem chamada)
AtomicIncrement(Counter);
AtomicIncrement(Counter, 10);
WriteLn('Counter = ', Counter);
Old := AtomicExchange(Counter, 100);
WriteLn('antes: ', Old, ', agora: ', Counter);
// compara e troca: só grava se o valor atual for o esperado (o último argumento)
Old := AtomicCmpExchange(Counter, 555, 100);
WriteLn('trocou? ', Old = 100, ' Counter = ', Counter);
Old := AtomicCmpExchange(Counter, 999, 100);
WriteLn('trocou? ', Old = 100, ' Counter = ', Counter);
end.Counter = 11
antes: 11, agora: 100
trocou? TRUE Counter = 555
trocou? FALSE Counter = 555AtomicExchange(Alvo, Novo)
AtomicCmpExchange(Alvo, Novo, Comparando [, out Sucesso])
AtomicIncrement(Alvo [, Passo])
AtomicDecrement(Alvo [, Passo])Valem como comando e aceitam inteiro de 1, 2, 4 e 8 bytes; Exchange e CmpExchange aceitam também ponteiro, classe, interface e enumerado. Increment e Decrement devolvem o valor novo, como no Delphi. Se a RTL declarar _AtomicExchange_<Tipo> (ou as irmãs), o compilador chama aquilo em vez de emitir a instrução; faltar o gancho não é erro.