Il primo tentativo di rimuovere Python GIL ha prodotto cattive prestazioni: perché?

13

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

This has been tried before, with disappointing results, which is why I'm reluctant to put much effort into it myself. In 1999 Greg Stein (with Mark Hammond?) produced a fork of Python (1.5 I believe) that removed the GIL, replacing it with fine-grained locks on all mutable data structures. He also submitted patches that removed many of the reliances on global mutable data structures, which I accepted. However, after benchmarking, it was shown that even on the platform with the fastest locking primitive (Windows at the time) it slowed down single-threaded execution nearly two-fold, meaning that on two CPUs, you could get just a little more work done without the GIL than on a single CPU with the GIL. This wasn't enough, and Greg's patch disappeared into oblivion. (See Greg's writeup on the performance.)

Difficilmente riesco a discuterne con i risultati effettivi, ma mi chiedo davvero perché sia successo. Presumibilmente, il motivo principale per cui la rimozione di GIL da CPython è così difficile è a causa del sistema di gestione della memoria di conteggio di riferimento. Un tipico programma Python chiamerà Py_INCREF e Py_DECREF migliaia o milioni di volte, rendendolo un punto di contesa chiave se dovevamo avvolgere le serrature intorno.

Ma non capisco perché l'aggiunta di primitive atomiche possa rallentare un programma con thread singolo . Supponiamo di aver appena modificato CPython in modo che la variabile refcount in ogni oggetto Python fosse una primitiva atomica. Quindi eseguiamo un incremento atomico (istruzione fetch-and-add) quando è necessario incrementare il conteggio dei riferimenti. Ciò renderebbe il conteggio dei riferimenti di Python sicuro per i thread e non dovrebbe comportare alcuna penalizzazione delle prestazioni in un'applicazione a thread singolo, perché non ci sarebbe alcuna contesa di blocco.

Ma ahimè, molte persone che sono 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?

    
posta Siler 04.07.2014 - 00:25
fonte

2 risposte

9

Non ho familiarità con la forcella di Greg Stein Python, quindi sconsiglia questo confronto come analogia storica speculativa se lo desideri. Ma questa era esattamente l'esperienza storica di molti codebase dell'infrastruttura che passavano da implementazioni da singolo a multi-thread.

Essenzialmente tutte le implementazioni Unix che ho studiato negli anni '90 - AIX, DEC OSF / 1, DG / UX, DYNIX, HP-UX, IRIX, Solaris, SVR4 e SVR4 MP - sono state tutte affrontate esattamente con questo tipo di " abbiamo inserito un blocco a grana più fine - ora è più lento !! " problema. I DBMS che ho seguito - DB2, Ingres, Informix, Oracle e Sybase - sono stati tutti analizzati.

Ho sentito "questi cambiamenti non ci rallenteranno quando eseguiremo un thread singolo" un milione di volte. Non funziona mai così. Il semplice atto di controllare condizionatamente "stiamo eseguendo il multithreading o no?" aggiunge un sovraccarico reale, specialmente su CPU altamente pipeline. Operazioni atomiche e serrature occasionali aggiunte per garantire che l'integrità delle strutture di dati condivise debba essere chiamata abbastanza spesso e sono molto lente. Anche le primitive di sincronizzazione / 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 interblocco era necessaria in vari luoghi. Poi si rendono conto di dove inizialmente hanno schiaffeggiato i primitivi di chiusura 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 di incollaggio alla fine hanno ottenuto l'accelerazione del sistema operativo o dell'hardware, ma l'intera evoluzione ha richiesto 3-5 anni, il minimo indispensabile. Nel frattempo, le versioni MP o MT stavano zoppicando, in termini di prestazioni.

Altrimenti, sofisticati team di sviluppo hanno sostenuto che tali rallentamenti sono fondamentalmente un fatto di vita persistente e intrattabile. Ad es. rifiutato di abilitare SMP AIX per almeno 5 anni dopo la competizione, fermamente convinto che il single-threaded fosse semplicemente migliore. Sybase ha usato alcuni degli stessi argomenti. L'unica ragione per cui alcuni dei team alla fine sono venuti in giro era che le prestazioni single-thread non potevano più essere ragionevolmente migliorate a livello di CPU. Sono stati costretti a passare a MP / MT o ad accettare un prodotto sempre più non competitivo.

La concorrenza attiva è difficile. Ed è ingannevole. Tutti si precipitano dentro pensando che "non sarà così male". Poi colpiscono le sabbie mobili e devono arrancare. Ho visto questo accadere con almeno una dozzina di team di marca, ben finanziati e intelligenti. Generalmente sembrava che ci volessero almeno cinque anni dopo aver scelto il multi-thread per "tornare al punto in cui dovevano essere, in termini di prestazioni" con i prodotti MP / MT; la maggior parte stava ancora migliorando significativamente l'efficienza / scalabilità MP / MT anche dieci anni dopo aver effettuato il turno.

Quindi la mia ipotesi è che, in assenza di approvazione e supporto da parte di GvR, nessuno ha preso la via lunga per Python e GIL. Anche se dovessero farlo oggi, sarebbe il tempo di Python 4.x prima che tu dicessi "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 stateful: tutti i runtime linguistici, i sistemi operativi, i monitor delle transazioni e i gestori di database che sono già andati avanti. Ma se è così, è unico o quasi. Tutti gli altri che rimuovono un equivalente GIL hanno impiegato più di cinque anni di sforzi e investimenti impegnativi per ottenere da MT-non a MT-hot.

    
risposta data 09.07.2014 - 04:18
fonte
-1

Un'altra ipotesi selvaggia: nel 1999 Linux e altri Unici non avevano una sincronizzazione performante come ora con futex(2) ( collegamento ). Sono venuti intorno al 2002 (e sono stati fusi in 2.6 attorno al 2004).

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

    
risposta data 04.07.2014 - 23:10
fonte

Leggi altre domande sui tag