A tentativa inicial de remover o Python GIL resultou em um desempenho ruim: por quê?

13

Esta postagem do criador do Python, Guido Van Rossum, menciona uma tentativa inicial de remover o GIL do Python:

Isso já foi tentado antes, com resultados decepcionantes, e é por isso que estou relutante em me esforçar muito. Em 1999, Greg Stein (com Mark Hammond?) Produziu uma bifurcação de Python (1,5 acredito) que removeu o GIL, substituindo-o por bloqueios refinados em todas as estruturas de dados mutáveis. Ele também enviou patches que removeram muitas das dependências nas estruturas globais de dados mutáveis, que eu aceitei. No entanto, após o benchmarking, foi mostrado que, mesmo na plataforma com a primitiva de bloqueio mais rápida (Windows na época), a execução de thread único desacelerava quase duas vezes, o que significa que, em duas CPUs, você poderia trabalhar um pouco mais feito sem o GIL do que em uma única CPU com o GIL. Isso não foi suficiente, e o remendo de Greg desapareceu no esquecimento. (Veja o artigo de Greg sobre o desempenho.)

Mal posso argumentar com os resultados reais, mas realmente me pergunto por que isso aconteceu. Presumivelmente, a principal razão pela qual a remoção do GIL do CPython é tão difícil é por causa do sistema de gerenciamento de memória de contagem de referência. Um programa típico Python chamará Py_INCREFe Py_DECREFmilhares ou milhões de vezes, tornando-se um ponto de discórdia chave se estávamos para quebrar bloqueios em torno dele.

Mas não entendo por que adicionar primitivas atômicas atrasaria um único programa encadeado. Suponha que apenas modificamos o CPython para que a variável refcount em cada objeto Python seja uma primitiva atômica. E então fazemos apenas um incremento atômico (instruções de busca e adição) quando precisamos incrementar a contagem de referência. Isso faria com que a referência do Python contasse com segurança para threads e não deveria ter nenhuma penalidade de desempenho em um aplicativo de thread único, porque não haveria contenção de bloqueio.

Mas, infelizmente, muitas pessoas que são mais espertas que eu tentaram e falharam, então, obviamente, estou perdendo alguma coisa aqui. O que há de errado com a maneira como estou encarando esse problema?

Siler
fonte
1
Observe que a operação refcount não seria o único local que precisaria de sincronização. A citação menciona "bloqueios refinados em todas as estruturas de dados mutáveis" que, presumo, incluem pelo menos um mutex para cada objeto de lista e dicionário. Além disso, não acho que operações inteiras atômicas sejam tão eficientes quanto o equivalente não atômico, independentemente da contenção. Você tem uma fonte para isso?
simplesmente, porque as operações atômicas são mais lentas que os equivalentes não atômicos. Só porque é uma única instrução não significa que é trivial. Veja isso para alguma discussão
quarta

Respostas:

9

Não estou familiarizado com o garfo de Greg Stein Python, portanto, desconsidere essa comparação como analogia histórica especulativa, se desejar. Mas essa foi exatamente a experiência histórica de muitas bases de código de infraestrutura, passando de implementações single-to multi-threaded.

Essencialmente, todas as implementações do Unix que estudei nos anos 90 - AIX, DEC OSF / 1, DG / UX, DYNIX, HP-UX, IRIX, Solaris, SVR4 e SVR4 MP - passaram por exatamente esse tipo de " bloqueio mais refinado - agora é mais lento !! " problema. Os DBMSs que eu segui - DB2, Ingres, Informix, Oracle e Sybase - também passaram por isso.

Ouvi dizer que "essas mudanças não nos atrasam quando estamos executando o thread único" um milhão de vezes. Isso nunca funciona dessa maneira. O simples ato de verificar condicionalmente "estamos executando multithread, ou não?" adiciona uma sobrecarga real, especialmente em CPUs com pipelines. Operações atômicas e bloqueios de rotação ocasionais adicionados para garantir a integridade das estruturas de dados compartilhadas precisam ser chamados com bastante frequência e são muito lentos. As primitivas de bloqueio / sincronização de primeira geração também foram lentas. A maioria das equipes de implementação eventualmente adiciona várias classes de primitivas, em vários "pontos fortes", dependendo da quantidade de proteção de intertravamento necessária em vários locais. Então eles percebem que onde eles deram um tapa nas primitivas de bloqueio não era realmente o lugar certo, então eles tiveram que criar um perfil, projetar em torno dos gargalos encontrados, e sistematicamente rotacionar. Alguns desses pontos de conflito acabaram acelerando o sistema operacional ou o hardware, mas toda essa evolução levou de 3 a 5 anos, no mínimo. Enquanto isso, as versões MP ou MT estavam mancando, em termos de desempenho.

De outro modo, equipes de desenvolvimento sofisticadas argumentaram que essas desacelerações são basicamente um fato persistente e intratável da vida. A IBM, por exemplo, recusou-se a habilitar o AIX para SMP por pelo menos 5 anos após a competição, insistindo que o encadeamento único era simplesmente melhor. A Sybase usou alguns dos mesmos argumentos. A única razão pela qual algumas equipes surgiram foi que o desempenho de thread único não podia mais ser razoavelmente aprimorado no nível da CPU. Eles foram forçados a optar por MP / MT ou aceitar um produto cada vez menos competitivo.

A simultaneidade ativa é HARD. E é enganoso. Todo mundo se apressa pensando "isso não será tão ruim". Então eles atingem a areia movediça e precisam atravessar. Eu já vi isso acontecer com pelo menos uma dúzia de equipes inteligentes, bem financiadas e de marca. Em geral, pareceu levar pelo menos cinco anos depois de escolher o multiencadeamento para "voltar para onde eles deveriam estar, em termos de desempenho" com os produtos MP / MT; a maioria ainda estava melhorando significativamente a eficiência / escalabilidade do MP / MT mesmo dez anos após a mudança.

Portanto, minha especulação é que, sem o apoio e o apoio do GvR, ninguém assumiu a longa jornada pelo Python e seu GIL. Mesmo se o fizessem hoje, seria o período de tempo do Python 4.x antes que você dissesse "Uau! Estamos realmente superando o problema da MT!"

Talvez haja alguma mágica que separa o Python e seu tempo de execução de todos os outros softwares de infraestrutura com estado - todos os tempos de execução da linguagem, sistemas operacionais, monitores de transações e gerenciadores de banco de dados anteriores. Mas se assim for, é único ou quase. Todos os outros que removeram o equivalente a GIL levaram mais de cinco anos de esforço e investimento comprometidos e duradouros para passar do MT para o MT quente.

Jonathan Eunice
fonte
2
+1 Demorou esse tipo de tempo para multi-thread Tcl com uma equipe bastante pequena de desenvolvedores. O código era seguro para MT antes disso, mas tinha sérios problemas de desempenho, principalmente no gerenciamento de memória (que suspeito ser uma área muito quente para linguagens dinâmicas). A experiência realmente não é transferida para o Python em nada além dos termos mais gerais; os dois idiomas têm modelos de threading completamente diferentes. Apenas ... espere um trabalho árduo e esperar erros estranhos ...
Donal Fellows
-1

Outra hipótese absurda: em 1999, o Linux e outros Unices não tinham uma sincronização de alto desempenho como agora futex(2)( http://en.wikipedia.org/wiki/Futex ). Esses vieram por volta de 2002 (e foram fundidos em 2,6 por volta de 2004).

Como todas as estruturas de dados incorporadas precisam ser sincronizadas, o bloqueio custa muito. Ᶎσᶎ já apontou, que operações atômicas não são necessárias barato.

Sahib
fonte
1
Você tem alguma coisa para apoiar isso? ou isso é quase especulação?
1
A citação do GvR descreve o desempenho "na plataforma com o primitivo de bloqueio mais rápido (Windows na época)", de modo que bloqueios lentos no Linux não são relevantes.