4 millió Python-kódsor típusellenőrzésének elérési útja. 2. rész

Ma közzétesszük a második részét annak az anyagnak a fordításának, amely arról szól, hogyan szervezte meg a Dropbox több millió sornyi Python-kód típusvezérlését.

4 millió Python-kódsor típusellenőrzésének elérési útja. 2. rész

Olvassa el az első részt

Hivatalos típustámogatás (PEP 484)

Az első komoly kísérleteinket a mypy-vel a Dropboxnál végeztük a 2014-es Hack Week alatt. A Hack Week egy egyhetes rendezvény, amelynek a Dropbox ad otthont. Ez idő alatt az alkalmazottak azt dolgozhatnak, amit akarnak! A Dropbox néhány leghíresebb technológiai projektje az ehhez hasonló eseményeken kezdődött. A kísérlet eredményeként arra a következtetésre jutottunk, hogy a mypy ígéretesnek tűnik, bár a projekt még nem áll készen a széles körű felhasználásra.

Akkoriban a levegőben volt a Python típusú célzási rendszerek szabványosításának ötlete. Mint mondtam, a Python 3.0 óta lehetett függvényekhez típusjegyzeteket használni, de ezek csak tetszőleges kifejezések voltak, meghatározott szintaxis és szemantika nélkül. A program végrehajtása során ezeket a megjegyzéseket a legtöbb esetben egyszerűen figyelmen kívül hagyták. A Hack Week után elkezdtünk dolgozni a szemantika szabványosításán. Ez a munka vezetett a megjelenéshez PEP 484 (Guido van Rossum, Łukasz Langa és én együtt dolgoztunk ezen a dokumentumon).

Az indítékainkat két oldalról lehetett szemlélni. Először is azt reméltük, hogy az egész Python-ökoszisztéma közös megközelítést tud alkalmazni a típusutalások használatára (ez a kifejezés a Pythonban a "típusannotációk" megfelelőjeként használatos). Ez, figyelembe véve a lehetséges kockázatokat, jobb lenne, mint sok, egymással összeegyeztethetetlen megközelítés alkalmazása. Másodszor, a Python közösség sok tagjával nyíltan meg akartuk beszélni a típusannotációs mechanizmusokat. Ezt a vágyat részben az diktálta, hogy a Python programozók széles tömegei előtt nem akarunk a nyelv alapgondolataiból „hitehagyottnak” tűnni. Ez egy dinamikusan gépelt nyelv, az úgynevezett "kacsa gépelés". A közösségben a legelején egy kissé gyanakvó hozzáállás a statikus gépelés gondolatával szemben nem tudott segíteni, de felmerült. De ez az érzés végül alábbhagyott, miután világossá vált, hogy a statikus gépelés nem lesz kötelező (és miután az emberek rájöttek, hogy ez valóban hasznos).

A típus-hivatkozás szintaxisa, amelyet végül elfogadtak, nagyon hasonló volt ahhoz, amit a mypy akkoriban támogatott. A PEP 484 3.5-ben jelent meg Python 2015-tel. A Python már nem volt dinamikusan tipizált nyelv. Szeretek úgy gondolni erre az eseményre, mint egy jelentős mérföldkőre a Python történetében.

A migráció kezdete

2015 végén a Dropbox három fős csapatot hozott létre, hogy a mypy-n dolgozzanak. Volt köztük Guido van Rossum, Greg Price és David Fisher. Ettől a pillanattól kezdve a helyzet rendkívül gyorsan kezdett fejlődni. A mypy növekedésének első akadálya a teljesítmény volt. Ahogy fentebb utaltam rá, a projekt kezdeti napjaiban azon gondolkodtam, hogy a mypy implementációt C nyelvre fordítom, de ez az ötlet egyelőre kikerült a listáról. Elakadtunk a rendszer futtatásánál a CPython interpreter használatával, amely nem elég gyors az olyan eszközökhöz, mint a mypy. (A PyPy projekt, egy alternatív Python implementáció JIT fordítóval sem segített nekünk.)

Szerencsére itt segítségünkre volt néhány algoritmikus fejlesztés. Az első erőteljes „gyorsító” a fokozatos ellenőrzés megvalósítása volt. A fejlesztés mögött az ötlet egyszerű volt: ha a modul összes függősége nem változott a mypy előző futtatása óta, akkor az előző futás során gyorsítótárban tárolt adatokat használhatjuk a függőségek kezelése során. Csak a módosított fájlok és a tőlük függő fájlok típusellenőrzését kellett végrehajtanunk. A Mypy még egy kicsit tovább ment: ha egy modul külső felülete nem változott, a mypy feltételezte, hogy a többi modult, amely ezt a modult importálta, nem kell újra ellenőrizni.

A növekményes ellenőrzés sokat segített nekünk, amikor nagy mennyiségű meglévő kódot annotáltunk. A lényeg az, hogy ez a folyamat általában sok iteratív mypy-futtatást tartalmaz, mivel a megjegyzéseket fokozatosan hozzáadják a kódhoz, és fokozatosan javítják. A mypy első futtatása még mindig nagyon lassú volt, mert sok függőséget kellett ellenőrizni. Ezután a helyzet javítása érdekében bevezettünk egy távoli gyorsítótárazási mechanizmust. Ha a mypy azt észleli, hogy a helyi gyorsítótár valószínűleg elavult, letölti a teljes kódbázis aktuális gyorsítótár-pillanatképét a központi tárhelyről. Ezután növekményes ellenőrzést hajt végre ezzel a pillanatfelvétellel. Ezzel még egy nagy lépést tettünk a mypy teljesítményének növelése felé.

Ez a típusellenőrzés gyors és természetes átvételének időszaka volt a Dropboxnál. 2016 végére már körülbelül 420000 XNUMX Python-sorral rendelkeztünk típusjegyzetekkel. Sok felhasználó lelkesedett a típusellenőrzésért. Egyre több fejlesztőcsapat használta a Dropbox mypy-t.

Akkor még minden jónak tűnt, de még mindig sok dolgunk volt. Elkezdtünk időszakos belső felhasználói felméréseket végezni, hogy beazonosítsuk a projekt problémás területeit, és megértsük, milyen problémákat kell először megoldani (ezt a gyakorlatot a mai napig alkalmazzák a vállalatnál). A legfontosabb, mint kiderült, két feladat volt. Először is több típusú kódra volt szükségünk, másodszor pedig a mypy gyorsabb működéséhez. Teljesen egyértelmű volt, hogy a mypy felgyorsítására és a vállalati projektekbe való beépítésre irányuló munkánk még messze volt a befejezéstől. Mi e két feladat fontosságának teljes tudatában hozzáfogtunk ezek megoldásához.

Több termelékenység!

A fokozatos ellenőrzések gyorsabbá tették a mypy-t, de az eszköz még mindig nem volt elég gyors. Sok növekményes ellenőrzés körülbelül egy percig tartott. Ennek oka a ciklikus import volt. Ez valószínűleg senkit sem fog meglepni, aki nagy Pythonban írt kódbázisokkal dolgozott. Több száz modulból álló készletünk volt, amelyek mindegyike közvetve importálta az összes többit. Ha az importálási hurokban bármely fájl megváltozott, a mypy-nek fel kellett dolgoznia a hurokban lévő összes fájlt, és gyakran minden olyan modult, amely modulokat importált abból a ciklusból. Az egyik ilyen ciklus volt a hírhedt „függőségi gubanc”, amely sok gondot okozott a Dropboxnál. Egyszer ez a struktúra több száz modult tartalmazott, miközben közvetve vagy közvetlenül importálták, sok tesztet, a gyártási kódban is használták.

Fontolóra vettük a körkörös függőségek "feloldásának" lehetőségét, de nem volt rá forrásunk. Túl sok kód volt, amit nem ismertünk. Ennek eredményeként egy alternatív megközelítést találtunk ki. Úgy döntöttünk, hogy a mypy gyorsan működni fog még „függőségi gubancok” jelenlétében is. Ezt a célt a mypy démon segítségével értük el. A démon egy szerverfolyamat, amely két érdekes szolgáltatást valósít meg. Először is a teljes kódbázisról tárol információkat a memóriában. Ez azt jelenti, hogy minden alkalommal, amikor a mypy-t futtatja, nem kell betöltenie a több ezer importált függőséggel kapcsolatos gyorsítótárazott adatot. Másodszor, gondosan, a kis szerkezeti egységek szintjén elemzi a funkciók és más entitások közötti függőséget. Például, ha a függvény foo függvényt hív meg bar, akkor van egy függőség foo -tól bar. Amikor egy fájl megváltozik, a démon először, elszigetelten, csak a megváltozott fájlt dolgozza fel. Ezután megvizsgálja a fájl külsőleg látható változásait, például a megváltozott függvényaláírásokat. A démon csak a módosított függvényt ténylegesen használó függvények ellenőrzésére használ részletes információkat az importálásról. Ezzel a megközelítéssel általában nagyon kevés funkciót kell ellenőrizni.

Mindezt nem volt könnyű megvalósítani, mivel az eredeti mypy implementáció erősen egy-egy fájl feldolgozására összpontosított. Sok határhelyzettel kellett megküzdenünk, amelyek előfordulása ismételt ellenőrzést igényelt olyan esetekben, amikor valami megváltozott a kódban. Ez például akkor fordul elő, ha egy osztályhoz új alaposztályt rendelnek. Miután megtettük, amit akartunk, a legtöbb növekményes ellenőrzés végrehajtási idejét néhány másodpercre tudtuk csökkenteni. Ez nagy győzelemnek tűnt számunkra.

Még nagyobb termelékenység!

A fentebb tárgyalt távoli gyorsítótárazással együtt a mypy démon szinte teljesen megoldotta azokat a problémákat, amelyek akkor merülnek fel, amikor a programozó gyakran futtat típusellenőrzést, és néhány fájlt módosít. A rendszer teljesítménye azonban a legkedvezőtlenebb felhasználási esetben még mindig messze volt az optimálistól. A mypy tiszta indítása több mint 15 percet is igénybe vehet. És ez sokkal több volt, mint amivel elégedettek lettünk volna. A helyzet hétről hétre rosszabb lett, mivel a programozók továbbra is új kódot írtak, és megjegyzéseket adtak a meglévő kódhoz. Felhasználóink ​​még mindig ki voltak éhezve a nagyobb teljesítményre, de örömmel találkoztunk velük félúton.

Úgy döntöttünk, hogy visszatérünk az egyik korábbi gondolathoz a mypy-vel kapcsolatban. Mégpedig a Python kód C kódká alakítására. A Cythonnal (olyan rendszerrel, amely lehetővé teszi a Pythonban írt kódok C-kódra történő fordítását) végzett kísérletezés nem hozott látható gyorsulást, ezért úgy döntöttünk, hogy újraélesztjük a saját fordítóprogramunk megírásának gondolatát. Mivel a mypy kódbázis (Python nyelven íródott) már tartalmazta az összes szükséges típusannotációt, úgy gondoltuk, hogy érdemes megpróbálni ezekkel a megjegyzésekkel felgyorsítani a rendszert. Gyorsan létrehoztam egy prototípust az ötlet tesztelésére. Több mint 10-szeres teljesítménynövekedést mutatott különböző mikro-benchmarkokon. Az ötletünk az volt, hogy Python modulokat C modulokká fordítunk Cython segítségével, és a típusjegyzeteket futásidejű típusellenőrzésekké alakítjuk (a típusjegyzeteket általában figyelmen kívül hagyják futás közben, és csak a típusellenőrző rendszerek használják). Valójában azt terveztük, hogy a mypy implementációt Pythonból lefordítjuk egy statikusan gépelhető nyelvre, amely pontosan úgy néz ki (és többnyire működik), mint a Python. (Ez a fajta nyelvek közötti migráció a mypy projekt hagyományává vált. Az eredeti mypy implementációt Alore-ban írták, aztán volt a Java és a Python szintaktikai hibridje).

A CPython kiterjesztés API-ra való összpontosítás kulcsfontosságú volt ahhoz, hogy ne veszítse el a projektmenedzsment képességeit. Nem volt szükségünk virtuális gépre vagy olyan könyvtárra, amelyre a mypynek szüksége volt. Ezenkívül továbbra is hozzáférhetnénk a teljes Python-ökoszisztémához és az összes eszközhöz (például a pytesthez). Ez azt jelentette, hogy továbbra is használhattunk értelmezett Python-kódot a fejlesztés során, ami lehetővé tette számunkra, hogy továbbra is nagyon gyors kódmódosítási és tesztelési mintával dolgozhassunk, ahelyett, hogy a kód fordítására várnánk. Úgy tűnt, remek munkát végeztünk, hogy úgymond két széken ülünk, és imádtuk.

A fordító, amit mypyc-nek hívtunk (mivel a mypy-t használja front-endként a típusok elemzéséhez), nagyon sikeres projektnek bizonyult. Összességében megközelítőleg négyszeres gyorsulást értünk el a gyorsítótárazás nélküli, gyakori mypy-futtatásoknál. A mypyc projekt magjának kidolgozása Michael Sullivanből, Ivan Levkivskyből, Hugh Hahnból és jómagamból körülbelül 4 naptári hónapot vett igénybe. Ez a munkamennyiség sokkal kisebb volt, mint amennyire például a mypy átírásához kellett volna, például C++ vagy Go nyelven. És sokkal kevesebb változtatást kellett végrehajtanunk a projekten, mint amennyit más nyelven átírva kellett volna. Azt is reméltük, hogy a mypyc-et olyan szintre tudjuk hozni, hogy más Dropbox programozók felhasználhassák kódjuk fordítására és felgyorsítására.

Ahhoz, hogy ezt a teljesítményszintet elérjük, néhány érdekes mérnöki megoldást kellett alkalmaznunk. Így a fordító számos műveletet fel tud gyorsítani gyors, alacsony szintű C konstrukciók használatával, például egy lefordított függvényhívást C függvényhívássá fordít. És egy ilyen hívás sokkal gyorsabb, mint egy értelmezett függvény meghívása. Egyes műveletek, például a szótárkeresések továbbra is a CPython rendszeres C-API-hívásait tartalmazták, amelyek fordításkor csak kis mértékben voltak gyorsabbak. Az értelmezés által a rendszerre nehezedő többletterhelést ki tudtuk küszöbölni, de ez jelen esetben csak kis teljesítménynövekedést hozott.

A leggyakoribb „lassú” műveletek azonosítása érdekében kódprofilozást végeztünk. Ezekkel az adatokkal felvértezve megpróbáltuk vagy módosítani a mypyc-et, hogy gyorsabb C kódot generáljon az ilyen műveletekhez, vagy gyorsabb műveletekkel átírtuk a megfelelő Python kódot (és néha egyszerűen nem volt elég egyszerű megoldásunk erre vagy más problémára) . A Python-kód újraírása gyakran könnyebb megoldás volt a problémára, mintha a fordító automatikusan végrehajtaná ugyanazt az átalakítást. Hosszú távon sok ilyen átalakítást szerettünk volna automatizálni, de akkoriban a mypy minimális erőfeszítéssel történő felgyorsítására összpontosítottunk. És e cél felé haladva több sarkot is bevágtunk.

Folytatás ...

Kedves olvasók! Milyen benyomásai voltak a mypy projektről, amikor értesült a létezéséről?

4 millió Python-kódsor típusellenőrzésének elérési útja. 2. rész
4 millió Python-kódsor típusellenőrzésének elérési útja. 2. rész

Forrás: will.com

Hozzászólás