Cesta k ověřování typů 4 milionů řádků kódu v Pythonu. Část 2

Dnes publikujeme druhou část překladu materiálu o tom, jak v Dropboxu organizovali kontrolu typů několika milionů řádků Python kódu.

Cesta k ověřování typů 4 milionů řádků kódu v Pythonu. Část 2

→ Číst první část

Oficiální podpora typů (PEP 484)

Na Hack Weeku 2014 jsme provedli naše první vážné experimenty s mypy v Dropboxu. Hack Week je akce, kterou Dropbox pořádá jednou za rok. Během tohoto týdne mají zaměstnanci možnost pracovat na čemkoli! Některé z nejznámějších technických projektů Dropboxu začaly právě na těchto akcích. Z těchto experimentů jsme dospěli k závěru, že mypy vypadá slibně, ačkoliv tento projekt ještě nebyl připraven k širokému použití.

V té době se ve vzduchu vznášela myšlenka na standardizaci systémů pro poskytování typových nápověd v Pythonu. Jak jsem již zmínil, od Pythonu 3.0 bylo možné používat typové anotace pro funkce, ale šlo pouze o libovolné výrazy, bez definovaného syntaxe a sémantiky. Během provádění programu byli většinou tyto anotace jednoduše ignorovány. Po Hack Weeku jsme začali pracovat na standardizaci sémantiky. Tato práce vedla k přijetí PEP 484 (na tomto dokumentu se podíleli Guido van Rossum, Lukáš Langa a já).

Naše motivy lze posuzovat ze dvou stran. Za prvé, doufali jsme, že celá ekosystém Pythonu by mohl přistoupit k jednotnému přístupu k používání typových nápověd (type hints — termín použité v Pythonu jako analog „typových anotací“). To by, s ohledem na možné rizika, bylo lepší než používání mnoha vzájemně neslučitelných přístupů. Za druhé, chtěli jsme otevřeně diskutovat o mechanismech typového anotování s mnoha zástupci komunity Python. Částečně bylo toto přání motivováno tím, že jsme nechtěli vypadat jako „odpadlíci“ od základních myšlenek jazyka v očích široké veřejnosti Python programátorů. Je to dynamicky typovaný jazyk, známý „kachením typizace“. V komunitě, na začátku, nemohlo nevzniknout několika podezřelé názory na myšlenku statické typizace. Ale tento postoj nakonec oslabil — poté, co bylo jasné, že statická typizace nebude povinná (a poté, co lidé pochopili, že je skutečně užitečná).

Přijatá syntaxe typových návrhů byla velmi podobná té, kterou tehdy podporoval mypy. Dokument PEP 484 byl vydán spolu s Pythonem 3.5 v roce 2015. Python již nebyl jazykem, který podporoval pouze dynamické typování. Rád o této události přemýšlím jako o významném milníku v historii Pythonu.

Začátek migrace

Na konci roku 2015 byla v Dropboxu pro práci na mypy vytvořena tým tří lidí. Patřili sem Guido van Rossum, Greg Price a David Fisher. Od tohoto okamžiku se situace začala velmi rychle vyvíjet. Prvním problémem na cestě za růstem mypy byla výkonnost. Jak jsem již naznačil výše, v rané fázi vývoje projektu jsem přemýšlel o překladu implementace mypy do jazyka C, ale tato myšlenka byla prozatím vyřazena. Zůstali jsme na tom, že k spuštění systému byl použit interpretr CPython, který nemá dostatečnou rychlost pro nástroje jako mypy. (Projekt PyPy, alternativní implementace Pythonu s JIT kompilátorem, nám také nepomohl.)

Naštěstí nám sem přišly na pomoc některé algoritmické vylepšení. Prvním mocným "zrychlovačem" byla implementace inkrementální kontroly. Myšlenka tohoto vylepšení byla jednoduchá: pokud se všechny závislosti modulu od posledního spuštění mypy nezměnily, můžeme během práce se závislostmi použít data, která byla ukládána během předchozího sezení. Museli jsme pouze provést kontrolu typů ve změněných souborech a v těch souborech, které na ně závisely. Mypy šel dokonce o něco dál: pokud se externí rozhraní modulu nezměnilo – mypy považoval, že není nutné znovu kontrolovat jiné moduly, které tento modul importují.

Inkrementální kontrola nám velmi pomohla při anotaci velkých objemů existujícího kódu. Tento proces obvykle zahrnuje mnoho iterativních spuštění mypy, protože anotace se do kódu postupně přidávají a zlepšují. První spuštění mypy bylo stále velmi pomalé, protože během jeho provádění bylo nutné zkontrolovat mnoho závislostí. Abychom situaci zlepšili, implementovali jsme mechanismus dálkového caching. Pokud mypy zjistí, že místní cache je pravděpodobně zastaralá, načte aktuální snapshot cache pro celý kód ze centralizovaného repozitáře. Poté provede inkrementální kontrolu s použitím tohoto snapshotu. To nás posunulo zase o významný krok vpřed k zvýšení výkonu mypy.

Bylo to období rychlého a přirozeného zavádění systému kontroly typů v Dropboxu. Na konci roku 2016 jsme již měli zhruba 420000 řádků Python kódu s anotacemi typů. Mnoho uživatelů se nadšeně zapojilo do kontroly typů. V Dropboxu mypy využívalo čím dál více týmů vývojářů.

Všechno tehdy vypadalo dobře, ale ještě nás čekalo mnoho práce. Začali jsme provádět pravidelné interní dotazování uživatelů, abychom zjistili problematické oblasti projektu a pochopili, které otázky je třeba řešit jako první (tato praxe se používá ve společnosti dodnes). Nejvýznamnějšími úkoly byly, jak se ukázalo, dvě úkoly. První — potřebovali jsme větší pokrytí kódu typy, druhá — mypy potřeboval fungovat rychleji. Bylo zcela jasné, že naše práce na zrychlení mypy a jeho zavedení do projektů společnosti byla ještě daleko od dokončení. V plné míře si uvědomujíc důležitost těchto dvou úkolů, pustili jsme se do jejich řešení.

Více výkonu!

Inkrementální kontroly urychlily mypy, ale tento nástroj stále nebyl dostatečně rychlý. Mnoho inkrementálních kontrol trvalo kolem minuty. Příčinou byla cyklická importování. To pravděpodobně nepřekvapí ty, kdo pracovali s velkými kódovými základnami napsanými v Pythonu. Měli jsme sady ze stovek modulů, z nichž každý nepřímo importoval všechny ostatní. Pokud byl jakýkoli soubor v cyklu importů změněn, mypy musel zpracovat všechny soubory, které do tohoto cyklu patřily, a často i jakékoli moduly importující moduly z tohoto cyklu. Jedním z těchto cyklů byla notoricky známá „klubko závislostí“, které způsobilo mnoho nepříjemností v Dropboxu. Jednou tato struktura obsahovala několik set modulů, přičemž ji importovalo, přímo nebo nepřímo, mnoho testů a byla používána i v produkčním kódu.

Zvažovali jsme možnost „rozpletení“ cyklických závislostí, ale neměli jsme prostředky na to, abychom to udělali. Bylo tam příliš mnoho kódu, se kterým jsme nebyli obeznámeni. Nakonec jsme se dostali k alternativnímu přístupu. Rozhodli jsme se, že mypy bude fungovat rychle i v případě „klubků závislostí“. Tento cíl jsme dosáhli pomocí démona mypy. Démona - to je serverový proces, který implementuje dvě zajímavé funkce. Za prvé - uchovává v paměti informace o celé kódové základně. To znamená, že při každém spuštění mypy není třeba načítat vyrovnávací data vztahující se k tisícům importovaných závislostí. Za druhé - důkladně analyzuje závislosti mezi funkcemi a jinými entitami na úrovni malých strukturálních jednotek. Například, pokud funkce foo vyvolává funkci bar, pak existuje závislost foo od bar. Když se soubor změní - démon nejprve izolovaně zpracovává pouze změněný soubor. Poté se podívá na změny tohoto souboru, které jsou viditelné zvenčí, jako jsou například změněné podpisy funkcí. Démon používá podrobné informace o importech pouze pro opětovné ověření těch funkcí, které skutečně používají změněnou funkci. Obvykle při tomto přístupu je třeba kontrolovat velmi málo funkcí.

Realizace toho všeho nebyla snadná, protože původní implementace mypy byla silně zaměřena na zpracování jednoho souboru najednou. Museli jsme se vypořádat s mnoha okrajovými situacemi, jejichž vznik vyžadoval opakované kontroly, kdykoli došlo k jakékoliv změně v kódu. Například se to stává, když je třídě přiřazen nový základní třída. Po tom, co jsme udělali to, co jsme chtěli, jsme dokázali snížit čas provádění většiny inkrementálních kontrol na několik sekund. Přišlo nám to jako velké vítězství.

Ještě větší výkon!

Společně s dálkovým kešováním, o kterém jsem se zmínil výše, démon mypy prakticky úplně vyřešil problémy, které vznikaly, když programátor často spouštěl kontrolu typů, přičemž prováděl změny v malém počtu souborů. Nicméně výkon systému v nejméně příznivém variantě jeho použití byl stále daleko od optimálního. Čisté spuštění mypy mohlo trvat více než 15 minut. A to bylo mnohem více, než bychom byli ochotni akceptovat. Situace se každým týdnem zhoršovala, protože programátoři pokračovali v psaní nového kódu a přidávání anotací k existujícímu kódu. Naši uživatelé stále toužili po větším výkonu a my jsme s potěšením byli připraveni jim vyjít vstříc.

Rozhodli jsme se vrátit k jedné z raných myšlenek ohledně mypy. A to konkrétně k převodu kódu Pythonu do kódu C. Experimenty s Cythonem (což je systém, který umožňuje převádět kód napsaný v Pythonu do kódu C) nám nepřinesly žádné viditelné zrychlení, takže jsme se rozhodli oživit myšlenku napsání vlastního kompilátoru. Vzhledem k tomu, že kódová základna mypy (napsaná v Pythonu) již obsahovala všechny potřebné typové anotace, přišlo nám smysluplné vyzkoušet tyto anotace k urychlení chodu systému. Rychle jsem vytvořil prototyp pro ověření této myšlenky. Ukázal více než desetinásobný nárůst výkonu na různých mikrobenchmarcích. Naší myšlenkou bylo kompilovat moduly Pythonu na moduly C pomocí Cythona a převádět typové anotace na typové kontroly prováděné během běhu programu (typové anotace se obvykle během běhu programů ignorují a využívají se pouze kontrolními systémy typů). V podstatě jsme plánovali převést implementaci mypy z Pythonu do jazyka, který byl staticky typovaný, a který by vypadal (a většinou fungoval) přesně jako Python. (Tento typ mezijazykové migrace se stal jakousi tradicí projektu mypy. Původní implementace mypy byla napsána v Alore, poté byl syntaktický hybrid Javy a Pythonu).

Orientace na API rozšíření CPython byla klíčem k tomu, abychom neztratili příležitosti v oblasti řízení projektů. Neminulo nás potřeba implementovat virtuální stroj nebo jakékoli knihovny, které by mypy potřebovalo. Navíc by nám stále byla přístupná celá ekosystém Pythonu, byly by dostupné všechny nástroje (jako je pytest). To znamenalo, že bychom mohli pokračovat v používání interpretovaného kódu Pythonu během vývoje, což by nám umožnilo nadále pracovat s velmi rychlou metodou úprav kódu a jeho testování, místo abychom čekali na kompilaci kódu. Vypadalo to, že se nám skvěle daří, jak se říká, sedět na dvou židlích, a to se nám líbilo.

Komplátor, který jsme nazvali mypyc (protože jako frontend používá mypy pro analýzu typů), se ukázal jako velmi úspěšný projekt. Celkově jsme dosáhli přibližně čtyřnásobného zrychlení častých spuštění mypy bez použití kešování. Vývoj jádra projektu mypyc zabral malému týmu, do kterého patřili Michael Sullivan, Ivan Levkyvskyi, Hugh Han a já, přibližně 4 kalendářní měsíce. Tento objem práce byl mnohem menší, než by byl potřebný pro přepsání mypy například do C++ nebo Go. A také jsme museli provést mnohem méně změn v projektu, než kolik by bylo potřeba při přepisování do jiného jazyka. Doufali jsme také, že dostaneme mypyc na úroveň, na které by ho mohli používat i jiní programátoři z Dropboxu k tomu, aby kompilovali a zrychlovali svůj kód.

Abychom dosáhli takové úrovně výkonu, museli jsme aplikovat některá zajímavá inženýrská řešení. Komplátor může urychlit vykonávání mnoha operací díky použití rychlých nízkoúrovňových konstrukcí C. Například volání zkompilované funkce se překládá na volání C funkce. Takové volání se vykonává mnohem rychleji než volání interpretované funkce. Některé operace, jako například vyhledávání ve slovnících, stále spočívaly v běžném volání C-API z CPython, které po kompilaci bylo jen o něco rychlejší. Dokázali jsme se zbavit dodatečné zátěže na systém, kterou vytvářela interpretace, ale to v tomto případě přineslo jen malé výhody v oblasti výkonu.

Pro identifikaci nejběžnějších "pomalých" operací jsme provedli profilování kódu. Armed with the obtained data, we tried either to tweak mypyc so that it would generate faster C code for such operations, or rewrite the corresponding Python code using faster operations (sometimes we simply didn't have a straightforward solution for a specific problem). Rewriting Python code often proved to be an easier solution to the problem than implementing the same transformation automatically in the compiler. In the long term, we wanted to automate many of these transformations, but at that moment we were focused on speeding up mypy with minimal effort. And in pursuit of this goal, we cut a few corners.

Pokračování příště…

Vážení čtenáři! Jaké dojmy ve vás vyvolal projekt mypy, když jste se o něm dozvěděli?

Cesta k ověřování typů 4 milionů řádků kódu v Pythonu. Část 2
Cesta k ověřování typů 4 milionů řádků kódu v Pythonu. Část 2

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster