Put do provjere tipa 4 miliona linija Python koda. Dio 2

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

Put do provjere tipa 4 miliona linija Python koda. Dio 2

Pročitaj prvi dio

Službena podrška tipa (PEP 484)

Svoje prve ozbiljne eksperimente sa mypy-jem izveli smo na Dropboxu tokom Hack Week-a 2014. Hack Week je jednonedeljni događaj čiji je domaćin Dropbox. Za to vrijeme zaposleni mogu raditi šta god žele! Neki od najpoznatijih Dropbox-ovih tehnoloških projekata započeli su na ovakvim događajima. Kao rezultat ovog eksperimenta, zaključili smo da mypy izgleda obećavajuće, iako projekat još nije spreman za široku upotrebu.

U to vrijeme, ideja o standardizaciji Python tipa hinting sistema je 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. Tokom izvršavanja programa, ove napomene su, uglavnom, jednostavno ignorisane. Nakon Hack Week-a, počeli smo raditi na standardizaciji semantike. Ovaj rad je doveo do pojave PEP 484 (Guido van Rossum, Łukasz Langa i ja smo sarađivali na ovom dokumentu).

Naši motivi se mogu posmatrati sa dve strane. Prvo, nadali smo se da bi ceo Python ekosistem mogao usvojiti zajednički pristup korišćenju nagoveštaja tipa (termin koji se koristi u Pythonu kao ekvivalent "napomenama tipa"). Ovo bi, s obzirom na moguće rizike, bilo bolje od korištenja mnogih međusobno nekompatibilnih pristupa. Drugo, željeli smo otvoreno razgovarati o mehanizmima označavanja tipova sa mnogim članovima Python zajednice. Ova želja je dijelom bila diktirana činjenicom da ne bismo željeli izgledati kao „otpadnici“ od osnovnih ideja jezika u očima širokih masa Python programera. To je dinamički kucani jezik, poznat kao "pačje kucanje". U zajednici, na samom početku, nije mogao a da se ne pojavi pomalo sumnjičav odnos prema ideji statičkog kucanja. Ali taj osjećaj je na kraju splasnuo nakon što je postalo jasno da statičko kucanje neće biti obavezno (i nakon što su ljudi shvatili da je zapravo korisno).

Sintaksa tipskih nagoveštaja koja je na kraju usvojena bila je vrlo slična onoj koju je mypy podržavao u to vreme. PEP 484 je objavljen sa Pythonom 3.5 2015. godine. Python više nije bio dinamički kucani jezik. Volim da mislim o ovom događaju kao o značajnoj prekretnici u istoriji Pythona.

Početak migracije

Krajem 2015. Dropbox je stvorio tim od tri osobe 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 rastu mypy-ja bile su performanse. Kao što sam gore nagovijestio, u ranim danima projekta razmišljao sam o prevođenju implementacije mypy u C, ali ova ideja je za sada precrtana sa liste. Zaglavili smo sa pokretanjem sistema koristeći CPython interpreter, koji nije dovoljno brz za alate kao što je mypy. (Nije nam pomogao ni PyPy projekat, alternativna Python implementacija sa JIT kompajlerom.)

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 zavisnosti modula nisu promijenile od prethodnog pokretanja mypy-ja, tada možemo koristiti podatke keširane tokom prethodnog pokretanja dok radimo sa ovisnostima. Trebali smo samo izvršiti provjeru tipa na modificiranim datotekama i na datotekama koje su zavise od njih. Mypy je čak otišao malo dalje: ako se eksterni interfejs modula nije promenio, mypy je pretpostavio da drugi moduli koji su uvezli ovaj modul ne moraju ponovo da se proveravaju.

Inkrementalna provjera nam je puno pomogla pri označavanju velikih količina postojećeg koda. Poenta je u tome da ovaj proces obično uključuje mnogo iterativnih pokretanja mypy-ja jer se napomene postepeno dodaju kodu i postepeno poboljšavaju. Prvo pokretanje mypy-ja je još uvijek bilo vrlo sporo jer je trebalo provjeriti mnogo zavisnosti. Zatim, da poboljšamo situaciju, implementirali smo mehanizam za daljinsko keširanje. Ako mypy otkrije da je lokalna keš memorija vjerovatno zastarjela, preuzima trenutni snimak predmemorije za cijelu kodnu bazu iz centraliziranog spremišta. Zatim izvodi inkrementalnu provjeru koristeći ovaj snimak. Ovo nas je dovelo do još jednog velikog koraka ka povećanju performansi mypy-ja.

Ovo je bio period brzog i prirodnog usvajanja provjere tipa u Dropboxu. Do kraja 2016. već smo imali otprilike 420000 linija Python koda s napomenama tipa. Mnogi korisnici su bili oduševljeni provjerom tipa. Sve više razvojnih timova koristi Dropbox mypy.

Sve je tada izgledalo dobro, ali smo morali još mnogo toga da uradimo. Počeli smo da provodimo periodične interne ankete korisnika kako bismo identifikovali problematična područja projekta i shvatili koja pitanja prvo treba riješiti (ova praksa se i danas koristi u kompaniji). Najvažnija su, kako je postalo jasno, dva zadatka. Prvo, trebalo nam je više pokrića tipa koda, drugo, trebao nam je mypy da radi brže. Bilo je potpuno jasno da je naš rad na ubrzanju mypyja i implementaciji u projekte kompanije još uvijek daleko od završetka. Mi smo, potpuno svjesni važnosti ova dva zadatka, krenuli u njihovo rješavanje.

Više produktivnosti!

Inkrementalne provjere su učinile mypy bržim, ali alat još uvijek nije bio dovoljno brz. Mnoge inkrementalne provjere trajale su oko minut. Razlog za to je bio ciklični uvoz. Ovo vjerovatno neće iznenaditi nikoga ko je radio sa velikim kodnim bazama napisanim u Pythonu. Imali smo setove od stotine modula, od kojih je svaki indirektno uvozio sve ostale. Ako je bilo koja datoteka u uvoznoj petlji 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 "zamršen problem" koji je izazvao mnogo problema u Dropboxu. Nekada je ova struktura sadržavala nekoliko stotina modula, dok je uvezeno, direktno ili indirektno, mnogo testova, korišćena je i u proizvodnom kodu.

Razmatrali smo mogućnost "raspetljavanja" kružnih zavisnosti, ali nismo imali resurse za to. Bilo je previše koda sa kojima nismo bili upoznati. Kao rezultat toga, došli smo do alternativnog pristupa. Odlučili smo da učinimo da mypy brzo radi čak i u prisustvu „zamršenosti zavisnosti“. Ovaj cilj smo postigli koristeći mypy demon. Daemon je serverski proces koji implementira dvije zanimljive karakteristike. Prvo, pohranjuje informacije o cijeloj bazi koda u memoriji. To znači da svaki put kada pokrenete mypy, ne morate učitavati keširane podatke koji se odnose na hiljade uvezenih zavisnosti. Drugo, on pažljivo, na nivou malih strukturnih jedinica, analizira zavisnosti između funkcija i drugih entiteta. Na primjer, ako je funkcija foo poziva funkciju bar, onda postoji zavisnost 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 uvozu samo da bi dvaput provjerio one funkcije koje zapravo koriste modificiranu funkciju. Obično, s ovim pristupom, morate provjeriti vrlo malo funkcija.

Implementacija svega ovoga nije bila laka, budući da je originalna implementacija mypyja bila jako fokusirana na obradu jednog po jednog fajla. Morali smo se nositi s mnogim graničnim situacijama, čija je pojava zahtijevala ponovljene provjere u slučajevima kada se nešto promijenilo u kodu. Na primjer, ovo se dešava kada se klasi dodijeli nova osnovna klasa. Kada smo uradili ono što smo želeli, mogli smo da smanjimo vreme izvršenja većine inkrementalnih provera na samo nekoliko sekundi. Ovo nam se činilo kao velika pobeda.

Još veća produktivnost!

Zajedno sa daljinskim keširanjem o kojem sam gore govorio, mypy daemon je gotovo u potpunosti riješio probleme koji nastaju kada programer često pokreće provjeru tipa, praveći izmjene na malom broju datoteka. Međutim, performanse sistema u najnepovoljnijem slučaju još uvijek su bile daleko od optimalnih. Čisto pokretanje mypyja moglo bi potrajati više od 15 minuta. A ovo je bilo mnogo više nego što bismo bili zadovoljni. Svake sedmice situacija se pogoršavala jer su programeri nastavili da pišu novi kod i dodaju napomene postojećem kodu. Naši korisnici su i dalje bili gladni više performansi, ali mi smo bili sretni što smo ih dočekali na pola puta.

Odlučili smo da se vratimo na jednu od ranijih ideja u vezi s mypy. Naime, pretvoriti Python kod u C kod. Eksperimentisanje sa Cythonom (sistemom koji vam omogućava da prevedete kod napisan u Pythonu u C kod) nije nam dalo nikakvo vidljivo ubrzanje, pa smo odlučili da oživimo ideju ​​pisanja sopstvenog kompajlera. Budući da je mypy kodna baza (napisana u Pythonu) već sadržavala sve potrebne napomene tipa, smatrali smo da bi bilo vrijedno pokušati koristiti ove napomene da ubrzamo sistem. 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 ideja je bila da kompajliramo Python module u C module koristeći Cython, i da pretvorimo napomene tipa u provjere tipa u vrijeme izvođenja (obično se napomene tipa zanemaruju u vrijeme izvođenja i koriste ih samo sistemi za provjeru tipa). Zapravo smo planirali da prevedemo implementaciju mypy sa Pythona u jezik koji je dizajniran da bude statički kucan, koji bi izgledao (i, uglavnom, radio) baš kao Python. (Ova vrsta međujezičke migracije postala je nešto kao tradicija mypy projekta. Originalna implementacija mypyja napisana je u Aloreu, zatim je postojao sintaktički hibrid Jave i Python-a).

Fokusiranje na API proširenja CPython bilo je ključno da se ne izgube sposobnosti upravljanja projektima. Nismo morali da implementiramo virtuelnu mašinu ili bilo koje biblioteke koje su bile potrebne mypyju. Osim toga, i dalje bismo imali pristup cijelom Python ekosistemu i svim alatima (kao što je pytest). To je značilo da možemo nastaviti da koristimo interpretirani Python kod tokom razvoja, omogućavajući nam da nastavimo raditi s vrlo brzim obrascem unošenja izmjena koda i njegovog testiranja, umjesto da čekamo da se kod kompajlira. Izgledalo je kao da radimo odličan posao sedeći na dve stolice, da tako kažem, i to nam se svidelo.

Kompajler, koji smo nazvali mypyc (pošto koristi mypy kao front-end za analizu tipova), pokazao se vrlo uspješnim projektom. Sve u svemu, postigli smo približno 4x ubrzanje za česta izvođenja mypy bez keširanja. Za razvoj jezgra mypyc projekta potrebno je malom timu Michaela Sullivana, Ivana Levkivskog, Hugha Hahna i mene oko 4 kalendarska mjeseca. Ova količina posla bila je mnogo manja od onoga što bi bilo potrebno da se prepiše mypy, na primjer, u C++ ili Go. I morali smo da napravimo mnogo manje promena u projektu nego što bismo morali da unesemo kada smo ga prepisali na drugom jeziku. Također smo se nadali da možemo dovesti mypyc na takav nivo da bi ga drugi Dropbox programeri mogli koristiti za kompajliranje i ubrzanje svog koda.

Da bismo postigli ovaj nivo performansi, morali smo primijeniti neka zanimljiva inženjerska rješenja. Dakle, kompajler može ubrzati mnoge operacije koristeći brze konstrukcije niskog nivoa C. Na primjer, prevedeni poziv funkcije se prevodi u poziv funkcije C. A takav poziv je mnogo brži od pozivanja interpretirane funkcije. Neke operacije, poput pretraživanja rječnika, i dalje su uključivale korištenje redovnih C-API poziva iz CPython-a, koji su bili samo neznatno brži kada su kompajlirani. Uspjeli smo eliminirati dodatno opterećenje sistema stvoreno interpretacijom, ali je to u ovom slučaju dalo samo mali dobitak u pogledu performansi.

Da 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 (a ponekad jednostavno nismo imali dovoljno jednostavno rješenje za taj ili neki drugi problem) . Ponovno pisanje Python koda je često bilo lakše rješenje problema nego da kompajler automatski izvrši istu transformaciju. Dugoročno, željeli smo automatizirati mnoge od ovih transformacija, ali u to vrijeme smo bili fokusirani na ubrzavanje mypy-ja uz minimalan napor. I krećući se ka ovom cilju, srezali smo nekoliko uglova.

Da se nastavi ...

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

Put do provjere tipa 4 miliona linija Python koda. Dio 2
Put do provjere tipa 4 miliona linija Python koda. Dio 2

izvor: www.habr.com

Dodajte komentar