Dnes zverejňujeme druhú časť prekladu materiálu o tom, ako v Dropboxe organizovali kontrolu typov niekoľkých miliónov riadkov Python kódu.
→
Oficiálna podpora typov (PEP 484)
Prvé vážne experimenty s mypy sme uskutočnili v Dropboxe počas Hack Weeku 2014. Hack Week je podujatie, ktoré sa uskutočňuje v Dropboxe počas jedného týždňa. V tomto čase môžu zamestnanci pracovať na čomkoľvek! Niektoré z najznámejších technologických projektov Dropboxu sa začali práve počas týchto podujatí. Z tohto experimentu sme vyvodili závery, že mypy vyzerá sľubne, hoci tento projekt zatiaľ nebol pripravený na široké použitie.
V tom čase bola vo vzduchu myšlienka na štandardizáciu systémov vydávania odporúčaní o typoch Pythonu. Ako som už spomenul, už od Pythonu 3.0 bolo možné používať anotácie typov pre funkcie, ale to boli len náhodné výrazy, bez určeného syntaxe a sémantiky. Počas vykonávania programu sa tieto anotácie väčšinou len ignorovali. Po Hack Weeku sme začali pracovať na štandardizácii sémantiky. Táto práca viedla k vzniku (na tomto dokumente sa spolupodieľali Guido van Rossum, Lukáš Langa a ja).
Naše motivácie mohli byť posudzované z dvoch perspektív. Po prvé, dúfali sme, že celá ekosystém Pythonu by mohol prijať spoločný prístup k používaniu odporúčaní typov (type hints - termín, ktorý sa v Python používa ako ekvivalent „anotácií typov“). To by, vo svetle možných rizík, bolo lepšie než používanie množstva vzájomne nekompatibilných prístupov. Po druhé, chceli sme otvorene diskutovať o mechanizmoch anotovania typov s množstvom predstaviteľov Python komunity. Čiastočne bolo toto želanie motivované tým, že sme nechceli vyzerať ako „odpadlíci“ od základných myšlienok jazyka v očiach širokej verejnosti Python programátorov. Ide o dynamicky typizovaný jazyk, známy „kačacou typizáciou“. V komunite bol zo začiatku nevyhnutne zaujatý postoj k myšlienke statickej typizácie. Ale tento postoj sa nakoniec oslaboval - po tom, čo sa ukázalo, že zavedenie statickej typizácie nie je plánované ako povinné (a po tom, čo si ľudia uvedomili, že je skutočne užitočná).
Prijatý syntax typových nápoved bol nakoniec veľmi podobný tomu, ktorý vtedy podporoval mypy. Dokument PEP 484 bol vydaný spolu s Python 3.5 v roku 2015. Python už nebol jazykom, ktorý podporoval len dynamickú typizáciu. Rád nad týmto udalím rozmýšľam ako o významnom míľniku v histórii Pythonu.
Začiatok migrácie
Na konci roka 2015 bola v Dropboxe vytvorená trojčlenná tím pre prácu na mypy. Tvorili ho Guido van Rossum, Greg Price a David Fisher. Od tohto okamihu sa situácia začala rozvíjať veľmi rýchlo. Prvým problémom na ceste rastu mypy bola výkonnosť. Ako som už naznačil vyššie, v ranných fázach vývoja projektu som uvažoval o presunutí implementácie mypy do jazyka C, ale táto myšlienka bola zatiaľ z programu vyčlenená. Uviazli sme na tom, že systém bežal na interpreteri CPython, ktorý neprekvapil dostatočnou rýchlosťou pre nástroje akým je mypy. (Projekt PyPy, alternatívna implementácia Pythonu s JIT kompilátorom, nám tiež príliš nepomohol.)
Našťastie nám prichádzali na pomoc niektoré algoritmické vylepšenia. Prvým silným "zrýchľovačom" bola implementácia inkrementálnych kontrol. Myšlienka tohto vylepšenia bola jednoduchá: ak sa všetky závislosti modulu od posledného spustenia mypy nezmenili, môžeme pri práci so závislosťami použiť údaje, ktoré boli uložené počas predchádzajúcej relácie. Museli sme len vykonať kontrolu typov v zmenených súboroch a v tých súboroch, ktoré od nich záviseli. Mypy išiel dokonca o krok ďalej: ak sa vonkajší rozhranie modulu nezmenilo — mypy predpokladal, že iné moduly, ktoré tento modul importujú, nie je potrebné kontrolovať znovu.
Inkrementálna kontrola nám veľmi pomohla pri anotovaní veľkých objemov existujúceho kódu. Ide o to, že tento proces zvyčajne zahŕňa množstvo iteratívnych spustení mypy, pretože anotácie sú postupne pridávané do kódu a postupne vylepšované. Prvé spustenie mypy bolo stále veľmi pomalé, pretože pri jeho vykonávaní bolo potrebné overiť množstvo závislostí. Preto sme, v snahe zlepšiť situáciu, implementovali mechanizmus vzdialeného caching-u. Ak mypy zistí, že lokálny cache je pravdepodobne zastaraný, načíta aktuálny snapshot cache pre celú kódovú základňu z centralizovaného repozitára. Potom vykonáva inkrementálnu kontrolu s použitím tohto snapshotu. To nás posunulo o ďalší významný krok na ceste k zvýšeniu výkonu mypy.
Bol to čas rýchlej a prirodzenej implementácie systému kontroly typov v Dropboxe. Na konci roka 2016 sme mali už asi 420 000 riadkov Python kódu s anotáciami typov. Mnohí používatelia sa nadšene postavili k kontrole typov. V Dropboxe mypy využívalo čoraz viac tímov vývojárov.
Všetko vtedy vyzeralo dobre, ale ešte nás čakalo veľa práce. Začali sme vykonávať pravidelné interné prieskumy používateľov, aby sme identifikovali problematické oblasti projektu a pochopili, aké otázky je potrebné riešiť ako prvé (táto prax sa v spoločnosti používa aj dnes). Ako sa ukázalo, najdôležitejšie boli dve úlohy. Prvá — potrebovali sme väčšie pokrytie kódu typmi, druhá — mypy musel pracovať rýchlejšie. Bolo úplne jasné, že naša práca na zrýchlení mypy a jeho implementácii do projektov spoločnosti bola ešte ďaleko od dokončenia. Plne sme si uvedomovali dôležitosť týchto dvoch úloh a pustili sme sa do ich riešenia.
Viac výkonu!
Inkrementálne kontroly zrýchlili mypy, ale tento nástroj stále nebol dosť rýchly. Mnohé inkrementálne kontroly trvali okolo minúty. Príčinou bola cyklická importácia. To pravdepodobne neprekvapí nikoho, kto pracoval s veľkými kódovými základmi napísanými v Pythone. Mali sme sady zo stoviek modulov, z ktorých každý nepriamo importoval všetky ostatné. Ak bol akýkoľvek súbor v cykle importov zmenený, mypy musel spracovať všetky súbory, ktoré patrili do tohto cyklu, a často aj akékoľvek moduly, ktoré importovali moduly z tohto cyklu. Jedným z takýchto cyklov bol známy "klb závislostí", ktorý spôsobil množstvo problémov v Dropboxe. Raz táto štruktúra obsahovala niekoľko stoviek modulov, pričom bola importovaná, priamo alebo nepriamo, množstvom testov a používala sa aj v produkčnom kóde.
Zvažovali sme možnosť "rozuzlenia" cyklických závislostí, ale nemali sme zdroje na to, aby sme to uskutočnili. Bolo tam príliš veľa kódu, s ktorým sme neboli oboznámení. Nakoniec sme sa dostali k alternatívnemu prístupu. Rozhodli sme sa, že mypy bude rýchlo fungovať aj pri existencii "klbov závislostí". Tento cieľ sme dosiahli pomocou démona mypy. Demon je serverový proces, ktorý realizuje dve zaujímavé funkcie. Po prvé – uchováva v pamäti informácie o celej kódovej báze. To znamená, že pri každom spustení mypy nie je potrebné načítavať cachované údaje týkajúce sa tisícov importovaných závislostí. Po druhé – starostlivo, na úrovni drobných štrukturálnych jednotiek, analyzuje závislosti medzi funkciami a inými entitami. Napríklad, ak funkcia foo vyvoláva funkciu bar, potom existuje závislosť foo od bar. Keď sa súbor zmení – démon najprv, v izolácii, spracováva iba zmenený súbor. Potom sa pozrie na zmeny tohto súboru, viditeľné navonok, ako sú zmenené podpisy funkcií. Demon používa podrobné informácie o importoch iba na overenie tých funkcií, ktoré naozaj používajú zmenenú funkciu. Zvyčajne je pri takomto prístupe potrebné overiť len veľmi málo funkcií.
Realizácia všetkého tohto nebola jednoduchá úloha, pretože pôvodná implementácia mypy bola veľmi zameraná na spracovanie jedného súboru naraz. Museli sme čeliť mnohým hraničným situáciám, ktorých vznik si vyžadoval opakované kontroly v prípadoch, keď sa v kóde niečo zmenilo. Napríklad to nastáva, keď triede pridelia novú základnú triedu. Po tom, čo sme dosiahli, čo sme chceli, sme dokázali znížiť čas vykonávania väčšiny inkrementálnych kontrol na niekoľko sekúnd. Považovali sme to za veľké víťazstvo.
Ešte viac výkonu!
Spolu s diaľkovým cachingom, o ktorom som hovoril vyššie, démon mypy prakticky úplne vyriešil problémy, ktoré vznikajú, keď programátor často spúšťa kontrolu typov, pričom robí zmeny v malom počte súborov. Avšak výkon systému v najmenej priaznivom variante jeho použitia bol stále ďaleko od optimálneho. Čisté spustenie mypy mohlo trvať viac než 15 minút. A to bolo oveľa viac, než by nám vyhovovalo. 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í k existujúcemu kódu. Naši používatelia stále túžili po väčšej výkonnosti, a my sme boli ochotní im vyhovieť.
Rozhodli sme sa vrátiť k jednej z našich skorších myšlienok týkajúcich sa mypy. A síce k transformácii Python kódu na C kód. Experimenty s Cython (čo je systém, ktorý umožňuje prekladať kód napísaný v Pythone do C kódu) nám nepriniesli žiadne viditeľné zrýchlenie, a 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é typové anotácie, zdalo sa nám, že stojí za to pokúsiť sa využiť tieto anotácie na zrýchlenie práce systému. Rýchlo som vytvoril prototyp na overenie tejto myšlienky, ktorý ukázal na rôznych mikrobenchmarkoch viac ako 10-násobný nárast výkonu. Naša myšlienka spočívala v tom, aby sme prekladali Python moduly na C moduly pomocou Cython a premenili typové anotácie na typové kontroly vykonávané počas behu programu (zvyčajne sa typové anotácie ignorujú počas behu programov a používajú sa len systémami na kontrolu typov). V skutočnosti sme plánovali preniesť implementáciu mypy z Pythonu do jazyka, ktorý bol vytvorený staticky typovaným spôsobom, ktorý by vyzeral (a vo väčšine by aj fungoval) presne ako Python. (Tento druh medzi-jazykovej migrácie sa stal akousi tradíciou projektu mypy. Pôvodná implementácia mypy bola napísaná v Alore, potom bol syntaktický hybrid Java a Python).
Zameranie na API rozšírení CPython bolo kľúčom k tomu, aby sme nestratili možnosti pri správe projektu. Nemuseli sme implementovať virtuálny stroj alebo akékoľvek knižnice, ktoré mypy potreboval. Okrem toho by nám stále bola k dispozícii celá ekosystém Pythonu, všetky nástroje (ako pytest). To znamenalo, že sme mohli pokračovať v používaní interpretovaného Python kódu počas vývoja, čo nám umožnilo pokračovať v práci s veľmi rýchlym schémirakom zmien v kóde a jeho testovania, a nie čakať na kompiláciu kódu. Vyzeralo to tak, že sa nám skvele darí, keby sme to tak povedali, sedieť na dvoch stoličkách, a to sa nám páčilo.
Kompilátor, ktorý sme nazvali mypyc (keďže ako frontend používa na analýzu typov mypy), sa ukázal byť veľmi úspešným projektom. Celkovo sme dosiahli približne 4-násobné urýchlenie častých spustení mypy bez použitia cache. Vývoj jadra projektu mypyc trval malej skupine, do ktorej patrili Michael Sullivan, Ivan Levkivsky, Hugh Han a ja, približne 4 kalendárne mesiace. Tento objem práce bol oveľa menší, ako by bol potrebný na prepis mypy, napríklad do C++ alebo Go. A zmien v projekte sme museli vykonať oveľa menej, než keby sme ho prepisovali do iného jazyka. Verili sme tiež, že dokážeme mypyc vylepšiť do takej úrovne, aby ho mohli používať aj iní programátori z Dropboxu na kompiláciu a urýchlenie svojho kódu.
Na dosiahnutie podobnej úrovne výkonu sme museli aplikovať niektoré zaujímavé inžinierske riešenia. Takto kompilátor môže urýchliť vykonávanie mnohých operácií použitím rýchlych nízkoúrovňových konštrukcií C. Napríklad, volanie skompilovanej funkcie sa preloží na volanie C-funkcie. A takéto volanie sa vykonáva oveľa rýchlejšie ako volanie interpretovanej funkcie. Niektoré operácie, ako vyhľadávanie v slovníkoch, sa stále museli riadiť bežnými volaniami C-API z CPythonu, ktoré po kompilácii boli len o niečo rýchlejšie. Podarilo sa nám zbaviť dodatočnej záťaže na systém, ktorú vytváralo interpretovanie, ale to v tomto prípade prinieslo len malý zisk z hľadiska výkonu.
Na odhalenie najbežnejších „pomalých“ operácií sme vykonali profilovanie kódu. S použitím získaných údajov sme sa pokúsili buď tak „vyladiť“ mypyc, aby generoval rýchlejší C-kód pre podobné operácie, alebo prepracovať príslušný Python kód pomocou rýchlejších operácií (niekedy sme jednoducho nemali dostatočne jednoduché riešenie pre daný problém). Prepisovanie Python kódu sa často ukázalo ako jednoduchšie riešenie problému ako implementácia automatického vykonávania tej istej transformácie v kompilátore. Z dlhodobého hľadiska sme chceli automatizovať mnohé z týchto transformácií, ale v tej chvíli sme sa zameriavali na to, aby sme s minimálnym úsilím zrýchlili mypy. A na ceste k tomuto cieľu sme urobili niekoľko ústupkov.
Pokračování následuje…
Vážení čitatelia! Aké dojmy vo vás vyvolal projekt mypy v čase, keď ste sa o ňom dozvedeli?
Zdroj: habr.com
