Demonstração de coleta de lixo mais rápida que o gerenciamento manual de memória

23

Eu li em muitos lugares (diabos, eu mesmo escrevi) que a coleta de lixo pode (teoricamente) ser mais rápida que o gerenciamento manual de memória.

No entanto, mostrar é muito mais difícil de encontrar do que dizer.
Na verdade, nunca vi nenhum código que demonstre esse efeito em ação.

Alguém tem (ou sabe onde posso encontrar) código que demonstra essa vantagem de desempenho?

Mehrdad
fonte
5
o problema com o GC é que a maioria das implementações não é determinística, portanto, duas execuções podem ter resultados muito diferentes, sem mencionar que é difícil isolar as variáveis ​​certas para comparar
catraca anormal
@ratchetfreak: Se você souber de alguns exemplos que são apenas mais rápidos (digamos) 70% das vezes, tudo bem comigo também. Deve haver alguma maneira de comparar os dois, em termos de taxa de transferência, pelo menos (a latência provavelmente não funcionaria).
Mehrdad
1
Bem, isso é um pouco complicado, porque você sempre pode fazer manualmente qualquer coisa que dê ao GC uma vantagem sobre o que você fez manualmente. Talvez seja melhor restringir isso às ferramentas manuais "padrão" de gerenciamento de memória (malloc () / free (), ponteiros próprios, ponteiros compartilhados com refcount, ponteiros fracos, sem alocadores personalizados)? Ou, se você permitir alocadores personalizados (que podem ser mais realistas ou menos realistas, dependendo do tipo de programador que você assume), imponha restrições ao esforço desses alocadores. Caso contrário, a estratégia manual "copiar o que o GC faz neste caso" é sempre pelo menos tão rápida quanto o GC.
1
"Copie o que o GC faz" não quis dizer "crie seu próprio GC" (observe que isso é teoricamente possível no C ++ 11 e além, o que introduz suporte opcional para um GC). Eu quis dizer, como escrevi anteriormente no mesmo comentário, "fazer o que dá ao GC uma vantagem sobre o que você fez manualmente". Por exemplo, se a compactação do tipo Cheney ajudar muito esse aplicativo, você poderá implementar manualmente um esquema de alocação + compactação semelhante, com ponteiros inteligentes personalizados para lidar com a correção do ponteiro. Além disso, com técnicas como uma pilha de sombras, você pode fazer a busca raiz em C ou C ++, à custa de trabalho extra.
1
@ Ike: Está tudo bem. Veja por que eu fiz a pergunta? Esse foi o ponto principal da minha pergunta - as pessoas apresentam todos os tipos de explicações que deveriam fazer sentido, mas todo mundo tropeça quando você pede que forneçam uma demonstração que comprove que o que dizem é correto na prática. O ponto principal desta questão era mostrar de uma vez por todas que isso pode realmente acontecer na prática.
Mehrdad

Respostas:

26

Veja http://blogs.msdn.com/b/ricom/archive/2005/05/10/416151.aspx e siga todos os links para ver Rico Mariani x Raymond Chen (ambos programadores muito competentes da Microsoft). . Raymond melhoraria o não gerenciado, Rico responderia otimizando a mesma coisa nos gerenciados.

Com um esforço de otimização praticamente zero, as versões gerenciadas começaram muitas vezes mais rápido que o manual. Eventualmente, o manual superou o gerenciado, mas apenas otimizando para um nível que a maioria dos programadores não gostaria de ir. Em todas as versões, o uso de memória do manual foi significativamente melhor que o gerenciado.

btilly
fonte
+1 por citar um exemplo real com código :), embora o uso adequado de construções C ++ (como swap) não seja tão difícil e provavelmente o levará até lá com bastante facilidade em termos de desempenho ...
Mehrdad
5
Você pode superar Raymond Chen no desempenho. Estou confiante de que não posso, a menos que ele esteja fora disso por estar doente, estou trabalhando muitas vezes mais e tive sorte. Não sei por que ele não escolheu a solução que você escolheria. Estou certo de que ele tinha razões para isso
btilly
Copiei o código de Raymond aqui e, para comparar, escrevi minha própria versão aqui . O arquivo ZIP que contém o arquivo de texto está aqui . No meu computador, o meu roda em 14 ms e o Raymond em 21 ms. A menos que eu tenha feito algo errado (o que é possível), o código de 215 linhas é 50% mais lento que minha implementação de 48 linhas, mesmo sem o uso de arquivos mapeados na memória ou conjuntos de memória personalizados (que ele usou). O meu é metade do tempo que a versão C #. Eu fiz errado ou você observa a mesma coisa?
Mehrdad
1
@Mehrdad Retirando uma cópia antiga do gcc neste laptop, posso relatar que nem o seu código nem o dele serão compilados, muito menos fazer alguma coisa com ele. O fato de eu não estar no Windows provavelmente explica isso. Mas vamos supor que seus números e código estejam corretos. Eles executam o mesmo em um compilador e computador de uma década? (Veja quando o blog foi escrito.) Talvez, talvez não. Vamos supor que eles são, que ele (sendo um programador C) não sabia como usar C ++ corretamente, etc. Com o que nos resta?
btilly
1
Ficamos com um programa C ++ razoável, que pode ser traduzido em memória gerenciada e acelerado. Mas onde a versão C ++ pode ser otimizada e acelerada ainda mais. O que todos concordamos é o padrão geral que sempre acontece quando o código gerenciado é mais rápido que o não gerenciado. No entanto, ainda temos um exemplo concreto de código razoável de um bom programador que foi mais rápido em uma versão gerenciada.
btilly
5

A regra geral é que não há almoços grátis.

O GC elimina a dor de cabeça do gerenciamento manual de memória e reduz a probabilidade de cometer erros. Existem algumas situações em que uma estratégia específica de GC é a solução ideal para o problema. Nesse caso, você não pagará nenhuma penalidade por usá-la. Mas há outros onde outras soluções serão mais rápidas. Como você sempre pode simular abstrações mais altas de um nível mais baixo, mas não o contrário, pode provar efetivamente que não há como abstrações mais altas serem mais rápidas do que as mais baixas no caso geral.

O GC é um caso especial de gerenciamento manual de memória

Pode ser muito trabalhoso ou mais propenso a erros para obter melhor desempenho manualmente, mas essa é uma história diferente.

Guy Sirton
fonte
1
Isso não faz sentido para mim. Para dar alguns exemplos concretos: 1) os alocadores e as barreiras de gravação nos GCs de produção são montadores escritos à mão porque C é muito ineficiente; portanto, como você superará isso de C; e 2) a eliminação da chamada de cauda é um exemplo de otimização feito em linguagens de alto nível (funcionais) que não são feitas pelo compilador C e, portanto, não podem ser feitas em C. A caminhada em pilha é outro exemplo de algo feito abaixo do nível C por linguagens de alto nível.
quer
2
1) Eu precisaria ver o código específico para comentar, mas se os alocadores / barreiras escritos à mão no assembler forem mais rápidos, use o assembler escrito à mão. Não tenho certeza do que isso tem a ver com o GC. 2) Dê uma olhada aqui: stackoverflow.com/a/9814654/441099 a questão não é se algum idioma não-GC pode fazer a eliminação da recursão de cauda para você. O ponto é que você pode transformar seu código para ser o mais rápido ou mais rápido. Se o compilador de algum idioma específico pode fazer isso por você automaticamente é uma questão de conveniência. Em uma abstração baixa o suficiente, você sempre pode fazer isso sozinho, se desejar.
Guy Sirton 27/01
1
Esse exemplo de chamada final em C funciona apenas para o caso especial de uma função que se chama. C não pode lidar com o caso geral de funções que se chamam. Passar para o montador e assumir tempo infinito para o desenvolvimento é um tarpit de Turing.
precisa
3

É fácil construir uma situação artificial em que o GC é infinitamente mais eficiente que os métodos manuais - basta organizar que haja apenas uma "raiz" para o coletor de lixo e que tudo seja lixo, para que a etapa do GC seja concluída instantaneamente.

Se você pensar bem, esse é o modelo usado ao coletar a memória alocada para um processo. O processo morre, tudo que é memória é lixo, terminamos. Mesmo em termos práticos, um processo que inicia, executa e morre sem deixar rastro pode ser mais eficiente do que aquele que inicia e executa para sempre.

Para programas práticos, escritos em idiomas com coleta de lixo, a vantagem da coleta de lixo não é a velocidade, mas a correção e a simplicidade.

ddyer
fonte
Se é fácil construir um exemplo artificial, você se importaria em mostrar um exemplo simples?
Mehrdad 04/07/2013
1
@Mehrdad Ele explicou uma questão simples. Escreva um programa em que a versão do GC não execute o lixo antes de sair. A versão gerenciada pela memória manual será mais lenta porque estava explicitamente rastreando e liberando coisas.
btilly
3
@ btilly: "Escreva um programa em que a versão do GC não execute o lixo antes de sair." ... não executar a coleta de lixo em primeiro lugar é um vazamento de memória devido à falta de um GC em funcionamento, não uma melhoria de desempenho devido à presença de um GC! É como chamar abort()C ++ antes da saída do programa. É uma comparação sem sentido; você nem coleta lixo, apenas deixa a memória vazar. Você não pode dizer que a coleta de lixo é mais rápida (ou mais lenta) se você não estiver coleta de lixo para começar ...
Mehrdad
Para dar um exemplo extremo, você teria que definir um sistema completo com seu próprio gerenciamento de heap e heap, que seria um ótimo projeto para o aluno, mas grande demais para caber nessa margem. Você se sairia bem escrevendo um programa que aloca e desaloca matrizes de tamanho aleatório, de uma maneira projetada para ser estressante para os métodos de gerenciamento de memória não-gc.
Ddyer 4/07
3
@Mehrdad Não é assim. O cenário é que a versão do GC nunca atingiu o limite no qual teria executado, não que teria falhado em executar corretamente em um conjunto de dados diferente. Isso será trivialmente muito bom para a versão GC, embora não seja um bom preditor de desempenho eventual.
btilly
2

Deve-se considerar que o GC não é apenas uma estratégia de gerenciamento de memória; também exige todo o design do ambiente de linguagem e tempo de execução, que impõe custos (e benefícios). Por exemplo, uma linguagem que suporte o GC deve ser compilada de forma que os ponteiros não possam ser ocultados do coletor de lixo e geralmente onde não possam ser construídos, exceto por primitivas do sistema cuidadosamente gerenciadas. Outra consideração é a dificuldade de manter as garantias de tempo de resposta, pois o GC impõe algumas etapas que precisam ser executadas até a conclusão.

Conseqüentemente, se você tiver um idioma que é coletado de lixo e comparar a velocidade com a memória gerenciada manualmente no mesmo sistema, ainda precisará pagar a sobrecarga para oferecer suporte à coleta de lixo, mesmo que não esteja sendo usado.

ddyer
fonte
2

Mais rápido é duvidoso. No entanto, pode ser ultra-rápido, imperceptível ou mais rápido se for suportado por hardware. Havia projetos como esse para máquinas LISP há muito tempo. Um deles incorporou o GC no subsistema de memória do hardware, de modo que a CPU principal não sabia que estava lá. Como muitos projetos posteriores, o GC funcionava simultaneamente com o processador principal, com pouca ou nenhuma necessidade de pausas. Um design mais moderno são as máquinas Azul Systems Vega 3 que executam o código Java muito mais rápido do que a JVM, usando processadores criados especificamente para uso e um GC sem pausa. Pesquise no Google se quiser saber a rapidez com que o GC (ou Java) pode ser.

Nick P
fonte
2

Eu trabalhei bastante nisso e descrevi algumas delas aqui . Comparei o Boehm GC em C ++, alocando usando, mallocmas não liberando, alocando e liberando usando freee um GC de região de marca personalizado, escrito em C ++ all vs o GC de estoque da OCaml, executando um solucionador de n-rainhas baseado em lista. O GC do OCaml foi mais rápido em todos os casos. Os programas C ++ e OCaml foram escritos deliberadamente para executar as mesmas alocações na mesma ordem.

Obviamente, você pode reescrever os programas para resolver o problema usando apenas números inteiros de 64 bits e sem alocações. Embora mais rápido que isso derrotasse o objetivo do exercício (que era prever o desempenho de um novo algoritmo de GC, eu estava trabalhando usando um protótipo construído em C ++).

Passei muitos anos na indústria portando código C ++ real para idiomas gerenciados. Em quase todos os casos, observei melhorias substanciais no desempenho, muitas das quais provavelmente devido ao GC superando o gerenciamento manual de memória. A limitação prática não é o que pode ser alcançado em uma marca de microbench, mas o que pode ser realizado antes de um prazo e as linguagens baseadas em GC oferecem melhorias de produtividade tão grandes que eu nunca olhei para trás. Eu ainda uso C e C ++ em dispositivos incorporados (microcontroladores), mas mesmo isso está mudando agora.

Jon Harrop
fonte
+1 obrigado. Onde podemos ver e executar o código de referência?
Mehrdad 27/01
O código está espalhado pelo local. Publiquei a versão da região de marca aqui: groups.google.com/d/msg/…
Jon Harrop
1
Existem resultados para threads seguros e não seguros lá.
precisa saber é o seguinte
1
@ Mehrdad: "Você eliminou essas fontes potenciais de erro?". Sim. O OCaml possui um modelo de compilação muito simples, sem otimizações, como análise de escape. A representação do fechamento do OCaml é realmente substancialmente mais lenta que a solução C ++, portanto, ele deve realmente usar um costume List.filtercomo o C ++. Mas, sim, você certamente está certo de que algumas operações de RC podem ser eliminadas. No entanto, o maior problema que vejo na natureza é que as pessoas não têm tempo para realizar essas otimizações manualmente em grandes bases de códigos industriais.
precisa
2
Sim absolutamente. Nenhum esforço adicional para escrever, mas escrever código não é o gargalo do C ++. Manter código é. Manter o código com esse tipo de complexidade incidental é um pesadelo. A maioria das bases de códigos industriais são milhões de linhas de código. Você simplesmente não quer ter que lidar com isso. Eu já vi pessoas converterem tudo em shared_ptrapenas para corrigir erros de simultaneidade. O código é muito mais lento, mas, ei, agora funciona.
precisa
-1

Esse exemplo necessariamente tem um esquema de alocação manual de memória ruim.

Suponha o melhor coletor de lixo GC. Possui internamente métodos para alocar memória, determinar qual memória pode ser liberada e métodos para finalmente liberá-la. Juntos, eles levam menos tempo do que todos GC; algum tempo é gasto nos outros métodos do GC.

Agora considere um alocador manual que use o mesmo mecanismo de alocação que GCe cuja free()chamada apenas reserve a memória para ser liberada pelo mesmo método que GC. Não possui uma fase de verificação nem possui nenhum dos outros métodos. Leva necessariamente menos tempo.

MSalters
fonte
2
Um coletor de lixo geralmente pode liberar muitos objetos, sem precisar colocar a memória em um estado útil após cada um. Considere a tarefa de remover de uma lista de matrizes todos os itens que atendem a um determinado critério. A remoção de um único item de uma lista de itens N é O (N); removendo itens M de uma lista de N, um de cada vez é O (M * N). A remoção de todos os itens que atendem a um critério em uma única passagem pela lista, no entanto, é O (1).
supercat
@ supercat: freetambém pode coletar lotes. (E, claro, a remoção de todos os itens que tiveram um critério ainda é O (N), mesmo que apenas por causa da própria lista travessia)
MSalters
A remoção de todos os itens que atendem a um critério é pelo menos O (N). Você está certo de que freepoderia operar em modo de coleta em lote se cada item de memória tivesse um sinalizador associado a ele, embora o GC ainda possa sair à frente em algumas situações. Se alguém possui M referências que identificam L itens distintos de um conjunto de N itens, o tempo para remover todas as referências às quais não existe referência e consolidar o restante é O (M) em vez de O (N). Se houver M espaço extra disponível, a constante de escala pode ser bem pequena. Além disso, compactification em um sistema de GC não digitalização exige ...
supercat
@ supercat: Bem, certamente não é O (1), como diz a sua última frase no primeiro comentário.
MSalters
1
@MSalters: "E o que impediria um esquema determinístico de ter um berçário?". Nada. O coletor de lixo de rastreamento do OCaml é determinístico e usa um berçário. Mas não é "manual" e acho que você está usando incorretamente a palavra "determinista".
precisa saber é o seguinte