Il percorso per il typechecking di 4 milioni di righe di codice Python. Parte 2

Oggi pubblichiamo la seconda parte della traduzione del materiale su come Dropbox ha organizzato il controllo del tipo per diversi milioni di righe di codice Python.

Il percorso per il typechecking di 4 milioni di righe di codice Python. Parte 2

Leggi la prima parte

Supporto di tipo ufficiale (PEP 484)

Abbiamo condotto i nostri primi esperimenti seri con mypy su Dropbox durante la Hack Week 2014. Hack Week è un evento di una settimana ospitato da Dropbox. Durante questo periodo, i dipendenti possono lavorare su quello che vogliono! Alcuni dei progetti tecnologici più famosi di Dropbox sono iniziati in occasione di eventi come questi. Come risultato di questo esperimento, abbiamo concluso che mypy sembra promettente, sebbene il progetto non sia ancora pronto per un uso diffuso.

All’epoca l’idea di standardizzare i sistemi di suggerimento di tipo Python era nell’aria. Come ho detto, a partire da Python 3.0 era possibile utilizzare annotazioni di tipo per le funzioni, ma queste erano solo espressioni arbitrarie, senza sintassi e semantica definite. Durante l'esecuzione del programma, queste annotazioni venivano, per la maggior parte, semplicemente ignorate. Dopo la Hack Week, abbiamo iniziato a lavorare sulla standardizzazione della semantica. Questo lavoro ha portato all'emergere PIP 484 (Guido van Rossum, Łukasz Langa e io abbiamo collaborato a questo documento).

Le nostre motivazioni potrebbero essere viste da due lati. Innanzitutto, speravamo che l'intero ecosistema Python potesse adottare un approccio comune all'uso dei suggerimenti di tipo (un termine usato in Python come equivalente di "annotazioni di tipo"). Ciò, considerati i possibili rischi, sarebbe meglio che utilizzare molti approcci reciprocamente incompatibili. In secondo luogo, volevamo discutere apertamente i meccanismi di annotazione dei tipi con molti membri della comunità Python. Questo desiderio è stato in parte dettato dal fatto che non vorremmo apparire come “apostati” delle idee di base del linguaggio agli occhi delle grandi masse di programmatori Python. È un linguaggio tipizzato dinamicamente, noto come "duck writing". Nella comunità, fin dall’inizio, non poteva non nascere un atteggiamento un po’ sospettoso nei confronti dell’idea della tipizzazione statica. Ma quel sentimento alla fine svanì quando divenne chiaro che la digitazione statica non sarebbe stata obbligatoria (e dopo che le persone si resero conto che era effettivamente utile).

La sintassi del suggerimento di tipo che alla fine fu adottata era molto simile a quella supportata da Mypy all'epoca. PEP 484 è stato rilasciato con Python 3.5 nel 2015. Python non era più un linguaggio tipizzato dinamicamente. Mi piace pensare a questo evento come ad una pietra miliare significativa nella storia di Python.

Inizio della migrazione

Alla fine del 2015 Dropbox ha creato un team di tre persone per lavorare su mypy. Includevano Guido van Rossum, Greg Price e David Fisher. Da quel momento in poi, la situazione ha cominciato a svilupparsi in modo estremamente rapido. Il primo ostacolo alla crescita di Mypy sono state le prestazioni. Come ho accennato sopra, nei primi giorni del progetto avevo pensato di tradurre l'implementazione di mypy in C, ma per ora questa idea è stata cancellata dalla lista. Siamo rimasti bloccati nell'esecuzione del sistema utilizzando l'interprete CPython, che non è abbastanza veloce per strumenti come mypy. (Nemmeno il progetto PyPy, un'implementazione alternativa di Python con un compilatore JIT, ci ha aiutato.)

Fortunatamente, alcuni miglioramenti algoritmici sono venuti in nostro aiuto qui. Il primo potente “acceleratore” è stata l’implementazione del controllo incrementale. L'idea alla base di questo miglioramento era semplice: se tutte le dipendenze del modulo non sono cambiate rispetto all'esecuzione precedente di mypy, allora possiamo utilizzare i dati memorizzati nella cache durante l'esecuzione precedente mentre lavoriamo con le dipendenze. Dovevamo solo eseguire il controllo del tipo sui file modificati e sui file che dipendevano da essi. Mypy è andato anche un po' oltre: se l'interfaccia esterna di un modulo non cambiava, mypy presumeva che gli altri moduli che importavano questo modulo non dovessero essere ricontrollati.

Il controllo incrementale ci ha aiutato molto durante l'annotazione di grandi quantità di codice esistente. Il punto è che questo processo di solito comporta molte esecuzioni iterative di mypy poiché le annotazioni vengono gradualmente aggiunte al codice e gradualmente migliorate. La prima esecuzione di mypy era ancora molto lenta perché aveva molte dipendenze da controllare. Quindi, per migliorare la situazione, abbiamo implementato un meccanismo di memorizzazione nella cache remota. Se mypy rileva che è probabile che la cache locale non sia aggiornata, scarica l'istantanea della cache corrente per l'intero codebase dal repository centralizzato. Quindi esegue un controllo incrementale utilizzando questa istantanea. Questo ci ha portato a compiere un ulteriore grande passo avanti verso l'aumento delle prestazioni di mypy.

Questo è stato un periodo di adozione rapida e naturale del controllo del tipo su Dropbox. Alla fine del 2016 avevamo già circa 420000 righe di codice Python con annotazioni di tipo. Molti utenti erano entusiasti del controllo del tipo. Sempre più team di sviluppo utilizzano Dropbox mypy.

Allora tutto sembrava a posto, ma avevamo ancora molto da fare. Abbiamo iniziato a svolgere periodiche indagini sugli utenti interni per identificare le aree problematiche del progetto e capire quali problemi devono essere risolti per primi (questa pratica è ancora oggi utilizzata in azienda). I più importanti, come divenne chiaro, erano due compiti. Innanzitutto, avevamo bisogno di una maggiore copertura dei tipi del codice, in secondo luogo, avevamo bisogno che mypy funzionasse più velocemente. Era assolutamente chiaro che il nostro lavoro per accelerare mypy e implementarlo nei progetti aziendali era ancora lungi dall'essere completato. Noi, pienamente consapevoli dell'importanza di questi due compiti, abbiamo iniziato a risolverli.

Più produttività!

I controlli incrementali hanno reso Mypy più veloce, ma lo strumento non era ancora abbastanza veloce. Molti controlli incrementali sono durati circa un minuto. La ragione di ciò sono state le importazioni cicliche. Questo probabilmente non sorprenderà nessuno che abbia lavorato con codebase di grandi dimensioni scritte in Python. Avevamo set di centinaia di moduli, ognuno dei quali importava indirettamente tutti gli altri. Se un file in un ciclo di importazione veniva modificato, mypy doveva elaborare tutti i file in quel ciclo e spesso tutti i moduli che importavano moduli da quel ciclo. Uno di questi cicli è stato il famigerato “groviglio di dipendenze” che ha causato molti problemi a Dropbox. Un tempo questa struttura conteneva diverse centinaia di moduli, mentre venivano importati, direttamente o indirettamente, numerosi test, veniva utilizzata anche nel codice di produzione.

Abbiamo considerato la possibilità di “districare” le dipendenze circolari, ma non avevamo le risorse per farlo. C'era troppo codice con cui non avevamo familiarità. Di conseguenza, abbiamo trovato un approccio alternativo. Abbiamo deciso di far funzionare mypy velocemente anche in presenza di “grovigli di dipendenze”. Abbiamo raggiunto questo obiettivo utilizzando il demone mypy. Un demone è un processo server che implementa due funzionalità interessanti. Innanzitutto, memorizza in memoria le informazioni sull'intera base di codice. Ciò significa che ogni volta che esegui mypy, non devi caricare i dati memorizzati nella cache relativi a migliaia di dipendenze importate. In secondo luogo, analizza attentamente, a livello di piccole unità strutturali, le dipendenze tra funzioni e altre entità. Ad esempio, se la funzione foo chiama una funzione bar, allora c'è una dipendenza foo от bar. Quando un file cambia, il demone prima, in isolamento, elabora solo il file modificato. Quindi esamina le modifiche visibili esternamente a quel file, come le firme delle funzioni modificate. Il demone utilizza informazioni dettagliate sulle importazioni solo per ricontrollare quelle funzioni che utilizzano effettivamente la funzione modificata. In genere, con questo approccio, è necessario controllare pochissime funzioni.

Implementare tutto ciò non è stato facile, poiché l’implementazione originale di mypy era fortemente focalizzata sull’elaborazione di un file alla volta. Abbiamo dovuto affrontare molte situazioni limite, il cui verificarsi ha richiesto ripetute verifiche nei casi in cui qualcosa fosse cambiato nel codice. Ciò accade, ad esempio, quando a una classe viene assegnata una nuova classe base. Una volta ottenuto ciò che volevamo, siamo riusciti a ridurre il tempo di esecuzione della maggior parte dei controlli incrementali a pochi secondi. Ci è sembrata una grande vittoria.

Ancora più produttività!

Insieme al caching remoto di cui ho parlato sopra, il demone mypy ha risolto quasi completamente i problemi che sorgono quando un programmatore esegue frequentemente il controllo del tipo, apportando modifiche a un numero limitato di file. Tuttavia, le prestazioni del sistema nel caso d’uso meno favorevole erano ancora lontane dall’ottimale. Un avvio pulito di mypy potrebbe richiedere più di 15 minuti. E questo era molto più di quanto saremmo stati contenti. Ogni settimana la situazione peggiorava poiché i programmatori continuavano a scrivere nuovo codice e ad aggiungere annotazioni al codice esistente. I nostri utenti erano ancora affamati di maggiori prestazioni, ma eravamo felici di incontrarli a metà strada.

Abbiamo deciso di ritornare a una delle idee precedenti riguardo mypy. Vale a dire, per convertire il codice Python in codice C. Sperimentare con Cython (un sistema che consente di tradurre il codice scritto in Python in codice C) non ci ha dato alcuna accelerazione visibile, quindi abbiamo deciso di rilanciare l'idea di scrivere il nostro compilatore. Dato che il codice base di mypy (scritto in Python) conteneva già tutte le annotazioni di tipo necessarie, abbiamo pensato che valesse la pena provare a utilizzare queste annotazioni per velocizzare il sistema. Ho creato rapidamente un prototipo per testare questa idea. Ha mostrato un aumento di oltre 10 volte delle prestazioni su vari micro-benchmark. La nostra idea era quella di compilare moduli Python in moduli C utilizzando Cython e trasformare le annotazioni di tipo in controlli di tipo in fase di esecuzione (di solito le annotazioni di tipo vengono ignorate in fase di esecuzione e utilizzate solo dai sistemi di controllo di tipo). In realtà avevamo pianificato di tradurre l'implementazione di mypy da Python in un linguaggio progettato per essere tipizzato staticamente, che sarebbe stato (e, per la maggior parte, avrebbe funzionato) esattamente come Python. (Questo tipo di migrazione tra linguaggi è diventata una sorta di tradizione del progetto mypy. L'implementazione originale di mypy è stata scritta in Alore, poi c'era un ibrido sintattico di Java e Python).

Concentrarsi sull'API di estensione CPython è stato fondamentale per non perdere le funzionalità di gestione dei progetti. Non abbiamo avuto bisogno di implementare una macchina virtuale o le librerie di cui aveva bisogno Mypy. Inoltre, avremmo comunque accesso all'intero ecosistema Python e a tutti gli strumenti (come pytest). Ciò significava che potevamo continuare a utilizzare il codice Python interpretato durante lo sviluppo, permettendoci di continuare a lavorare con un modello molto veloce di apportare modifiche al codice e testarlo, piuttosto che attendere la compilazione del codice. Sembrava che stessimo facendo un ottimo lavoro sedendoci su due sedie, per così dire, e ci è piaciuto molto.

Il compilatore, che abbiamo chiamato mypyc (poiché utilizza mypy come front-end per l'analisi dei tipi), si è rivelato un progetto di grande successo. Nel complesso, abbiamo ottenuto una velocità di circa 4 volte per le frequenti esecuzioni di Mypy senza memorizzazione nella cache. Lo sviluppo del nucleo del progetto mypyc ha richiesto a un piccolo team composto da Michael Sullivan, Ivan Levkivsky, Hugh Hahn e me circa 4 mesi di calendario. Questa quantità di lavoro era molto inferiore a quella che sarebbe stata necessaria per riscrivere mypy, ad esempio, in C++ o Go. E abbiamo dovuto apportare molte meno modifiche al progetto di quelle che avremmo dovuto apportare riscrivendolo in un'altra lingua. Speravamo anche di poter portare mypyc a un livello tale che altri programmatori Dropbox potessero utilizzarlo per compilare e velocizzare il proprio codice.

Per raggiungere questo livello di prestazioni abbiamo dovuto applicare alcune soluzioni ingegneristiche interessanti. Pertanto, il compilatore può velocizzare molte operazioni utilizzando costrutti C veloci e di basso livello. Ad esempio, una chiamata di funzione compilata viene tradotta in una chiamata di funzione C. E una chiamata del genere è molto più veloce della chiamata di una funzione interpretata. Alcune operazioni, come le ricerche nei dizionari, implicavano ancora l'uso di regolari chiamate C-API da CPython, che erano solo leggermente più veloci una volta compilate. Siamo riusciti ad eliminare il carico aggiuntivo sul sistema creato dall'interpretazione, ma in questo caso questo ha dato solo un piccolo guadagno in termini di prestazioni.

Per identificare le operazioni “lente” più comuni, abbiamo eseguito la profilazione del codice. Armati di questi dati, abbiamo provato a modificare mypyc in modo che generasse codice C più veloce per tali operazioni, oppure a riscrivere il codice Python corrispondente utilizzando operazioni più veloci (e talvolta semplicemente non avevamo una soluzione abbastanza semplice per questo o altro problema) . Riscrivere il codice Python era spesso una soluzione più semplice al problema rispetto al fatto che il compilatore eseguisse automaticamente la stessa trasformazione. A lungo termine, volevamo automatizzare molte di queste trasformazioni, ma all'epoca eravamo concentrati sull'accelerazione di mypy con il minimo sforzo. E nel procedere verso questo obiettivo, abbiamo preso alcune scorciatoie.

To be continued ...

Cari lettori! Quali sono state le tue impressioni sul progetto mypy quando hai saputo della sua esistenza?

Il percorso per il typechecking di 4 milioni di righe di codice Python. Parte 2
Il percorso per il typechecking di 4 milioni di righe di codice Python. Parte 2

Fonte: habr.com

Aggiungi un commento