Internos do IL2CPP: Integração do coletor de lixo

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
Jul 9, 2015|8 Min
Internos do IL2CPP: Integração do coletor de lixo
Esta página da Web foi automaticamente traduzida para sua conveniência. Não podemos garantir a precisão ou a confiabilidade do conteúdo traduzido. Se tiver dúvidas sobre a precisão do conteúdo traduzido, consulte a versão oficial em inglês da página da Web.
Esta é a sétima postagem da série IL2CPP Internals. Nesta postagem, exploraremos um pouco sobre como o tempo de execução do IL2CPP se integra a um coletor de lixo. Especificamente, veremos como as raízes do GC no código gerenciado são comunicadas ao coletor de lixo nativo.

Como em todas as postagens desta série, esta trata de detalhes de implementação que podem mudar e provavelmente mudarão no futuro. Nesta postagem, examinaremos especificamente algumas APIs internas usadas pelo código de tempo de execução para se comunicar com o coletor de lixo. Essas APIs não são suportadas publicamente, e o senhor não deve tentar chamá-las a partir de nenhum código em um projeto real. Mas, como esta é uma postagem sobre aspectos internos, vamos nos aprofundar.

Coleta de lixo

Não discutirei técnicas gerais de coleta de lixo nesta postagem, pois esse é um assunto amplo e variado, com muitas pesquisas existentes e informações publicadas. Para continuar, basta pensar em um GC como um algoritmo que desenvolve um gráfico direcionado de referências a objetos. Se um objeto Child for usado por um objeto Parent (por meio de um ponteiro no código nativo), o gráfico terá a seguinte aparência:

image03

À medida que o GC examina a memória de um processo, ele procura objetos que não tenham um pai. Se ele encontrar um, poderá reutilizar a memória desse objeto em outra coisa.

É claro que a maioria dos objetos terá um pai de algum tipo, portanto, o GC realmente precisa saber quais objetos são os pais importantes. Gosto de pensar neles como os objetos que realmente estão sendo usados pelo seu programa. Na terminologia do GC, elas são chamadas de "raízes". Aqui está um exemplo de um pai sem uma raiz.

image02

Nesse caso, o Parent 2 não tem uma raiz, portanto, o GC pode reutilizar a memória do Parent 2 e do Child 2. O Parent 1 e o Child 1, no entanto, têm uma raiz, portanto, o GC não pode reutilizar a memória deles. O programa ainda os está usando para alguma coisa.

Para o .NET, há três tipos de raízes:

- Variáveis locais na pilha de qualquer thread que esteja executando código gerenciado

- Variáveis estáticas

- Objetos GCHandle

Veremos como o IL2CPP se comunica com o coletor de lixo sobre todos esses três tipos de raízes.

A configuração

Para esta postagem, estou usando o Unity 5.1.0p1 no OSX e estou criando para a plataforma iOS. Isso nos permitirá usar o Xcode para dar uma olhada em como o IL2CPP interage com o coletor de lixo. Como nos outros posts desta série, usarei um projeto de exemplo com um único script:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using UnityEngine;

public class AnyClass {}

public class HelloWorld : MonoBehaviour {
private static AnyClass staticAnyClass = new AnyClass();
void Start () {
var thread = new Thread(AnotherThread);
thread.Start();
thread.Join();
var anyClassForGCHandle = new AnyClass();
var gcHandle = GCHandle.Alloc(anyClassForGCHandle);
}

private static void AnotherThread() {
var anyClassLocal = new AnyClass();
}
}

Ativei a opção "Development Build" na caixa de diálogo Build Settings e defini a opção "Run in Xcode as" como "Debug". No projeto Xcode gerado, primeiro procure a string "Start_m". O senhor deve ver o código gerado para o método Start na classe HelloWorld chamada HelloWorld_Start_m3.

Adição de variáveis locais de thread como raízes

Adicione um ponto de interrupção na função HelloWorld_Start_m3 na linha em que Thread_Start_m9 é chamada. Esse método criará um novo thread gerenciado, portanto, esperamos que esse thread seja adicionado ao GC como uma raiz. Podemos ver onde isso acontece explorando os arquivos de cabeçalho libil2cpp que acompanham o Unity. Na instalação do Unity, abra o arquivo Contents/Frameworks/il2cpp/libil2cpp/gc/gc-internal.h. Esse arquivo tem vários métodos com o prefixo il2cpp_gc_ e serve como parte da API entre o tempo de execução do libil2cpp e o coletor de lixo. Observe que essa não é uma API pública, portanto, não chame esses métodos de nenhum código de projeto real. Elas estão sujeitas a alterações ou remoção sem aviso prévio.

Vamos criar um ponto de interrupção no Xcode na função il2cpp_gc_register_thread, usando Debug > Breakpoints > Create Symbolic Breakpoint.

image04

Se o senhor executar o projeto no Xcode, perceberá que o ponto de interrupção é atingido quase imediatamente. Não é possível ver o código-fonte aqui, pois ele foi criado na biblioteca estática de tempo de execução libil2cpp, mas podemos ver na pilha de chamadas que esse thread é criado no método InitializeScriptingBackend, que é executado quando o player é iniciado.

image01

Na verdade, veremos esse ponto de interrupção ser atingido várias vezes, pois o jogador cria cada thread gerenciado usado internamente. Por enquanto, o senhor pode desativar esse ponto de interrupção no Xcode e permitir que o projeto continue. Devemos atingir o ponto de interrupção que definimos anteriormente no método HelloWorld_Start_m3.

Agora estamos prestes a iniciar o thread gerenciado criado pelo nosso código de script, portanto, ative o ponto de interrupção em il2cpp_gc_register_thread novamente. Quando atingimos esse ponto de interrupção, o primeiro thread está esperando para se juntar ao thread criado, mas a pilha de chamadas do thread criado mostra que estamos apenas iniciando-o:

image05

Quando uma thread é registrada no coletor de lixo, o GC trata todos os objetos da pilha local dessa thread como raízes. Vejamos o código gerado para o método que executamos nesse thread (HelloWorld_AnotherThread_m4):

AnyClass_t1 * L_0 = (AnyClass_t1 *)il2cpp_codegen_object_new (AnyClass_t1_il2cpp_TypeInfo_var);
AnyClass__ctor_m0(L_0, /*hidden argument*/NULL);
V_0 = L_0;

Podemos ver uma variável local, L_0, que o GC deve tratar como uma raiz. Durante o (curto) tempo de vida desse thread, essa instância do objeto AnyClass e quaisquer outros objetos a que ele faça referência não poderão ser reutilizados pelo coletor de lixo. As variáveis definidas na pilha são o tipo mais comum de raízes de GC, pois a maioria dos objetos em um programa começa a partir de uma variável local em um método executado em um thread gerenciado.

Quando uma thread sai, a função il2cpp_gc_unregister_thread é chamada para dizer ao GC para parar de tratar os objetos na pilha da thread como raízes. O GC pode então trabalhar na reutilização da memória para o objeto AnyClass representado no código nativo por L_0.

Variáveis estáticas

No entanto, algumas variáveis não vivem em pilhas de chamadas de thread. Essas variáveis são estáticas e também precisam ser tratadas como raízes pelo coletor de lixo.

Quando o IL2CPP estabelece a representação nativa de uma classe, ele agrupa todos os campos estáticos em uma estrutura C++ diferente dos campos de instância da classe. No Xcode, podemos pular para a definição da classe HelloWorld_t2:

struct  HelloWorld_t2  : public MonoBehaviour_t3
{
};

struct HelloWorld_t2_StaticFields{
// AnyClass HelloWorld::staticAnyClass
AnyClass_t1 * ___staticAnyClass_2;
};

Observe que o IL2CPP não usa a palavra-chave estática do C++, pois precisa controlar o layout e a alocação dos campos estáticos para se comunicar adequadamente com o GC. Quando um tipo é usado pela primeira vez em tempo de execução, o código do libil2cpp inicializa o tipo. Parte dessa inicialização envolve a alocação de memória para a estrutura HelloWorld_t2_StaticFields. Essa memória é alocada com uma chamada especial para o GC: il2cpp_gc_alloc_fixed (também no arquivo gc-internal.h).

Essa chamada informa ao coletor de lixo para tratar a memória alocada como uma raiz, e o GC faz isso obedientemente durante todo o tempo de vida do processo. É possível definir um ponto de interrupção na função il2cpp_gc_alloc_fixed no Xcode, mas ela é chamada com bastante frequência (mesmo para esse projeto simples), portanto, o ponto de interrupção não é muito útil.

Objetos GCHandle

Suponha que o senhor não queira usar uma variável estática, mas ainda queira ter um pouco mais de controle sobre quando o coletor de lixo poderá reutilizar a memória de um objeto. Isso geralmente é útil quando o senhor precisa passar um ponteiro para um objeto gerenciado do código gerenciado para o nativo. Se o código nativo assumir a propriedade desse objeto, precisaremos informar ao coletor de lixo que o código nativo agora é uma raiz em seu gráfico de objetos. Isso funciona com o uso de um objeto gerenciado especial chamado GCHandle.

A criação de um GCHandle informa ao código de tempo de execução que um determinado objeto gerenciado deve ser tratado como uma raiz no GC, de modo que ele e todos os objetos aos quais faz referência não sejam reutilizados. No IL2CPP, podemos ver a API de baixo nível para fazer isso no arquivo Contents/Frameworks/il2cpp/libil2cpp/gc/GCHandle.h. Novamente, essa não é uma API pública, mas é divertido investigar. Vamos colocar um ponto de interrupção na função GCHandle::New. Se deixarmos o projeto continuar, veremos essa pilha de chamadas:

image00

Observe que o código gerado para nosso método Start está chamando GCHandle_Alloc_m11, que eventualmente cria um GCHandle e informa ao coletor de lixo que temos um novo objeto raiz.

Conclusão

Examinamos alguns métodos internos da API para ver como o tempo de execução do IL2CPP interage com o coletor de lixo, informando-o sobre quais objetos são as raízes que devem ser preservadas. Observe que não falamos nada sobre qual coletor de lixo o IL2CPP usa. Atualmente, ele está usando o GC Boehm-Demers-Weiser, mas trabalhamos muito para isolar o coletor de lixo atrás de uma interface limpa. Atualmente, temos planos de pesquisar a integração do coletor de lixo CoreCLR de código aberto. Ainda não temos uma data de lançamento definida para essa integração, mas observe nosso roteiro público para obter atualizações.

Como de costume, estamos apenas arranhando a superfície da integração do GC no IL2CPP. Recomendo que o senhor explore mais sobre como o IL2CPP e o GC interagem. Por favor, compartilhe também suas percepções.

Na próxima vez, encerraremos a série de aspectos internos do IL2CPP analisando como testamos o código IL2CPP.