Diferenças relacionadas a estratégias de coleta de lixo ¶
Os coletores de lixo usados ou implementados por PyPy não são baseados em contagem de referências, de modo que os objetos não são liberados instantaneamente quando não são mais alcançáveis. O efeito mais óbvio disto é que os arquivos (e soquetes, etc) não são prontamente fechados quando eles saem do escopo. Para arquivos que são abertos para escrita, os dados podem ser deixados em seus buffers de saída por um tempo, fazendo o arquivo em disco parecer vazio ou truncado. Além disso, você pode atingir o limite do seu SO no número de arquivos abertos simultaneamente.
Se você estiver depurando um caso em que um arquivo no seu programa não é fechadoproperamente, você pode usar a opção -X track-resources
de linha de comando. Se for dada, um ResourceWarning
é produzido para cada arquivo e soquete que o coletor de lixo fecha. O aviso irá conter o rastreamento da pilha da posição onde o arquivo ou socket foi criado, para facilitar a visualização de quais partes do programa não fecham os arquivos explicitamente.
Fixar esta diferença para o CPython é essencialmente impossível sem forçar a abordagem de contagem de referências para a coleta de lixo. O efeito que você esquece no CPython foi claramente descrito como um efeito colateral da teimplementação e não uma decisão de design de linguagem: programas que dependem disto são basicamente falsos. Seria uma restrição muito forte tentar impor o comportamento do CPython em uma especificação de linguagem, dado que ele não tem chance de ser adotado por Jython ou IronPython (ou qualquer outra porta de Python para Java ou.NET).
Even a idéia ingênua de forçar um GC completo quando estamos ficando perigosamente fechados ao limite do SO pode ser muito ruim em alguns casos. Se o seu programa abrir muito arquivos, então ele funcionaria, mas forçar um GCcycle completo a cada n’o arquivo vazado. O valor de n é uma constante, mas o programa pode tomar uma quantidade arbitrária de memória, o que faz um ciclo GC completo ser arbitrariamente longo. O resultado final é que PyPy gastaria anarbitrarily grande fração de seu tempo de execução no GC – diminuindo a execução real, não em 10% nem 100% nem 1000% mas essencialmente por qualquer fator.
Para o melhor de nosso conhecimento este problema não tem melhor solução do que consertar os programas. Se isso ocorrer em código de terceiros, isso significa ir até os autores e explicar o problema a eles: eles precisam fechar seus arquivos abertos para rodar em qualquer implementação não baseada em Python.
Aqui estão alguns detalhes mais técnicos. Este problema afeta o tempo exato no qual __del__
métodos são chamados, o que não é confiável ou oportuno em PyPy (nem Jython ou IronPython). Isso também significa que referências fracas podem permanecer vivas por um pouco mais do que o esperado. Isto faz com que os “proxies fracos” (como retornado por weakref.proxy()
) sejam um pouco menos úteis: eles parecerão permanecer vivos por um pouco mais de tempo em PyPy, euddenly eles estarão realmente mortos, levantando um ReferenceError
no acesso ao texto. Qualquer código que use proxies fracos deve ser capturado com cuidadoReferenceError
em qualquer lugar que os use. (Ou, melhor ainda, não useweakref.proxy()
em absoluto; use weakref.ref()
.)
Note um detalhe na documentação para callbacks weakref:
Se o callback for fornecido e não None, e o weakrefobject retornado ainda estiver vivo, o callback será chamado quando o objeto estiver prestes a ser finalizado.
Existem casos em que, devido à semântica de recontagem do CPython, um weakrefdies imediatamente antes ou depois dos objectos para os quais aponta (normalmente com alguma referência circular). Se acontecer de morrer logo a seguir, será invocada a chamada de retorno. Em um caso similar em PyPy, tanto o objeto quanto o weakref serão considerados mortos ao mesmo tempo, e a chamada de retorno não será invocada. (Edição #2030)
Existem algumas implicações extras da diferença no GC. Mais notavelmente, se um objeto tem um __del__
, o __del__
nunca é chamado mais de uma vez em PyPy; mas CPython chamará o mesmo __del__
várias vezes se o objeto for ressuscitado e morrer novamente (pelo menos ele é confiável assim CPythons mais inolder; CPythons mais novos tentam chamar os destruidores não mais de uma vez, mas há contra-exemplos). Os métodos __del__
são chamados na ordem “certa” se estiverem em objetos apontando uns para os outros, como no CPython, mas ao contrário do CPython, se houver um ciclo morto de objetos se referindo uns aos outros, seus métodos __del__
são chamados de qualquer forma; CPython os colocaria na lista garbage
do módulo gc
. Mais informações estão disponíveis no blog .
Note que esta diferença pode aparecer indiretamente em alguns casos. Forexample, um gerador deixado pendente no meio é – mais uma vez – recolhido mais tarde em PyPy do que em CPython. Você pode ver a diferença se a palavra-chave yield
em que ela está suspensa estiver fechada em um bloco try:
ou um bloco with:
. Isto aparece por exemplo na edição 736.
Usando o GC padrão (chamado minimark
), a função incorporada id()
funciona como no CPython. Com outros GCs ele retorna números que não são endereços reais (porque um objeto pode se mover várias vezes) e chamá-lo muito pode levar a problemas de performance.
Note que se você tiver uma longa cadeia de objetos, cada um com uma referência para o próximo, e cada um com um __del__
, o GC do PyPy terá um desempenho ruim. No lado positivo, na maioria dos outros casos, os benchmarks têm mostrado que os PyPy’sGCs têm um desempenho muito melhor que os CPython’s.
Outra diferença é que se você adicionar um __del__
a uma classe existente ela não será chamada:
>>>> class A(object):.... pass....>>>> A.__del__ = lambda self: None__main__:1: RuntimeWarning: a __del__ method added to an existing type will not be called
Even mais obscuro: o mesmo é verdade, para classes do estilo antigo, se você anexar o __del__
a uma instância (mesmo no CPython isso não funciona com classes do estilo novo). Você recebe um RuntimeWarning em PyPy. Para corrigir estes casos, certifique-se que existe um método __del__
na classe para começar com (mesmo contendo apenas pass
; substituí-lo ou substituí-lo mais tarde funciona bem).
Primeira nota: CPython tenta fazer um gc.collect()
automaticamente quando o programa termina; não em PyPy. (É possível tanto no CPython como no PyPy todesign um caso em que vários gc.collect()
são necessários antes de todos os objectsdie. Isto faz com que a abordagem do CPython só funcione “a maior parte do tempo” de qualquer forma.)