I dag publiserer vi den andre delen av oversettelsen av materialet om hvordan Dropbox organiserte typetilgang for flere millioner linjer med Python-kode.
→
Offisiell støtte for typer (PEP 484)
Vi gjennomførte de første alvorlige eksperimentene med mypy i Dropbox under Hack Week 2014. Hack Week er et arrangement som Dropbox holder i løpet av en uke. I løpet av denne tiden kan ansatte jobbe med hva som helst! Noen av de mest kjente teknologiprosjektene i Dropbox startet nettopp under slike arrangementer. Som et resultat av dette eksperimentet konkluderte vi med at mypy virker lovende, selv om dette prosjektet ikke var klart for bred bruk.
På den tiden sirkulerte ideen om å standardisere systemer for typehinting i Python. Som jeg allerede har nevnt, fra og med Python 3.0 kunne man bruke typeannotasjoner for funksjoner, men disse var bare tilfeldige uttrykk, uten spesifikk syntaks og semantikk. Under kjøring av programmet ble disse annotasjonene stort sett ignorert. Etter Hack Week begynte vi arbeidet med å standardisere semantikken. Dette arbeidet ledet til fremveksten av (dette dokumentet ble utviklet i samarbeid mellom Guido van Rossum, Łukasz Langa og meg).
Våre motiver kan sees fra to sider. For det første håpet vi at hele Python-økosystemet ville kunne adoptere en felles tilnærming til bruk av typehinting. Dette ville, med tanke på mulige risikoer, vært bedre enn å bruke mange gjensidig uforenlige metoder. For det andre ønsket vi å diskutere mekanismene for typeannotasjon åpent med mange representanter fra Python-samfunnet. Dels var dette ønsket drevet av at vi ikke ønsket å fremstå som "fravikere" fra de grunnleggende ideene i språket i øynene til et bredt publikum av Python-utviklere. Dette er et dynamisk typet språk kjent for sin "anda-typet". I samfunnet, i starten, kunne man ikke unngå en viss mistro til ideen om statisk typisering. Men denne holdningen ble til slutt svekket — etter at det ble klart at statisk typisering ikke var ment å bli obligatorisk (og etter at folk forsto at den faktisk er nyttig).
Det vedtatte syntaksen for type hinting lignet veldig på det som mypy støtte på den tiden. PEP 484-dokumentet ble lansert sammen med Python 3.5 i 2015. Python var ikke lenger et språk som kun støttet dynamisk typet i å følge opp. Jeg liker å tenke på dette som en betydelig milepæl i Python-historien.
Starten på migrasjonen
På slutten av 2015 ble det opprettet et team på tre personer i Dropbox for å arbeide med mypy. Teamet besto av Guido van Rossum, Greg Price og David Fisher. Fra dette punktet begynte situasjonen å utvikle seg ekstremt raskt. Det første hinderet for veksten av mypy var ytelsen. Som jeg antydet tidligere, vurderte jeg i den tidlige utviklingen av prosjektet å oversette implementeringen av mypy til C, men denne ideen ble midlertidig strøket fra listen. Vi var låst til at systemet ble kjørt med CPython-tolkeren, som ikke hadde hastigheten som var tilstrekkelig for verktøy som mypy. (PyPy-prosjektet, en alternativ implementering av Python med JIT-kompilator, hjalp oss heller ikke.)
Heldigvis fikk vi hjelp av noen algoritmiske forbedringer. Den første kraftige ‘akseleratoren’ var implementeringen av inkrementell sjekking. Ideen bak denne forbedringen var enkel: hvis alle avhengighetene til modulet har vært uendret siden forrige kjøring av mypy, så kan vi bruke data som ble hurtigbufret under forrige økt når vi jobber med avhengighetene. Vi måtte bare utføre typekontroll på endrede filer og de filene som avhang av dem. Mypy gikk til og med et skritt videre: hvis det eksterne grensesnittet til modulen ikke hadde endret seg — antok mypy at andre moduler som importerte denne modulen, ikke måtte kontrolleres på nytt.
Inkrementell kontroll har hjulpet oss mye med å annotere store mengder eksisterende kode. Prosessen innebærer vanligvis mange iterasjoner av mypy, ettersom annotasjonene legges til gradvis og forbedres over tid. Den første kjøringen av mypy var fortsatt veldig treg, da den måtte verifisere et stort antall avhengigheter. For å forbedre situasjonen, implementerte vi en mekanisme for fjernlagring. Hvis mypy oppdager at den lokale cachen sannsynligvis er foreldet, laster den opp et nåværende snapshot av cachen for hele kodebasen fra et sentralisert repository. Deretter utfører den, med dette snapshotet, den inkrementelle kontrollen. Dette har vært et betydelig steg fremover for å øke ytelsen til mypy.
Dette var en periode med rask og naturlig implementering av typesystemet i Dropbox. Ved slutten av 2016 hadde vi allerede nærmere 420000 linjer med Python-kode med typeannotasjoner. Mange brukere mottok typesjekken med entusiasme. Mypy ble brukt av stadig flere utviklerteam i Dropbox.
Alt så bra ut da, men vi hadde fortsatt mye arbeid foran oss. Vi begynte å gjennomføre periodiske interne undersøkelser blandt brukerne for å identifisere problemområder i prosjektet og for å forstå hvilke spørsmål som måtte løses først (denne praksisen brukes fortsatt i selskapet i dag). De to mest kritiske oppgavene, som ble klare, var at vi trengte bedre dekningsgrad av koden med typer, og at mypy måtte fungere raskere. Det var helt klart at arbeidet vårt med å forbedre hastigheten på mypy og implementeringen i selskapets prosjekter fortsatt var langt fra fullført. Med full forståelse for viktigheten av disse to oppgavene, satte vi i gang med å løse dem.
Mer ytelse!
Inkrementelle sjekker har akselerert mypy, men dette verktøyet har fortsatt ikke vært raskt nok. Mange inkrementelle sjekker varte i omtrent ett minutt. Årsaken til dette var sirkulære importeringer. Dette vil sannsynligvis ikke overraske dem som har jobbet med store kodebaser skrevet i Python. Vi hadde sett med hundrevis av moduler, hver av dem importerte indirekte alle de andre. Hvis noen filer i importsyklusen ble endret, måtte mypy behandle alle filer som inngikk i denne syklusen, og ofte også alle moduler som importerer moduler fra denne syklusen. En av disse syklusene var den beryktede 'avhengighetsknuten', som forårsaket mange problemer i Dropbox. En gang inneholdt denne strukturen flere hundre moduler, og den ble importert, enten direkte eller indirekte, av mange tester, og den ble brukt i produksjonskoden.
Vi vurderte muligheten for å 'løse opp' de sirkulære avhengighetene, men vi hadde ikke ressurser til å gjøre det. Det var for mye kode vi ikke var kjent med. Til slutt kom vi til en alternativ tilnærming. Vi bestemte oss for å få mypy til å fungere raskt selv med 'avhengighetsknuter'. Vi oppnådde dette ved hjelp av mypy-demonen. Demonen er en serverprosess som implementerer to interessante funksjoner. For det første lagrer den informasjon om hele kodebasen i minnet. Dette betyr at ved hver start av mypy trenger vi ikke å laste inn cached data relatert til tusenvis av importerte avhengigheter. For det andre analyserer den nøye, på nivået av små strukturenheter, avhengighetene mellom funksjoner og andre entiteter. For eksempel, hvis funksjonen foo kaller funksjonen bar, så er det en avhengighet foo fra bar. Når en fil endres, behandler demon først, isolert, bare den endrede filen. Deretter ser den på endringene i denne filen, synlige utenfra, som endrede funksjonssignaturer. Demonen bruker detaljert informasjon om importene kun for å verifisere de funksjonene som faktisk bruker den endrede funksjonen. Vanligvis må man sjekke ganske få funksjoner med denne tilnærmingen.
Implementeringen av alt dette var ingen enkel oppgave, ettersom den opprinnelige implementeringen av mypy var sterkt fokusert på behandling av én fil om gangen. Vi måtte håndtere mange grensekaser som krevde gjentatt sjekking når noe ble endret i koden. For eksempel skjer dette når en klasse tildeles en ny baseklasse. Etter at vi hadde gjort det vi ønsket, klarte vi å redusere kjøretiden for de fleste inkrementelle sjekker til et par sekunder. Vi så på dette som en stor seier.
Enda mer ytelse!
Sammen med den eksterne hurtigbufringen jeg nevnte tidligere, løste mypy-demonstrasjonen praktisk talt problemene som oppsto når en programmerer ofte kjørte typekontroll mens de gjorde endringer i et lite antall filer. Imidlertid var systemets ytelse i det minst gunstige bruksområdet fortsatt langt fra optimal. En ren oppstart av mypy kunne ta mer enn 15 minutter. Dette var langt mer enn vi var komfortable med. Hver uke ble situasjonen verre ettersom programmerere fortsatte å skrive ny kode og legge til annotasjoner til eksisterende kode. Våre brukere lengtet fortsatt etter bedre ytelse, og vi var glade for å imøtekomme dem.
Vi har besluttet å gå tilbake til en av de tidlige ideene om mypy. Nemlig konvertering av Python-kode til C-kode. Eksperimentene med Cython (dette er et system som lar oss oversette kode skrevet i Python til C-kode) ga oss ingen merkbar hastighetsforbedring, så vi bestemte oss for å gjenopplive ideen om å skrive vår egen kompilator. Siden mypy-kodebasen (skrevet i Python) allerede hadde alle nødvendige typeannotasjoner, virket det som et verdt forsøk å bruke disse annotasjonene for å øke systemets ytelse. Jeg laget raskt en prototype for å teste denne ideen. Den viste mer enn ti ganger ytelsesforbedring i forskjellige mikro-benchmarktester. Vår idé var å kompilere Python-moduler til C-moduler ved hjelp av Cython, og å omdanne typeannotasjoner til typekontroller utført under kjøring av programmet (vanligvis blir typeannotasjoner ignorert under programkjøring og brukes bare av typekontrollsystemer). Vi planla faktisk å oversette mypy-implementeringen fra Python til et språk som var statisk typet, og som ville se ut (og for det meste fungere) akkurat som Python. (Denne typen mellomspråklig migrasjon har blitt noe av en tradisjon i mypy-prosjektet. Den opprinnelige implementeringen av mypy ble skrevet i Alore, og deretter var det en syntaktisk hybrid av Java og Python).
Fokuset på CPython-utvidelses-API-et var nøkkelen til at vi ikke mistet mulighetene for prosjektledelse. Vi trengte ikke å implementere en virtuell maskin eller noen biblioteker som mypy trengte. I tillegg ville hele Python-økosystemet fortsatt være tilgjengelig for oss, og alle verktøyene (som pytest) ville være tilgjengelige. Dette betydde at vi kunne fortsette å bruke den tolket Python-koden under utviklingen, noe som ville tillate oss å jobbe med en veldig rask tilbakemeldingssløyfe på kodeendringer og testing, i stedet for å vente på kompilering av koden. Det så ut som vi klarte å sitte på to stoler, så å si, og vi likte det.
Kompilatoren vi kalte mypyc (siden den bruker mypy til typanalyse som frontend) viste seg å være et ganske vellykket prosjekt. Generelt sett oppnådde vi omtrent en 4-dobling av hastigheten ved hyppige kjøringer av mypy uten å bruke caching. Utviklingen av kjernen i prosjektet mypyc tok en liten gruppe, bestående av Michael Sullivan, Ivan Levkivskiy, Hugh Han og meg selv, omtrent 4 måneder. Dette arbeidsomfanget var betydelig mindre enn det som ville vært nødvendig for å omskrive mypy, for eksempel til C++ eller Go. Og endringene vi måtte gjøre i prosjektet var også langt færre enn de som ville vært nødvendige ved omskrivning til et annet språk. I tillegg håpet vi å kunne utvikle mypyc til et nivå der andre programmerere hos Dropbox kunne bruke det til å kompilere og akselerere sin egen kode.
For å oppnå et slikt ytelsesnivå måtte vi anvende noen interessante ingeniørløsninger. Kompilatoren kan for eksempel akselerere utførelsen av mange operasjoner ved å bruke raske lavnivå C-konstruksjoner. For eksempel blir anropet til en kompilert funksjon oversatt til et anrop av en C-funksjon. Et slikt anrop utføres mye raskere enn anropet til en tolket funksjon. Noen operasjoner, som ordboksøk, kom likevel ned til å bruke vanlige C-API-anrop fra CPython, som etter kompilering viste seg å være bare litt raskere. Vi klarte å kvitte oss med den ekstra belastningen på systemet som ble forårsaket av tolkning, men dette ga bare en liten gevinst i ytelse.
For identifying the most common 'slow' operations, we performed code profiling. Armed with the data we obtained, we attempted either to 'tweak' mypyc so that it would generate faster C code for such operations, or to rewrite the corresponding Python code using faster operations (sometimes we simply did not have a straightforward solution for a particular problem). Rewriting the Python code often turned out to be a more straightforward solution than implementing an automatic execution of the same transformation in the compiler. In the long run, we wanted to automate many of these transformations, but at that moment we were focused on speeding up mypy with minimal effort. As we pursued this goal, we cut some corners.
Fortsettelse følger...
Kjære lesere! Hvilke inntrykk fikk du av mypy-prosjektet da du først fikk vite om det?
Kilde: habr.com
