Cesta k typovej kontrole 4 miliónov riadkov kódu Python. Časť 2

Dnes zverejňujeme druhú časť prekladu materiálu o tom, ako Dropbox organizoval kontrolu typov pre niekoľko miliónov riadkov kódu Python.

Cesta k typovej kontrole 4 miliónov riadkov kódu Python. Časť 2

Prečítajte si časť prvá

Oficiálna podpora typu (PEP 484)

Naše prvé seriózne experimenty s mypy sme uskutočnili v Dropboxe počas Hack Week 2014. Hack Week je týždenná udalosť, ktorú organizuje Dropbox. Počas tejto doby môžu zamestnanci pracovať na čom chcú! Niektoré z najznámejších technologických projektov Dropboxu sa začali na udalostiach, ako sú tieto. V dôsledku tohto experimentu sme dospeli k záveru, že mypy vyzerá sľubne, hoci projekt ešte nie je pripravený na široké použitie.

V tom čase bola vo vzduchu myšlienka štandardizácie systémov tipovania typu Python. Ako som povedal, od Pythonu 3.0 bolo možné pre funkcie používať typové anotácie, ale išlo len o ľubovoľné výrazy, bez definovanej syntaxe a sémantiky. Počas vykonávania programu boli tieto anotácie z väčšej časti jednoducho ignorované. Po Hack Week sme začali pracovať na štandardizácii sémantiky. Táto práca viedla k vzniku PEP 484 (Na tomto dokumente sme spolupracovali Guido van Rossum, Łukasz Langa a ja).

Na naše motívy sa dalo pozerať z dvoch strán. Po prvé, dúfali sme, že celý ekosystém Pythonu by mohol prijať spoločný prístup k používaniu tipov typu (termín používaný v Pythone ako ekvivalent „anotácií typu“). Vzhľadom na možné riziká by to bolo lepšie ako používanie mnohých vzájomne nekompatibilných prístupov. Po druhé, chceli sme otvorene diskutovať o mechanizmoch typovej anotácie s mnohými členmi komunity Python. Táto túžba bola čiastočne diktovaná skutočnosťou, že v očiach širokých más programátorov Pythonu by sme nechceli vyzerať ako „odpadlíci“ od základných myšlienok jazyka. Ide o dynamicky písaný jazyk, známy ako „kačacie písanie“. V komunite na samom začiatku nemohol nevzniknúť trochu podozrievavý postoj k myšlienke statického písania. Tento sentiment však nakoniec opadol, keď sa ukázalo, že statické písanie nebude povinné (a keď si ľudia uvedomili, že je to skutočne užitočné).

Syntax tipu typu, ktorá bola nakoniec prijatá, bola veľmi podobná tomu, čo v tom čase podporoval mypy. PEP 484 bol vydaný s Pythonom 3.5 v roku 2015. Python už nebol dynamicky písaný jazyk. Rád o tejto udalosti uvažujem ako o významnom míľniku v histórii Pythonu.

Začiatok migrácie

Na konci roka 2015 vytvoril Dropbox tím troch ľudí, ktorí pracovali na mypy. Patrili medzi nich Guido van Rossum, Greg Price a David Fisher. Od tohto momentu sa situácia začala vyvíjať mimoriadne rýchlo. Prvou prekážkou rastu mypy bol výkon. Ako som naznačil vyššie, v začiatkoch projektu som uvažoval o preklade implementácie mypy do C, ale tento nápad bol zatiaľ zo zoznamu vyškrtnutý. Zasekli sme sa pri spustení systému pomocou tlmočníka CPython, ktorý nie je dostatočne rýchly pre nástroje ako mypy. (Nepomohol nám ani projekt PyPy, alternatívna implementácia Pythonu s JIT kompilátorom.)

Našťastie nám tu pomohli niektoré vylepšenia algoritmov. Prvým silným „urýchľovačom“ bola implementácia prírastkovej kontroly. Myšlienka tohto zlepšenia bola jednoduchá: ak sa všetky závislosti modulu od predchádzajúceho spustenia mypy nezmenili, potom môžeme pri práci so závislosťami použiť údaje uložené počas predchádzajúceho spustenia. Potrebovali sme vykonať iba kontrolu typu upravených súborov a súborov, ktoré na nich záviseli. Mypy dokonca zašiel o niečo ďalej: ak sa externé rozhranie modulu nezmenilo, mypy predpokladal, že ostatné moduly, ktoré importovali tento modul, nie je potrebné znova kontrolovať.

Inkrementálna kontrola nám veľmi pomohla pri anotovaní veľkého množstva existujúceho kódu. Ide o to, že tento proces zvyčajne zahŕňa mnoho opakovaných spustení mypy, pretože anotácie sa postupne pridávajú do kódu a postupne sa zlepšujú. Prvé spustenie mypy bolo stále veľmi pomalé, pretože bolo potrebné skontrolovať veľa závislostí. Potom sme na zlepšenie situácie implementovali mechanizmus vzdialeného ukladania do vyrovnávacej pamäte. Ak mypy zistí, že lokálna vyrovnávacia pamäť je pravdepodobne zastaraná, stiahne aktuálnu snímku vyrovnávacej pamäte pre celú kódovú základňu z centralizovaného úložiska. Potom vykoná prírastkovú kontrolu pomocou tejto snímky. To nás posunulo o ďalší veľký krok smerom k zvýšeniu výkonu mypy.

Bolo to obdobie rýchleho a prirodzeného prijatia kontroly typu v Dropboxe. Do konca roka 2016 sme už mali približne 420000 XNUMX riadkov kódu Python s typovými anotáciami. Mnohí používatelia boli nadšení z kontroly typu. Stále viac vývojových tímov používalo Dropbox mypy.

Všetko vtedy vyzeralo dobre, ale stále sme mali veľa práce. Začali sme vykonávať periodické interné užívateľské prieskumy, aby sme identifikovali problémové oblasti projektu a pochopili, aké problémy je potrebné najskôr vyriešiť (táto prax sa v spoločnosti používa dodnes). Najdôležitejšie, ako sa ukázalo, boli dve úlohy. Po prvé, potrebovali sme viac typov pokrytia kódu, po druhé, potrebovali sme, aby mypy fungoval rýchlejšie. Bolo úplne jasné, že naša práca na zrýchlení mypy a jej implementácii do firemných projektov ešte zďaleka nie je dokončená. My, plne si vedomí dôležitosti týchto dvoch úloh, sme sa pustili do ich riešenia.

Viac produktivity!

Postupné kontroly zrýchlili mypy, ale nástroj stále nebol dostatočne rýchly. Mnohé postupné kontroly trvali asi minútu. Dôvodom boli cyklické dovozy. To asi neprekvapí nikoho, kto pracoval s veľkými kódovými základňami napísanými v Pythone. Mali sme sady stoviek modulov, z ktorých každý nepriamo importoval všetky ostatné. Ak sa zmenil akýkoľvek súbor v slučke importu, mypy musel spracovať všetky súbory v tejto slučke a často aj všetky moduly, ktoré importovali moduly z tejto slučky. Jedným z takýchto cyklov bola neslávne známa „spleť závislosti“, ktorá spôsobila v Dropboxe veľa problémov. Kedysi táto štruktúra obsahovala niekoľko stoviek modulov, pričom bola dovezená, priamo či nepriamo, množstvo testov, bola použitá aj vo výrobnom kóde.

Zvažovali sme možnosť „rozmotania“ kruhových závislostí, ale nemali sme na to prostriedky. Bolo tam príliš veľa kódu, ktorý sme nepoznali. V dôsledku toho sme prišli s alternatívnym prístupom. Rozhodli sme sa, že mypy bude fungovať rýchlo aj v prítomnosti „závislosti“. Tento cieľ sme dosiahli pomocou démona mypy. Démon je serverový proces, ktorý implementuje dve zaujímavé funkcie. Po prvé, ukladá informácie o celej kódovej základni do pamäte. To znamená, že zakaždým, keď spustíte mypy, nemusíte načítať údaje uložené vo vyrovnávacej pamäti súvisiace s tisíckami importovaných závislostí. Po druhé, na úrovni malých štruktúrnych jednotiek starostlivo analyzuje závislosti medzi funkciami a inými entitami. Napríklad, ak funkcia foo volá funkciu bar, potom vzniká závislosť foo od bar. Keď sa súbor zmení, démon najprv izolovane spracuje iba zmenený súbor. Potom sa pozrie na externe viditeľné zmeny tohto súboru, ako napríklad zmenené podpisy funkcií. Démon používa podrobné informácie o importoch iba na dvojitú kontrolu tých funkcií, ktoré skutočne používajú upravenú funkciu. Pri tomto prístupe musíte zvyčajne skontrolovať veľmi málo funkcií.

Implementácia tohto všetkého nebola jednoduchá, pretože pôvodná implementácia mypy bola silne zameraná na spracovanie jedného súboru naraz. Museli sme riešiť mnohé hraničné situácie, ktorých vznik si vyžadoval opakované kontroly v prípadoch, keď sa niečo zmenilo v kódexe. Stáva sa to napríklad, keď je triede priradená nová základná trieda. Keď sme urobili to, čo sme chceli, dokázali sme skrátiť čas vykonania väčšiny prírastkových kontrol len na niekoľko sekúnd. Zdalo sa nám to ako veľké víťazstvo.

Ešte viac produktivity!

Spolu so vzdialeným ukladaním do vyrovnávacej pamäte, o ktorom som hovoril vyššie, démon mypy takmer úplne vyriešil problémy, ktoré vznikajú, keď programátor často spúšťa kontrolu typu a vykonáva zmeny v malom počte súborov. Výkon systému v najmenej priaznivom prípade použitia však stále nebol optimálny. Čisté spustenie mypy môže trvať viac ako 15 minút. A to bolo oveľa viac, ako by sme boli spokojní. Každý týždeň sa situácia zhoršovala, pretože programátori pokračovali v písaní nového kódu a pridávaní anotácií do existujúceho kódu. Naši používatelia boli stále hladní po väčšom výkone, no boli sme radi, že sme im vyšli v ústrety na polceste.

Rozhodli sme sa vrátiť k jednej z predchádzajúcich myšlienok o mypy. Konkrétne na konverziu kódu Python na kód C. Experimentovanie s Cythonom (systém, ktorý vám umožňuje prekladať kód napísaný v Pythone do kódu C) nám neprinieslo žiadne viditeľné zrýchlenie, preto sme sa rozhodli oživiť myšlienku písania vlastného kompilátora. Keďže kódová základňa mypy (napísaná v Pythone) už obsahovala všetky potrebné anotácie typu, mysleli sme si, že by stálo za to skúsiť použiť tieto anotácie na zrýchlenie systému. Rýchlo som vytvoril prototyp na testovanie tohto nápadu. Vykázala viac ako 10-násobný nárast výkonu na rôznych mikrobenchmarkoch. Našou myšlienkou bolo skompilovať moduly Pythonu do modulov C pomocou Cythonu a premeniť anotácie typu na kontroly typu spustenia (zvyčajne sa anotácie typu za behu ignorujú a používajú ich iba systémy na kontrolu typu). Vlastne sme plánovali preložiť implementáciu mypy z Pythonu do jazyka, ktorý bol navrhnutý na statické typovanie, ktorý by vyzeral (a z väčšej časti aj fungoval) presne ako Python. (Tento druh migrácie medzi jazykmi sa stal akousi tradíciou projektu mypy. Pôvodná implementácia mypy bola napísaná v Alore, potom existoval syntaktický hybrid Java a Python).

Zameranie sa na rozhranie API rozšírenia CPython bolo kľúčom k tomu, aby ste nestratili možnosti riadenia projektov. Nepotrebovali sme implementovať virtuálny stroj ani žiadne knižnice, ktoré mypy potreboval. Okrem toho by sme stále mali prístup k celému ekosystému Python a všetkým nástrojom (napríklad pytest). To znamenalo, že sme mohli pokračovať v používaní interpretovaného kódu Pythonu počas vývoja, čo nám umožnilo pokračovať v práci s veľmi rýchlym vzorom vykonávania zmien kódu a jeho testovania namiesto čakania na kompiláciu kódu. Vyzeralo to tak, že sme odviedli skvelú prácu, keď sme takpovediac sedeli na dvoch stoličkách, a páčilo sa nám to.

Kompilátor, ktorý sme nazvali mypyc (keďže používa mypy ako front-end na analýzu typov), sa ukázal ako veľmi úspešný projekt. Celkovo sme dosiahli približne 4-násobné zrýchlenie pre časté spúšťanie mypy bez ukladania do vyrovnávacej pamäte. Vývoj jadra projektu mypyc trval malému tímu Michaela Sullivana, Ivana Levkivského, Hugha Hahna a mne približne 4 kalendárne mesiace. Toto množstvo práce bolo oveľa menšie ako to, čo by bolo potrebné na prepísanie mypy, napríklad v C++ alebo Go. A v projekte sme museli urobiť oveľa menej zmien, ako by sme museli urobiť pri prepisovaní do iného jazyka. Tiež sme dúfali, že sa nám podarí posunúť mypyc na takú úroveň, aby ho ostatní programátori Dropboxu mohli použiť na kompiláciu a zrýchlenie svojho kódu.

Aby sme dosiahli túto úroveň výkonu, museli sme použiť niekoľko zaujímavých technických riešení. Kompilátor teda môže zrýchliť mnohé operácie pomocou rýchlych konštrukcií C na nízkej úrovni. Napríklad skompilované volanie funkcie je preložené do volania funkcie C. A takéto volanie je oveľa rýchlejšie ako volanie interpretovanej funkcie. Niektoré operácie, ako napríklad vyhľadávanie v slovníkoch, stále zahŕňali používanie bežných volaní C-API z CPythonu, ktoré boli pri kompilácii len o niečo rýchlejšie. Podarilo sa nám eliminovať dodatočnú záťaž systému vytvorenú interpretáciou, čo však v tomto prípade prinieslo len malý zisk z hľadiska výkonu.

Na identifikáciu najbežnejších „pomalých“ operácií sme vykonali profilovanie kódu. Vyzbrojení týmito údajmi sme sa pokúsili buď vyladiť mypyc tak, aby generoval rýchlejší kód C pre takéto operácie, alebo prepísať zodpovedajúci kód Pythonu pomocou rýchlejších operácií (a niekedy sme jednoducho nemali dostatočne jednoduché riešenie pre tento alebo iný problém) . Prepísanie kódu Pythonu bolo často jednoduchším riešením problému, ako keby kompilátor automaticky vykonal rovnakú transformáciu. Z dlhodobého hľadiska sme chceli zautomatizovať mnohé z týchto transformácií, ale v tom čase sme sa zamerali na zrýchlenie mypy s minimálnym úsilím. A pri posúvaní sa k tomuto cieľu sme odstrihli niekoľko rohov.

Ak sa chcete pokračovať ...

Vážení čitatelia! Aké boli vaše dojmy z projektu mypy, keď ste sa dozvedeli o jeho existencii?

Cesta k typovej kontrole 4 miliónov riadkov kódu Python. Časť 2
Cesta k typovej kontrole 4 miliónov riadkov kódu Python. Časť 2

Zdroj: hab.com

Pridať komentár