Após procurar várias respostas em um estouro de pilha, fica claro que alguns idiomas compilados nativamente têm coleta de lixo . Mas não está claro para mim como exatamente isso funcionaria.
Entendo como a coleta de lixo pode funcionar com uma linguagem interpretada. O coletor de lixo simplesmente rodaria ao lado do intérprete e excluiria objetos não utilizados e inacessíveis da memória do programa. Ambos estão correndo juntos.
Como isso funcionaria com as linguagens compiladas? Meu entendimento é que, uma vez que o compilador tenha compilado o código-fonte para o código de destino - especificamente o código da máquina nativa - isso é feito. Seu trabalho está terminado. Então, como o programa compilado também pode ser coletado de lixo?
O compilador funciona com a CPU de alguma forma, enquanto o programa é executado para excluir objetos "lixo"? Ou o compilador inclui algum coletor de lixo mínimo no executável do programa compilado.
Acredito que minha última declaração teria mais validade do que a anterior devido a esse trecho desta resposta no Stack Overflow :
Uma dessas linguagens de programação é Eiffel. A maioria dos compiladores Eiffel gera código C por motivos de portabilidade. Este código C é usado para produzir código de máquina por um compilador C padrão. As implementações do Eiffel fornecem GC (e às vezes até GC preciso) para esse código compilado, e não há necessidade de VM. Em particular, o compilador VisualEiffel gerou código de máquina x86 nativo diretamente com suporte total ao GC .
A última declaração parece implicar que o compilador inclui algum programa no executável final que atua como um coletor de lixo enquanto o programa está sendo executado.
A página no site da linguagem D sobre coleta de lixo - que é compilada nativamente e possui um coletor de lixo opcional - também parece sugerir que algum programa em segundo plano seja executado ao lado do programa executável original para implementar a coleta de lixo.
D é uma linguagem de programação de sistemas com suporte para coleta de lixo. Geralmente, não é necessário liberar memória explicitamente. Apenas aloque conforme necessário, e o coletor de lixo retornará periodicamente toda a memória não utilizada para o conjunto de memória disponível.
Se o método mencionado acima for usado, como exatamente funcionaria? O compilador armazena uma cópia de algum programa de coleta de lixo e a cola em cada executável gerado?
Ou eu sou falha no meu pensamento? Em caso afirmativo, quais métodos são usados para implementar a coleta de lixo para linguagens compiladas e como exatamente eles funcionariam?
fonte
malloc()
.Respostas:
A coleta de lixo em um idioma compilado funciona da mesma maneira que em um idioma interpretado. Idiomas como Go usam o rastreamento de coletores de lixo, mesmo que seu código geralmente seja compilado no código da máquina com antecedência.
A coleta de lixo (rastreamento) geralmente começa caminhando pelas pilhas de chamadas de todos os threads que estão em execução no momento. Os objetos nessas pilhas estão sempre ativos. Depois disso, o coletor de lixo percorre todos os objetos apontados por objetos ativos, até que todo o gráfico de objetos ativos seja descoberto.
É claro que isso exige informações extras que idiomas como C não fornecem. Em particular, requer um mapa do quadro de pilha de cada função que contenha as compensações de todos os ponteiros (e provavelmente seus tipos de dados), bem como mapas de todos os layouts de objetos que contêm as mesmas informações.
No entanto, é fácil ver que idiomas que possuem garantias de tipo fortes (por exemplo, se o ponteiro lança para tipos de dados diferentes não são permitidos) podem realmente computar esses mapas em tempo de compilação. Eles simplesmente armazenam uma associação entre endereços de instruções e mapas de quadros de pilha e uma associação entre tipos de dados e mapas de layout de objetos dentro do binário. Essas informações permitem que eles percorram o gráfico do objeto.
O coletor de lixo em si nada mais é do que uma biblioteca vinculada ao programa, semelhante à biblioteca padrão C. Por exemplo, essa biblioteca poderia fornecer uma função semelhante à
malloc()
que executa o algoritmo de coleta se a pressão da memória for alta.fonte
Parece deselegante e estranho, mas sim. O compilador tem uma biblioteca de utilidades inteira, contendo muito mais do que apenas código de coleta de lixo, e as chamadas para essa biblioteca serão inseridas em cada executável que criar. Isso é chamado de biblioteca de tempo de execução e você ficaria surpreso com quantas tarefas diferentes ela normalmente serve.
fonte
malloc()
efree()
não está embutido no idioma, não faz parte do sistema operacional, mas é função nesta biblioteca. Às vezes, o C ++ também é compilado com uma biblioteca de coleta de lixo, mesmo que o idioma não tenha sido projetado com o GC em mente.dynamic_cast
e exceções funcionarem, mesmo se você não adicionar um GC.main()
, e é perfeitamente legal, digamos, acionar um encadeamento de GC nesse código. (Supondo que o GC não seja feito dentro de chamadas de alocação de memória.) Em tempo de execução, o GC realmente precisa apenas saber quais partes de um objeto são ponteiros ou referências a objetos, e o compilador precisa emitir o código para converter uma referência de objeto em um ponteiro. se o GC realocar objetos.crt0.o
(que significa " C R un T ime, o básico do básico"), que fica ligado com cada programa (ou pelo menos a cada programa que não é free-standing ).Essa é uma maneira estranha de dizer "o compilador vincula o programa a uma biblioteca que executa a coleta de lixo". Mas sim, é isso que está acontecendo.
Isso não é nada de especial: os compiladores geralmente vinculam toneladas de bibliotecas nos programas que compilam; caso contrário, os programas compilados não poderiam fazer muito sem reimplementar muitas coisas do zero: até escrever texto na tela / um arquivo / ... requer uma biblioteca.
Mas talvez o GC seja diferente dessas outras bibliotecas, que fornecem APIs explícitas que o usuário chama?
Não: na maioria dos idiomas, as bibliotecas de tempo de execução fazem muito trabalho nos bastidores sem a API voltada ao público, além do GC. Considere estes três exemplos:
Portanto, uma biblioteca de coleta de lixo não é nada especial e, a priori , nada tem a ver com a compilação antecipada de um programa.
fonte
Sua redação está errada. Uma linguagem de programação é uma especificação escrita em algum relatório técnico (para um bom exemplo, consulte R5RS ). Na verdade, você está se referindo a alguma implementação de linguagem específica (que é um software).
(algumas linguagens de programação têm especificações ruins, ou mesmo os ausentes, ou apenas como em conformidade com alguma implementação amostra; ainda assim, uma linguagem de programação define um comportamento - por exemplo, tem uma sintaxe e semântica -, é não um produto de software, mas poderia ser implementado por algum produto de software; muitas linguagens de programação têm várias implementações; em particular, "compilado" é um adjetivo aplicado a implementações - mesmo que algumas linguagens de programação sejam mais facilmente implementadas por intérpretes do que por compiladores.)
Observe que intérpretes e compiladores têm um significado pouco amplo, e algumas implementações de linguagem podem ser consideradas como sendo ambas. Em outras palavras, há um continuum no meio. Leia o Dragon Book mais recente e pense sobre bytecode , compilação JIT , emitindo código C dinamicamente compilado em algum "plugin" e depois dlopen (3) -ed pelo mesmo processo (e nas máquinas atuais, isso é rápido o suficiente para ser compatível com um REPL interativo , veja isso )
Eu recomendo fortemente a leitura do manual do GC . É necessário um livro inteiro para responder . Antes disso, leia a wiki da Garbage Collection (que eu assumo que você leu antes de ler abaixo).
O sistema de tempo de execução da implementação da linguagem compilada contém o coletor de lixo, e o compilador está gerando um código adequado ao sistema de tempo de execução específico. Em particular, as primitivas de alocação (são compiladas no código da máquina que) chamarão (ou poderão) o sistema de tempo de execução.
Apenas emitindo código de máquina que usa (e é "amigável" e "compatível com") o sistema de tempo de execução.
Observe que você pode encontrar várias bibliotecas de coleta de lixo, em particular o Boehm GC , o MPS de Ravenbrook ou mesmo o meu (não mantido) Qish . E codificação de um simples GC não é muito difícil (no entanto, a depuração é mais difícil, e codificação de um competidor GC é difícil ).
Em alguns casos, o compilador usaria um GC conservador (como o Boehm GC ). Então, não há muito para codificar. O GC conservador (quando o compilador chama sua rotina de alocação ou toda a rotina do GC) às vezes varre toda a pilha de chamadas e assume que qualquer zona de memória (indiretamente) acessível a partir da pilha de chamadas está ativa. Isso é chamado de GC conservador porque as informações de digitação são perdidas: se um número inteiro na pilha de chamadas parecer algum endereço, ele será seguido etc.
Em outros casos (mais difíceis), o tempo de execução fornece uma coleta de lixo de cópia geracional (um exemplo típico é o compilador Ocaml, que compila o código Ocaml para o código da máquina usando esse GC). A questão é encontrar com precisão nas pilhas de chamadas todos os indicadores, e alguns deles são movidos pelo GC. Em seguida, o compilador gera metadados que descrevem os quadros da pilha de chamadas, que o tempo de execução usa. Portanto, as convenções de chamada e a ABI estão se tornando específicas para essa implementação (ou seja, compilador) e sistema de tempo de execução.
Em alguns casos, o código de máquina gerado pelo compilador (na verdade, até os fechamentos apontando para ele) é o próprio lixo coletado . Esse é o caso da SBCL (uma boa implementação do Common Lisp), que gera código de máquina para cada interação REPL . Isso também requer alguns metadados que descrevem o código e os quadros de chamada usados dentro dele.
Tipo de. No entanto, o sistema de tempo de execução pode ser uma biblioteca compartilhada, etc. Às vezes (no Linux e em vários outros sistemas POSIX), pode até ser um interpretador de script, por exemplo, passado para execve (2) com um shebang . Ou um intérprete ELF , consulte elf (5) e
PT_INTERP
etc.Hoje, a maioria dos compiladores de idiomas com coleta de lixo (e seu sistema de tempo de execução) são hoje software livre . Então faça o download do código fonte e estude-o.
fonte
Array#[]
é O (1) pior caso,Hash#[]
é O (1) pior caso amortizado). E por último mas não menos importante: o cérebro de matz.Já existem boas respostas, mas gostaria de esclarecer alguns mal-entendidos por trás dessa pergunta.
Não existe "linguagem compilada nativamente" em si. Por exemplo, o mesmo código Java foi interpretado (e depois compilado parcialmente em tempo de execução) no meu telefone antigo (Java Dalvik) e é (antecipado) compilado no meu novo telefone (ART).
A diferença entre executar código nativamente e interpretado é muito menos rigorosa do que parece ser. Ambos precisam de algumas bibliotecas de tempo de execução e de algum sistema operacional para funcionar (*). O código interpretado precisa de um intérprete, mas o intérprete é apenas uma parte do tempo de execução. Mas mesmo isso não é rigoroso, pois você pode substituir o intérprete por um compilador (just-in-time). Para obter o desempenho máximo, você pode querer ambos (o Java Runtime para desktop contém um intérprete e dois compiladores).
Não importa como executar o código, ele deve se comportar da mesma maneira. Alocar e liberar memória é uma tarefa para o tempo de execução (assim como abrir arquivos, iniciar threads etc.). No seu idioma, você apenas escreve
new X()
ou afins. A especificação da linguagem diz o que deve acontecer e o tempo de execução o faz.Alguma memória livre é alocada, o construtor é chamado etc. Quando não há memória suficiente, o coletor de lixo é chamado. Como você já está no tempo de execução, que é um trecho de código nativo, a existência de um intérprete não importa.
Realmente não há conexão direta entre a interpretação de código e a coleta de lixo. É que linguagens de baixo nível como C são projetadas para velocidade e controle refinado de tudo, o que não se encaixa bem com a idéia de código não nativo ou um coletor de lixo. Portanto, há apenas uma correlação.
Isso era muito verdadeiro nos velhos tempos, onde, por exemplo, o interpretador Java era muito lento e o coletor de lixo, bastante ineficiente. Atualmente, as coisas são muito diferentes e falar sobre uma linguagem interpretada perdeu algum sentido.
(*) Pelo menos quando se fala de código de uso geral, deixando de lado os gerenciadores de inicialização e similares.
fonte
java myprog
é tão ou pouco nativo quantogrep myname /etc/passwd
ould.so myprog
: é um executável (o que isso significa) que recebe um argumento e executa operações com os dados.Os detalhes variam entre as implementações, mas geralmente é uma combinação do seguinte:
No GC incremental e simultâneo, o código compilado e o GC precisam cooperar para manter alguns invariantes. Por exemplo, em um coletor de cópias, o GC trabalha copiando dados ativos do espaço A para o espaço B, deixando para trás o lixo. Para o próximo ciclo, vira A e B e repete. Portanto, uma regra pode ser garantir que, sempre que o programa do usuário tente se referir a um objeto no espaço A, isso seja detectado e o objeto seja copiado imediatamente no espaço B, onde o programa pode continuar acessando. Um endereço de encaminhamento fica no espaço A para indicar ao GC que isso aconteceu, para que outras referências ao objeto sejam atualizadas à medida que são rastreadas. Isso é conhecido como uma "barreira de leitura".
Os algoritmos de GC são estudados desde os anos 60, e há extensa literatura sobre o assunto. Google se você quiser mais informações.
fonte