Út a 4 millió sor Python kód típusainak ellenőrzéséhez. 2. rész

Ma közzétesszük a Dropbox-ban a Python-kód több millió sorának típuskontrolljáról szóló anyag fordításának második részét.

Út a 4 millió sor Python kód típusainak ellenőrzéséhez. 2. rész

→ Első rész olvasása

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

2014 Hack Week során végeztük el az első komoly kísérleteinket a mypy-val a Dropbox-ban. A Hack Week egy esemény, amelyet a Dropbox számára szerveznek egy héten keresztül, amikor az alkalmazottak bármin dolgozhatnak! A Dropbox legnevesebb technológiai projektjei közül sok éppen ezeken a rendezvényeken indult. E kísérlet eredményeként arra a következtetésre jutottunk, hogy a mypy ígéretesnek tűnik, bár ez a projekt még nem volt kész a széleskörű használatra.

Akkoriban a típusokkal kapcsolatos javaslatok standardizálásának ötlete volt a levegőben. Ahogy már említettem, a Python 3.0-tól lehetett használni a típus-annotációkat a funkciókhoz, de ezek csupán tetszőleges kifejezések voltak, meghatározott szintaxis és szemantika nélkül. A program végrehajtása során ezeket az annotációkat nagyrészt figyelmen kívül hagyták. A Hack Week után elkezdtünk a szemantika standardizálásán dolgozni. Ez a munka a következőhöz vezetett: PEP 484 (ez a dokumentum Guido van Rossum, Lukás Langa és az én közreműködésemmel készült).

Motívumainkat két szempontból lehetett vizsgálni. Először is, reméltük, hogy a Python ökoszisztémája magáévá teheti a típusokkal kapcsolatos javaslatok (type hints — a Pythonban „típus-annotációk” analógiájaként használt kifejezés) közös megközelítését. Ez a lehetséges kockázatokat figyelembe véve jobbnak tűnt, mint sok kölcsönösen inkompatibilis megközelítés alkalmazása. Másodsorban szeretnénk nyíltan megvitatni a típusok annotálásának mechanizmusait a Python közösség sok képviselőjével. Részben ezt a vágyat az indokolta, hogy nem akartunk a Python programozók széles körében „lázadóként” megjelenni. Ez egy dinamikusan típusozott nyelv, amelyet „kacsa típizálásnak” ismernek. A közösségben kezdetben nem lehetett megkerülni a statikus típusok ötletével szembeni némi gyanakvást. De ez a hozzáállás végül enyhült — miután világossá vált, hogy a statikus típusok kötelezővé tétele nem várható (és miután az emberek megértették, hogy valóban hasznosak).

A véglegesen elfogadott típusokkal kapcsolatos javaslat szintaxisa nagyon hasonló volt ahhoz, amit a mypy abban az időben támogatott. A PEP 484 dokumentum 2015-ben, a Python 3.5-tel együtt jelent meg. A Python már nem csupán dinamikus típusként működő nyelv volt. Úgy gondolom, hogy ez az esemény jelentős mérföldkő a Python történetében.

A migráció kezdete

2015 végén a Dropboxnál, a mypy fejlesztésére három fős csapatot alakítottak. Tagjai voltak Guido van Rossum, Greg Price és David Fisher. Ekkortól kezdve a helyzet rendkívül gyorsan fejlődött. Az első akadály a mypy növekedése előtt a teljesítmény volt. Ahogy azt korábban jeleztem, a projekt korai fejlesztése során azon tűnődtem, hogy a mypy megvalósítását C nyelvre kellene átírni, de ezt az ötletet egyelőre elvetettük. Az a tény, hogy a rendszer futtatásához a CPython interpretert használtuk, nem volt elég gyors a mypy-hoz hasonló eszközök számára. (A PyPy projekt, egy JIT-kompilátorral rendelkező alternatív Python megvalósítás, szintén nem segített.)

Szerencsére itt egy sor algoritmikus fejlesztés segített nekünk. Az első komoly "gyorsító" az inkrementális ellenőrzés megvalósítása volt. E fejlesztés ötlete egyszerű volt: ha a mypy legutóbbi futása óta a modul összes függősége nem változott, akkor a függőségek kezelése során a legutolsó munkamenet során tárolt gyorsítótárazott adatokat használhatjuk. Csak a módosított fájlok és a tőlük függő fájlok típusellenőrzését kellett elvégezni. A mypy még egy lépéssel tovább ment: ha a modul külső interfésze nem változott, a mypy úgy vélte, hogy az ezt a modult importáló más modulokat nem szükséges újból ellenőrizni.

Az inkrementális ellenőrzés jelentősen segített nekünk a meglévő kód nagy mennyiségű annotálásában. A helyzet az, hogy ez a folyamat általában számos iteratív mypy futtatást igényel, mivel az annotációk fokozatosan kerülnek be a kódba, és fokozatosan javítják azt. Az első mypy futtatás még mindig nagyon lassú volt, mivel rengeteg függőséget kellett ellenőrizni. Ezért, a helyzet javítása érdekében, megvalósítottuk a távoli gyorsítótárazás mechanizmusát. Ha a mypy észleli, hogy a helyi gyorsítótár valószínűleg elavult, betölti a teljes kódalap aktuális gyorsítótár pillanatképét egy központosított tárolóból. Ezután ezzel a pillanatképpel végzi az inkrementális ellenőrzést. Ez egy újabb jelentős lépést jelentett a mypy teljesítményének növelése felé.

Ez volt a típusellenőrző rendszer gyors és természetes bevezetésének időszaka a Dropboxnál. 2016 végére körülbelül 420 000 sor Python-kódunk volt típusannotációkkal. Sok felhasználó lelkesen fogadta a típuskontrollt. A Dropboxnál a mypy-t egyre több fejlesztőcsapat használta.

Akkor minden jól nézett ki, de még sok tennivaló állt előttünk. Szisztematikus belső felméréseket kezdtünk végezni a felhasználók körében, hogy felismerjük a projekt problémáit, és megértsük, mely kérdéseket kell elsőként megoldani (ezt a gyakorlatot a cég a mai napig alkalmazza). Kiderült, hogy a legfontosabb két feladat van. Az első az, hogy több kódot kellene lefedni típusokkal, a második pedig, hogy a mypy gyorsabban működjön. Teljesen világos volt, hogy a mypy felgyorsításán és a vállalati projektekbe való bevezetésén végzett munkánk messze nem volt befejeződve. Teljes mértékben tudatában voltunk e két feladat fontosságának, és nekiláttunk a megoldásuknak.

Több teljesítmény!

Az inkrementális ellenőrzések felgyorsították a mypy-t, de ez az eszköz még mindig nem volt elég gyors. Sok inkrementális ellenőrzés körülbelül egy percig tartott. Ennek oka a ciklikus importok voltak. Ez valószínűleg nem meglepő annak, aki már dolgozott nagy Python kódalapokkal. Számos olyan modulpakettünk volt, amelyek közvetve importálták egymást. Ha bármelyik fájl a ciklusban megváltozott, a mypy-nak minden fájlt újra kellett dolgoznia, ami ebbe a ciklusba tartozott, és gyakran azokat a modulokat is, amelyek importálták a modulokat ebből a ciklusból. Az egyik ilyen ciklus a hírhedt „függőségi csomó” volt, amely sok kellemetlenséget okozott a Dropbox-nál. Egyszer ez a struktúra több száz modult tartalmazott, amelyet közvetlenül vagy közvetve sok teszt használt, és a termelési kódban is szerepelt.

Megfontoltuk a ciklikus függőségek „kibogozásának” lehetőségét, de nem volt erőforrásunk arra, hogy ezt megtegyük. Túl sok kóddal álltunk szemben, amelyet nem ismertünk. Végül az alternatív megközelítést választottuk. Azt döntöttük el, hogy a mypy gyorsan működjön még akkor is, ha „függőségi csomók” vannak. E cél eléréséhez mypy démont használtunk. A démon egy háttérfolyamat, amely két érdekes lehetőséget valósít meg. Először is – az egész kódalapról az információt a memóriában tartja. Ez azt jelenti, hogy a mypy minden indításakor nem kell betölteni a több ezer importált függőséghez tartozó cache-elt adatokat. Másodszor – alaposan, a legkisebb strukturális egységeken elemezve vizsgálja a függőségeket a funkciók és egyéb egységek között. Például ha egy funkció foo megkeresi a függvényt bar, akkor van egy függőség foo tól bar. Amikor egy fájl megváltozik – a démon először, izoláltan, csak a megváltozott fájlt dolgozza fel. Ezt követően megvizsgálja a fájl e változásait, amely a külső féltől látható, például a megváltozott funkciók aláírásait. A démon a részletes importinformációkat csak az olyan funkciók újraellenőrzésére használja, amelyek ténylegesen használják a megváltozott funkciót. Általában e megközelítés mellett csupán néhány funkciót kell ellenőrizni.

Mindez megvalósítása nem volt egyszerű, mivel az eredeti mypy megvalósítás nagyban a fájlok egyikének feldolgozására összpontosított. Számos határhelyzettel kellett szembenéznünk, amelyek újabb ellenőrzéseket igényeltek, amikor a kód bármilyen változást szenvedett. Például, ha egy osztály új alapértelmezett osztályt kapott. Miután elvégeztük, amit akartunk, a legtöbb inkrementális ellenőrzés végrehajtási idejét másodpercekre csökkenthettük. Ezt nagy győzelemnek éreztük.

Még nagyobb teljesítmény!

A fent említett távoli gyorsítással együttesen a mypy démon gyakorlatilag teljesen megoldotta azokat a problémákat, amelyek akkor merültek fel, amikor a programozó gyakran indította el a típusellenőrzést, kis mennyiségű fájl módosításával. Azonban a rendszer teljesítménye a legkevésbé kedvező használati esetben még mindig messze volt az optimálistól. A mypy tiszta indítása több mint 15 percet is igénybe vehetett. Ez sokkal több volt, mint amennyivel megelégedtünk volna. Minden héten a helyzet egyre rosszabb lett, mivel a programozók folyamatosan új kódot írtak és annotációkat adtak a meglévő kódhoz. Felhasználóink továbbra is nagyobb teljesítményre vágytak, mi pedig szívesen teljesítettük ezt a kérésüket.

Visszatértünk az egyik korai ötlethez a mypy-val kapcsolatban. Nevezetesen, a Python kód C kóddá való átírásához. A Cython-nal végzett kísérletek (ez egy rendszer, amely lehetővé teszi a Python-ban írt kód C kódra való fordítását) nem hoztak látható gyorsulást, ezért úgy döntöttünk, hogy újjáélesztjük a saját fordító írásának ötletét. Mivel a mypy kód alapja (amely Python-ban íródott) már tartalmazta az összes szükséges típusannotációt, úgy tűnt, megéri megpróbálni ezeket az annotációkat a rendszer teljesítményének fokozására. Gyorsan készítettem egy prototípust ennek az ötletnek a tesztelésére. Ez több mint 10-szeres teljesítménynövekedést mutatott különböző mikro-benchmarkok során. Az ötletünk az volt, hogy a Python modulokat C modulokká fordítjuk Cython segítségével, és hogy az annotációkat típusellenőrzésekké alakítjuk, amelyek a program futtatása során történnek (általában a típusannotációk figyelmen kívül maradnak a programok futása közben, és csak a típusellenőrző rendszerek használják őket). Valójában azt terveztük, hogy a mypy megvalósítását Pythonból egy statikusan típusos nyelvre fordítjuk, amely úgy néz ki (és nagyrészt úgy is működik), mint a Python. (Ez az internyelvi migrációs forma a mypy projekt hagyományává vált. A mypy eredeti megvalósítása Alore nyelven készült, majd a Java és Python szintaktikai hibridjére váltott.)

A CPython bővítmények API-jára való fókuszálás kulcsfontosságú volt ahhoz, hogy megtartsuk a projekt menedzsment lehetőségeit. Nem kellett létrehoznunk virtuális gépet vagy bármilyen könyvtárat, amire a mypy-nak szüksége volt. Ráadásul továbbra is hozzáférhettünk volna az egész Python ökoszisztémához, és minden eszköz (például pytest) elérhető lett volna. Ez azt jelentette, hogy a fejlesztés során továbbra is használhattuk a Python értelmezett kódot, ami lehetővé tette a kód gyors módosítását és tesztelését anélkül, hogy a kód fordítására kellett volna várnunk. Úgy tűnt, hogy remekül sikerül az úgynevezett "két széken ülni", és ez nagyon tetszett nekünk.

A fordító, amelyet mypyc-nek neveztünk el (mivel a mypy-t használja a típusok elemzésére a frontendjeként), meglehetősen sikeres projektté vált. Összességében körülbelül négyszeres gyorsulást értünk el a mypy gyakori indításához, anélkül hogy a cache-elést használnánk. A mypyc projekt magjának fejlesztése körülbelül 4 naptári hónapot vett igénybe egy kis csapat, amelyben Michael Sullivan, Ivan Levkivsky, Hugh Han és én magam is részt vettünk. Ez a munkamennyiség sokkal kisebb volt, mint ami a mypy más nyelvekre, például C++-ra vagy Go-ra való újraírásához szükséges lett volna. Ráadásul sokkal kevesebb változtatást kellett végrehajtanunk a projekten, mint ami egy másik nyelvre való újraírás során szükséges lenne. Emellett abban is bíztunk, hogy a mypyc-ot olyan szintre tudjuk emelni, hogy a Dropbox más programozói is használhassák a kódjuk fordítására és gyorsítására.

A hasonló teljesítmény elérése érdekében néhány érdekes mérnöki megoldást kellett alkalmaznunk. Így a fordító sok művelet végrehajtását felgyorsíthatja a C alacsony szintű gyors konstrukcióinak használatával. Például, egy lefordított függvény hívás C-függvény hívássá alakul. Egy ilyen hívás sokkal gyorsabb, mint egy értelmezett függvény hívása. Néhány művelet, mint például a szótárakban való keresés, még mindig a CPython szokásos C-API hívásait használták, amelyek a fordítás után csak kissé gyorsabbak voltak. El tudtuk tüntetni az értelmezés által okozott extra terheket a rendszerről, de ez az esetben csak kis mértékű teljesítménynövekedést hozott.

A legelterjedtebb "lassú" műveletek azonosítása érdekében kódprofilozást végeztünk. A megszerzett adatok birtokában megpróbáltuk vagy úgy "finomhangolni" a mypyc-t, hogy gyorsabb C-kódot generáljon az ilyen műveletekhez, vagy átkonvertálni a megfelelő Python-kódot gyorsabb műveletek használatával (néha egyszerűen nem volt elegendő megoldás a problémákra). A Python-kód átkonvertálása gyakran könnyebb megoldásnak bizonyult, mint a fordítóban ugyanazon transzformáció automatikus végrehajtásának megvalósítása. Hosszú távon sok ilyen transzformáció automatizálására vágytunk, de abban az időpontban a mypy gyorsítására törekedtünk minimális erőfeszítéssel. E cél felé haladva néhány sarkot lekerekítettünk.

Folytatás következik…

Használjátok a CI/CD technológiákat a projektjeitekben? Milyen benyomásokat keltett önben a mypy projekt, amikor először tudomást szerzett róla?

Út a 4 millió sor Python kód típusainak ellenőrzéséhez. 2. rész
Út a 4 millió sor Python kód típusainak ellenőrzéséhez. 2. rész

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster