Put do provjere tipa 4 milijuna linija Python koda. 2. dio

Danas objavljujemo drugi dio prijevoda materijala o tome kako je Dropbox organizirao kontrolu tipa za nekoliko milijuna linija Python koda.

Put do provjere tipa 4 milijuna linija Python koda. 2. dio

Pročitajte prvi dio

Službena podrška tipa (PEP 484)

Svoje smo prve ozbiljne eksperimente s mypyjem proveli u Dropboxu tijekom Hack Weeka 2014. Hack Week je jednotjedni događaj čiji je domaćin Dropbox. Za to vrijeme zaposlenici mogu raditi što god žele! Neki od Dropboxovih najpoznatijih tehnoloških projekata započeli su na ovakvim događajima. Kao rezultat ovog eksperimenta zaključili smo da mypy izgleda obećavajuće, iako projekt još nije spreman za široku upotrebu.

U to je vrijeme ideja o standardizaciji sustava hintiranja tipa Python bila u zraku. Kao što sam rekao, od Pythona 3.0 bilo je moguće koristiti oznake tipa za funkcije, ali to su bili samo proizvoljni izrazi, bez definirane sintakse i semantike. Tijekom izvođenja programa, te su napomene, većinom, jednostavno ignorirane. Nakon Hack Weeka, počeli smo raditi na standardizaciji semantike. Ovaj rad doveo je do nastanka PEP 484 (Guido van Rossum, Łukasz Langa i ja surađivali smo na ovom dokumentu).

Naši motivi mogu se promatrati s dvije strane. Prvo, nadali smo se da bi cijeli ekosustav Pythona mogao usvojiti zajednički pristup korištenju tipskih naznaka (izraz koji se u Pythonu koristi kao ekvivalent za "oznake tipa"). To bi, s obzirom na moguće rizike, bilo bolje od korištenja mnogih međusobno nekompatibilnih pristupa. Drugo, htjeli smo otvoreno razgovarati o mehanizmima označavanja tipa s mnogim članovima Python zajednice. Ta je želja dijelom bila diktirana činjenicom da ne bismo željeli izgledati kao “otpadnici” od temeljnih ideja jezika u očima širokih masa Python programera. To je jezik s dinamičkim tipkama, poznat kao "duck typing". U zajednici se na samom početku pojavio pomalo sumnjičav stav prema ideji statičkog tipkanja. Ali taj je osjećaj na kraju splasnuo nakon što je postalo jasno da statično tipkanje neće biti obavezno (i nakon što su ljudi shvatili da je zapravo korisno).

Sintaksa savjeta tipa koja je na kraju usvojena bila je vrlo slična onoj koju je mypy podržavao u to vrijeme. PEP 484 objavljen je s Pythonom 3.5 2015. Python više nije bio dinamički tipiziran jezik. Volim razmišljati o ovom događaju kao o značajnoj prekretnici u povijesti Pythona.

Početak migracije

Krajem 2015. Dropbox je stvorio tim od troje ljudi za rad na mypyju. Među njima su bili Guido van Rossum, Greg Price i David Fisher. Od tog trenutka situacija se počela razvijati izuzetno brzo. Prva prepreka mypyjevom rastu bila je izvedba. Kao što sam nagovijestio gore, u ranim danima projekta razmišljao sam o prevođenju mypy implementacije u C, ali ta je ideja za sada prekrižena s popisa. Zapeli smo s pokretanjem sustava pomoću CPython interpretera, koji nije dovoljno brz za alate poput mypyja. (Projekt PyPy, alternativna implementacija Pythona s JIT kompajlerom, također nam nije pomogao.)

Srećom, ovdje su nam u pomoć priskočila neka algoritamska poboljšanja. Prvi moćni "akcelerator" bila je implementacija inkrementalne provjere. Ideja iza ovog poboljšanja bila je jednostavna: ako se sve ovisnosti modula nisu promijenile od prethodnog pokretanja mypyja, tada možemo koristiti podatke predmemorirane tijekom prethodnog pokretanja dok radimo s ovisnostima. Trebali smo samo izvršiti provjeru tipa na izmijenjenim datotekama i na datotekama koje ovise o njima. Mypy je čak otišao malo dalje: ako se vanjsko sučelje modula nije promijenilo, mypy je pretpostavio da druge module koji su uvezli ovaj modul ne treba ponovno provjeravati.

Inkrementalna provjera puno nam je pomogla pri označavanju velikih količina postojećeg koda. Poanta je u tome da ovaj proces obično uključuje mnogo iterativnih pokretanja mypyja jer se komentari postupno dodaju kodu i postupno poboljšavaju. Prvo pokretanje mypyja još uvijek je bilo vrlo sporo jer je trebalo provjeriti puno ovisnosti. Zatim smo, kako bismo poboljšali situaciju, implementirali mehanizam daljinskog predmemoriranja. Ako mypy otkrije da je lokalna predmemorija vjerojatno zastarjela, preuzima trenutnu snimku predmemorije za cijelu bazu kodova iz centraliziranog repozitorija. Zatim izvodi inkrementalnu provjeru koristeći ovu snimku. Ovo nas je učinilo još jednim velikim korakom prema povećanju performansi mypyja.

Bilo je to razdoblje brzog i prirodnog usvajanja provjere tipa u Dropboxu. Do kraja 2016. već smo imali otprilike 420000 XNUMX redaka Python koda s oznakama tipa. Mnogi korisnici bili su oduševljeni provjerom tipa. Sve više i više razvojnih timova koristilo je Dropbox mypy.

Tada je sve izgledalo dobro, ali imali smo još puno toga za napraviti. Počeli smo provoditi periodične interne ankete korisnika kako bismo identificirali problematična područja projekta i shvatili koje probleme treba prvo riješiti (ova praksa se i danas koristi u tvrtki). Najvažnija su, kako je postalo jasno, dva zadatka. Prvo, trebalo nam je veće pokrivanje tipova koda, drugo, trebao nam je mypy da radi brže. Bilo je potpuno jasno da je naš rad na ubrzanju mypyja i njegovoj implementaciji u projekte tvrtke još daleko od završetka. Mi smo, potpuno svjesni važnosti ova dva zadatka, krenuli u njihovo rješavanje.

Više produktivnosti!

Inkrementalne provjere učinile su mypy bržim, ali alat još uvijek nije bio dovoljno brz. Mnoge inkrementalne provjere trajale su oko minutu. Razlog tome bio je ciklički uvoz. Ovo vjerojatno neće iznenaditi nikoga tko je radio s velikim bazama kodova napisanim u Pythonu. Imali smo setove od stotina modula, od kojih je svaki neizravno uvozio sve ostale. Ako je bilo koja datoteka u petlji uvoza promijenjena, mypy je morao obraditi sve datoteke u toj petlji, a često i sve module koji su uvezli module iz te petlje. Jedan takav ciklus bio je zloglasni "klupko ovisnosti" koji je uzrokovao mnogo problema u Dropboxu. Nekada je ova struktura sadržavala nekoliko stotina modula, dok je bila uvezena, izravno ili neizravno, mnogi testovi, također je korištena u proizvodnom kodu.

Razmatrali smo mogućnost "razmrsivanja" kružnih ovisnosti, ali nismo imali resurse za to. Bilo je previše koda s kojim nismo bili upoznati. Kao rezultat toga, došli smo do alternativnog pristupa. Odlučili smo natjerati mypy da radi brzo čak i u prisustvu "zapleta ovisnosti". Taj smo cilj postigli pomoću mypy demona. Daemon je poslužiteljski proces koji implementira dvije zanimljive mogućnosti. Prvo, pohranjuje informacije o cijeloj bazi koda u memoriju. To znači da svaki put kada pokrenete mypy, ne morate učitavati predmemorirane podatke koji se odnose na tisuće uvezenih ovisnosti. Drugo, pažljivo, na razini malih strukturnih jedinica, analizira ovisnosti između funkcija i drugih cjelina. Na primjer, ako funkcija foo poziva funkciju bar, tada postoji ovisnost foo iz bar. Kada se datoteka promijeni, demon prvo, u izolaciji, obrađuje samo promijenjenu datoteku. Zatim gleda vanjske vidljive promjene te datoteke, kao što su promijenjeni potpisi funkcija. Daemon koristi detaljne informacije o uvozima samo za dvostruku provjeru onih funkcija koje stvarno koriste modificiranu funkciju. Obično, s ovim pristupom, morate provjeriti vrlo malo funkcija.

Implementacija svega ovoga nije bila laka, budući da je izvorna implementacija mypyja bila snažno fokusirana na obradu jedne po jedne datoteke. Morali smo se suočiti s mnogim graničnim situacijama, čija je pojava zahtijevala ponovljene provjere u slučajevima kada se nešto promijenilo u kodu. Na primjer, to se događa kada se klasi dodijeli nova osnovna klasa. Nakon što smo učinili što smo htjeli, uspjeli smo smanjiti vrijeme izvršenja većine inkrementalnih provjera na samo nekoliko sekundi. Ovo nam se činilo kao velika pobjeda.

Još više produktivnosti!

Zajedno s udaljenim predmemoriranjem o kojem sam gore govorio, mypy demon je gotovo u potpunosti riješio probleme koji nastaju kada programer često pokreće provjeru tipa, mijenjajući mali broj datoteka. Međutim, performanse sustava u najnepovoljnijem slučaju upotrebe još uvijek su bile daleko od optimalnih. Čisto pokretanje mypyja moglo bi potrajati više od 15 minuta. A ovo je bilo puno više nego što bismo bili zadovoljni. Svaki tjedan situacija je postajala gora jer su programeri nastavili pisati novi kod i dodavati bilješke postojećem kodu. Naši su korisnici i dalje bili gladni više performansi, ali smo bili sretni što smo im izašli u susret na pola puta.

Odlučili smo se vratiti na jednu od ranijih ideja vezanih uz mypy. Naime, za pretvaranje Python koda u C kod. Eksperimentiranje s Cythonom (sustav koji vam omogućuje prevođenje koda napisanog u Pythonu u C kod) nije nam dalo nikakvo vidljivo ubrzanje, pa smo odlučili oživjeti ideju o pisanju vlastitog prevoditelja. Budući da baza koda mypy (napisana u Pythonu) već sadrži sve potrebne bilješke tipa, smatrali smo da bi se isplatilo pokušati upotrijebiti te bilješke za ubrzanje sustava. Brzo sam napravio prototip da testiram ovu ideju. Pokazao je više od 10 puta povećanje performansi na različitim mikro-benchmarkovima. Naša je ideja bila kompajlirati Python module u C module pomoću Cythona i pretvoriti tipske komentare u provjere tipa tijekom izvođenja (obično se tipske komentare ignoriraju tijekom izvođenja i koriste samo sustavi za provjeru tipa). Zapravo smo planirali prevesti mypy implementaciju iz Pythona u jezik koji je dizajniran da se statički tipka, koji bi izgledao (i, većinom, radio) točno kao Python. (Ova vrsta međujezične migracije postala je nešto poput tradicije projekta mypy. Izvorna implementacija mypyja napisana je u Aloreu, zatim je postojao sintaktički hibrid Jave i Pythona).

Fokusiranje na API proširenja CPython bilo je ključno da se ne izgube mogućnosti upravljanja projektima. Nismo trebali implementirati virtualni stroj ili bilo koje biblioteke koje je mypy trebao. Osim toga, i dalje bismo imali pristup cijelom Python ekosustavu i svim alatima (kao što je pytest). To je značilo da smo mogli nastaviti koristiti interpretirani Python kod tijekom razvoja, što nam je omogućilo da nastavimo raditi s vrlo brzim uzorkom izmjene koda i njegovog testiranja, umjesto da čekamo da se kod kompajlira. Izgledalo je kao da radimo sjajan posao sjedeći na dvije stolice, da tako kažem, i svidjelo nam se.

Kompajler, koji smo nazvali mypyc (jer koristi mypy kao front-end za analizu tipova), pokazao se kao vrlo uspješan projekt. Sve u svemu, postigli smo približno 4x ubrzanje za česta pokretanja mypy bez predmemoriranja. Za razvoj jezgre mypyc projekta malom timu Michaela Sullivana, Ivana Levkivskog, Hugha Hahna i mene trebalo je oko 4 kalendarska mjeseca. Ova količina posla bila je puno manja od one koja bi bila potrebna za ponovno pisanje mypyja, na primjer, u C++ ili Go. I morali smo napraviti mnogo manje izmjena u projektu nego što bismo morali napraviti kada smo ga prepisivali na drugom jeziku. Također smo se nadali da možemo dovesti mypyc na takvu razinu da ga drugi programeri Dropboxa mogu koristiti za kompajliranje i ubrzanje svog koda.

Da bismo postigli ovu razinu performansi, morali smo primijeniti neka zanimljiva inženjerska rješenja. Stoga prevoditelj može ubrzati mnoge operacije korištenjem brzih konstrukcija niske razine C. Na primjer, prevedeni poziv funkcije prevodi se u poziv funkcije C. A takav je poziv mnogo brži od pozivanja interpretirane funkcije. Neke operacije, kao što je traženje rječnika, i dalje su uključivale korištenje uobičajenih C-API poziva iz CPythona, koji su bili samo neznatno brži kada su kompilirani. Uspjeli smo eliminirati dodatno opterećenje sustava stvoreno tumačenjem, ali to je u ovom slučaju dalo samo mali dobitak u pogledu performansi.

Kako bismo identificirali najčešće "spore" operacije, izvršili smo profiliranje koda. Naoružani ovim podacima, pokušali smo ili podesiti mypyc tako da generira brži C kod za takve operacije, ili prepisati odgovarajući Python kod koristeći brže operacije (i ponekad jednostavno nismo imali dovoljno jednostavno rješenje za taj ili drugi problem) . Ponovno pisanje Python koda često je bilo lakše rješenje problema nego da kompilator automatski izvrši istu transformaciju. Dugoročno, željeli smo automatizirati mnoge od tih transformacija, ali u to smo vrijeme bili usredotočeni na ubrzanje mypyja uz minimalan napor. I u kretanju prema ovom cilju, presjekli smo nekoliko uglova.

Da bi se nastavio ...

Dragi čitatelji! Kakvi su bili vaši dojmovi o mypy projektu kada ste saznali za njegovo postojanje?

Put do provjere tipa 4 milijuna linija Python koda. 2. dio
Put do provjere tipa 4 milijuna linija Python koda. 2. dio

Izvor: www.habr.com

Dodajte komentar