I primi tentativi di rimozione di Python GIL hanno prodotto prestazioni scadenti: perché?


13

Questo post del creatore di Python, Guido Van Rossum, menziona un primo tentativo di rimuovere il GIL da Python:

Questo è già stato provato, con risultati deludenti, motivo per cui sono riluttante a impegnarmi molto da solo. Nel 1999 Greg Stein (con Mark Hammond?) Produsse un fork di Python (1.5 credo) che rimosse il GIL, sostituendolo con blocchi a grana fine su tutte le strutture di dati mutabili. Ha anche inviato patch che hanno rimosso molte delle dipendenze da strutture di dati mutabili globali, che ho accettato. Tuttavia, dopo il benchmarking, è stato dimostrato che anche sulla piattaforma con la primitiva di blocco più veloce (Windows al momento) ha rallentato l'esecuzione a thread singolo quasi due volte, il che significa che su due CPU, potresti ottenere solo un po 'più di lavoro fatto senza GIL che su una singola CPU con GIL. Questo non era abbastanza e la patch di Greg scomparve nell'oblio. (Vedi il commento di Greg sulla performance.)

Difficilmente posso discutere con i risultati reali, ma mi chiedo davvero perché sia ​​successo. Presumibilmente, il motivo principale per cui la rimozione di GIL da CPython è così difficile è dovuto al sistema di gestione della memoria di conteggio dei riferimenti. Un tipico programma Python chiamerà Py_INCREFe Py_DECREFmigliaia o milioni di volte, rendendolo un punto di contesa chiave se dovessimo avvolgere i blocchi attorno ad esso.

Ma non capisco perché l'aggiunta di primitivi atomici possa rallentare un singolo programma thread. Supponiamo di aver appena modificato CPython in modo che la variabile refcount in ogni oggetto Python fosse una primitiva atomica. E poi facciamo solo un incremento atomico (istruzioni fetch-and-add) quando dobbiamo incrementare il conteggio dei riferimenti. Ciò renderebbe il conteggio dei riferimenti Python sicuro per i thread e non dovrebbe comportare alcuna riduzione delle prestazioni in un'applicazione a thread singolo, poiché non vi sarebbero contese di blocco.

Ma ahimè, molte persone più intelligenti di me hanno provato e fallito, quindi ovviamente mi manca qualcosa qui. Cosa c'è di sbagliato nel modo in cui sto guardando questo problema?


1
Si noti che l'operazione di conteggio non sarebbe l'unico posto che necessita di sincronizzazione. La citazione menziona "blocchi a grana fine su tutte le strutture di dati mutabili" che presumo includa almeno un mutex per ogni elenco e oggetto del dizionario. Inoltre, non credo che le operazioni con numeri interi atomici siano efficienti quanto l'equivalente non atomico indipendentemente dalla contesa, hai una fonte per questo?

semplicemente, perché le operazioni atomiche sono più lente degli equivalenti non atomici. Solo perché è una singola istruzione non significa che sia banale sotto il cofano. Vedi questo per qualche discussione
Móż

Risposte:


9

Non ho familiarità con la forcella di Python Greg Stein, quindi sconsiglia questo confronto come analogia storica speculativa se lo desideri. Ma questa è stata esattamente l'esperienza storica di molte basi di codice dell'infrastruttura che passano da implementazioni a thread singolo a thread multipli.

Essenzialmente ogni implementazione di Unix che ho studiato negli anni '90 - AIX, DEC OSF / 1, DG / UX, DYNIX, HP-UX, IRIX, Solaris, SVR4 e SVR4 MP - hanno attraversato esattamente questo tipo di "abbiamo inserito bloccaggio a grana fine - ora è più lento !! " problema. I DBMS che ho seguito - DB2, Ingres, Informix, Oracle e Sybase - lo hanno seguito tutti.

Ho sentito "questi cambiamenti non ci rallenteranno quando eseguiamo il thread singolo" un milione di volte. Non funziona mai così. Il semplice atto del controllo condizionale "stiamo eseguendo il multithreading o no?" aggiunge un sovraccarico reale, specialmente su CPU con pipeline elevate. Operazioni atomiche e spin-lock occasionali aggiunti per garantire l'integrità delle strutture di dati condivise devono essere chiamati abbastanza spesso e sono molto lenti. Anche le primitive di blocco / sincronizzazione di prima generazione erano lente. La maggior parte dei team di implementazione alla fine aggiunge diverse classi di primitivi, in vari "punti di forza", a seconda di quanta protezione di interblocco era necessaria in vari punti. Quindi si rendono conto che dove inizialmente hanno schiaffeggiato le primitive di bloccaggio non era davvero il posto giusto, quindi hanno dovuto profilare, progettare attorno ai colli di bottiglia trovati, e sistematicamente roto-till. Alcuni di questi punti critici alla fine ottennero un'accelerazione dell'hardware o del sistema operativo, ma l'intera evoluzione ha richiesto 3-5 anni, minimo indispensabile. Nel frattempo, le versioni MP o MT erano inerte, dal punto di vista delle prestazioni.

Squadre di sviluppo altrimenti sofisticate hanno sostenuto che tali rallentamenti sono fondamentalmente un fatto persistente e intrattabile della vita. IBM, ad esempio, ha rifiutato di abilitare AIX SMP per almeno 5 anni dopo la competizione, fermamente convinto che il single-threading fosse semplicemente migliore. Sybase ha usato alcuni degli stessi argomenti. L'unico motivo per cui alcuni dei team alla fine sono arrivati ​​è stato che le prestazioni a thread singolo non potevano più essere ragionevolmente migliorate a livello di CPU. Sono stati costretti a diventare MP / MT o ad accettare di avere un prodotto sempre più competitivo.

La concorrenza attiva è DIFFICILE. Ed è ingannevole. Tutti si precipitano dentro pensando "questo non sarà così male". Quindi hanno colpito le sabbie mobili e sono costretti a sgattaiolare via. L'ho visto accadere con almeno una dozzina di team intelligenti, ben finanziati e di marca. In genere, ci sono voluti almeno cinque anni dopo aver scelto il multi-thread per "tornare a dove dovrebbero essere, in termini di prestazioni" con i prodotti MP / MT; la maggior parte stava ancora migliorando significativamente l'efficienza / scalabilità di MP / MT anche dieci anni dopo aver effettuato il passaggio.

Quindi la mia speculazione è che, in assenza dell'appoggio e del supporto di GvR, nessuno ha assunto il lungo fardello per Python e il suo GIL. Anche se lo facessero oggi, sarebbe tempo di Python 4.x prima che tu dica "Wow! Siamo davvero sopra la gobba di MT!"

Forse c'è un po 'di magia che separa Python e il suo runtime da tutti gli altri software di infrastruttura con stato - tutti i tempi di esecuzione della lingua, i sistemi operativi, i monitor delle transazioni e i gestori di database che sono già passati. Ma se è così, è unico o quasi. Tutti gli altri che hanno rimosso un equivalente GIL hanno impiegato più di cinque anni di sforzi e investimenti per impegnarsi da MT-non a MT-hot.


2
+1 Ci è voluto circa quel tipo di tempo per eseguire il multi-thread di Tcl con un team di sviluppatori abbastanza piccolo. Il codice era MT-safe prima, ma aveva problemi di prestazioni, soprattutto nella gestione della memoria (che sospetto sia un'area molto calda per i linguaggi dinamici). L'esperienza in realtà non si ripercuote su Python in nient'altro che nei termini più generali; le due lingue hanno modelli di threading completamente diversi. Solo ... aspettati uno slog e aspettati strani bug ...
Donal Fellows

-1

Un'altra ipotesi selvaggia: nel 1999 Linux e altri Unices non avevano una sincronizzazione performante come quella che ha ora con futex(2)( http://en.wikipedia.org/wiki/Futex ). Quelli arrivarono intorno al 2002 (e furono uniti in 2.6 intorno al 2004).

Poiché tutte le strutture di dati integrate devono essere sincronizzate, i costi di blocco sono molto elevati. Ӎσᶎ ha già sottolineato che le operazioni atomiche non sono necessarie a buon mercato.


1
Hai qualcosa per sostenere questo? o è quasi una speculazione?

1
La citazione di GvR descrive le prestazioni "sulla piattaforma con la primitiva di blocco più veloce (Windows al momento)", quindi i blocchi lenti su Linux non sono rilevanti.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.