Veien til typesjekking av 4 millioner linjer med Python-kode. Del 2

I dag publiserer vi den andre delen av oversettelsen av materiale om hvordan Dropbox organiserte typekontroll for flere millioner linjer med Python-kode.

Veien til typesjekking av 4 millioner linjer med Python-kode. Del 2

Les del én

Offisiell typestøtte (PEP 484)

Vi gjennomførte våre første seriøse eksperimenter med mypy på Dropbox under Hack Week 2014. Hack Week er et én ukes arrangement arrangert av Dropbox. I løpet av denne tiden kan ansatte jobbe med hva de vil! Noen av Dropboxs mest kjente teknologiprosjekter begynte på arrangementer som disse. Som et resultat av dette eksperimentet konkluderte vi med at mypy ser lovende ut, selv om prosjektet ennå ikke er klart for utbredt bruk.

På den tiden lå ideen om å standardisere antydningssystemer av Python-typen i luften. Som sagt, siden Python 3.0 var det mulig å bruke typekommentarer for funksjoner, men dette var bare vilkårlige uttrykk, uten definert syntaks og semantikk. Under programkjøring ble disse merknadene for det meste rett og slett ignorert. Etter Hack Week begynte vi å jobbe med standardisering av semantikk. Dette arbeidet førte til fremveksten PEP 484 (Guido van Rossum, Łukasz Langa og jeg samarbeidet om dette dokumentet).

Motivene våre kan sees fra to sider. For det første håpet vi at hele Python-økosystemet kunne ta i bruk en felles tilnærming til å bruke typehint (et begrep som brukes i Python som ekvivalent med "typemerknader"). Dette, gitt de mulige risikoene, ville være bedre enn å bruke mange gjensidig uforenlige tilnærminger. For det andre ønsket vi åpent å diskutere typekommentarmekanismer med mange medlemmer av Python-fellesskapet. Dette ønsket ble delvis diktert av det faktum at vi ikke ville se ut som "frafalne" fra de grunnleggende ideene til språket i øynene til de brede massene av Python-programmerere. Det er et dynamisk skrevet språk, kjent som "duck typing". I samfunnet, helt i begynnelsen, kunne en noe mistenksom holdning til ideen om statisk skriving ikke unngå å oppstå. Men den følelsen avtok til slutt etter at det ble klart at statisk skriving ikke kom til å være obligatorisk (og etter at folk innså at det faktisk var nyttig).

Typehintsyntaksen som til slutt ble tatt i bruk var veldig lik det mypy støttet på den tiden. PEP 484 ble utgitt med Python 3.5 i 2015. Python var ikke lenger et dynamisk skrevet språk. Jeg liker å tenke på denne hendelsen som en betydelig milepæl i Python-historien.

Start av migrasjon

På slutten av 2015 opprettet Dropbox et team på tre personer for å jobbe med mypy. De inkluderte Guido van Rossum, Greg Price og David Fisher. Fra det øyeblikket begynte situasjonen å utvikle seg ekstremt raskt. Den første hindringen for mypys vekst var ytelse. Som jeg antydet ovenfor, tenkte jeg i de første dagene av prosjektet å oversette mypy-implementeringen til C, men denne ideen ble krysset av listen foreløpig. Vi ble sittende fast med å kjøre systemet ved hjelp av CPython-tolken, som ikke er rask nok for verktøy som mypy. (PyPy-prosjektet, en alternativ Python-implementering med en JIT-kompilator, hjalp oss heller ikke.)

Heldigvis har noen algoritmiske forbedringer kommet oss til hjelp her. Den første kraftige "akseleratoren" var implementeringen av inkrementell kontroll. Ideen bak denne forbedringen var enkel: Hvis alle modulens avhengigheter ikke har endret seg siden forrige kjøring av mypy, kan vi bruke dataene som ble bufret under forrige kjøring mens vi jobber med avhengigheter. Vi trengte bare å utføre typekontroll på de endrede filene og på filene som var avhengige av dem. Mypy gikk til og med litt lenger: hvis det eksterne grensesnittet til en modul ikke endret seg, antok mypy at andre moduler som importerte denne modulen ikke trengte å sjekkes på nytt.

Inkrementell kontroll har hjulpet oss mye når vi merker store mengder eksisterende kode. Poenget er at denne prosessen vanligvis involverer mange iterative kjøringer av mypy ettersom merknader gradvis legges til koden og gradvis forbedres. Den første kjøringen av mypy var fortsatt veldig treg fordi den hadde mange avhengigheter å sjekke. Så, for å forbedre situasjonen, implementerte vi en ekstern bufringsmekanisme. Hvis mypy oppdager at den lokale hurtigbufferen sannsynligvis er utdatert, laster den ned det gjeldende hurtigbufferen for hele kodebasen fra det sentraliserte depotet. Den utfører deretter en inkrementell sjekk ved hjelp av dette øyeblikksbildet. Dette har tatt oss enda et stort skritt mot å øke ytelsen til mypy.

Dette var en periode med rask og naturlig bruk av typekontroll hos Dropbox. Ved utgangen av 2016 hadde vi allerede omtrent 420000 XNUMX linjer med Python-kode med typekommentarer. Mange brukere var begeistret for typekontroll. Flere og flere utviklingsteam brukte Dropbox mypy.

Alt så bra ut da, men vi hadde fortsatt mye å gjøre. Vi begynte å gjennomføre periodiske interne brukerundersøkelser for å identifisere problemområder i prosjektet og forstå hvilke problemer som må løses først (denne praksisen brukes fortsatt i selskapet i dag). De viktigste, som det ble klart, var to oppgaver. For det første trengte vi mer typedekning av koden, for det andre trengte vi mypy for å jobbe raskere. Det var helt klart at arbeidet vårt med å få fart på mypy og implementere det i bedriftsprosjekter fortsatt var langt fra fullført. Vi, fullt klar over viktigheten av disse to oppgavene, satte i gang med å løse dem.

Mer produktivitet!

Inkrementelle kontroller gjorde mypy raskere, men verktøyet var fortsatt ikke raskt nok. Mange inkrementelle kontroller varte i omtrent ett minutt. Årsaken til dette var syklisk import. Dette vil sannsynligvis ikke overraske noen som har jobbet med store kodebaser skrevet i Python. Vi hadde sett med hundrevis av moduler, som hver indirekte importerte alle de andre. Hvis en fil i en importløkke ble endret, måtte mypy behandle alle filene i den løkken, og ofte alle moduler som importerte moduler fra den løkken. En slik syklus var den beryktede "avhengighetsfloken" som forårsaket mye trøbbel hos Dropbox. Når denne strukturen inneholdt flere hundre moduler, mens den ble importert, direkte eller indirekte, mange tester, ble den også brukt i produksjonskode.

Vi vurderte muligheten for å «løse ut» sirkulære avhengigheter, men vi hadde ikke ressurser til å gjøre det. Det var for mye kode som vi ikke var kjent med. Som et resultat kom vi opp med en alternativ tilnærming. Vi bestemte oss for å få mypy til å fungere raskt selv i nærvær av "avhengighetsfloker". Vi oppnådde dette målet ved å bruke mypy-demonen. En daemon er en serverprosess som implementerer to interessante funksjoner. For det første lagrer den informasjon om hele kodebasen i minnet. Dette betyr at hver gang du kjører mypy, trenger du ikke å laste inn bufrede data relatert til tusenvis av importerte avhengigheter. For det andre analyserer han nøye, på nivå med små strukturelle enheter, avhengighetene mellom funksjoner og andre enheter. For eksempel hvis funksjonen foo kaller en funksjon bar, så er det en avhengighet foo fra bar. Når en fil endres, behandler daemonen først, isolert sett, bare den endrede filen. Den ser deretter på eksternt synlige endringer i den filen, for eksempel endrede funksjonssignaturer. Daemonen bruker detaljert informasjon om import bare for å dobbeltsjekke de funksjonene som faktisk bruker den modifiserte funksjonen. Vanligvis, med denne tilnærmingen, må du sjekke svært få funksjoner.

Å implementere alt dette var ikke lett, siden den opprinnelige mypy-implementeringen var sterkt fokusert på å behandle én fil om gangen. Vi måtte håndtere mange grensesituasjoner, hvis forekomst krevde gjentatte kontroller i tilfeller der noe endret seg i koden. Dette skjer for eksempel når en klasse blir tildelt en ny basisklasse. Når vi gjorde det vi ønsket, kunne vi redusere utførelsestiden for de fleste inkrementelle sjekker til bare noen få sekunder. Dette virket som en stor seier for oss.

Enda mer produktivitet!

Sammen med fjernbufringen som jeg diskuterte ovenfor, løste mypy-demonen nesten fullstendig problemene som oppstår når en programmerer ofte kjører typekontroll, og gjorde endringer i et lite antall filer. Systemytelsen i det minst gunstige brukstilfellet var imidlertid fortsatt langt fra optimal. En ren oppstart av mypy kan ta over 15 minutter. Og dette var mye mer enn vi ville vært fornøyd med. Hver uke ble situasjonen verre ettersom programmerere fortsatte å skrive ny kode og legge til merknader til eksisterende kode. Brukerne våre var fortsatt sultne på mer ytelse, men vi var glade for å møte dem halvveis.

Vi bestemte oss for å gå tilbake til en av de tidligere ideene angående mypy. Nemlig å konvertere Python-kode til C-kode. Å eksperimentere med Cython (et system som lar deg oversette kode skrevet i Python til C-kode) ga oss ingen synlig hastighet, så vi bestemte oss for å gjenopplive ideen om å skrive vår egen kompilator. Siden mypy-kodebasen (skrevet i Python) allerede inneholdt alle nødvendige typekommentarer, tenkte vi at det ville være verdt å prøve å bruke disse merknadene for å få fart på systemet. Jeg laget raskt en prototype for å teste denne ideen. Den viste en mer enn 10 ganger økning i ytelse på ulike mikrobenchmarks. Ideen vår var å kompilere Python-moduler til C-moduler ved å bruke Cython, og å gjøre typemerknader om til kjøretidstypesjekker (vanligvis ignoreres typemerknader under kjøretid og brukes kun av typekontrollsystemer). Vi planla faktisk å oversette mypy-implementeringen fra Python til et språk som var designet for å skrives statisk, som ville se ut (og for det meste fungere) akkurat som Python. (Denne typen tverrspråkmigrering har blitt noe av en tradisjon for mypy-prosjektet. Den originale mypy-implementeringen ble skrevet i Alore, så var det en syntaktisk hybrid av Java og Python).

Å fokusere på CPython-utvidelsen API var nøkkelen til ikke å miste prosjektledelsesevner. Vi trengte ikke å implementere en virtuell maskin eller noen biblioteker som mypy trengte. I tillegg vil vi fortsatt ha tilgang til hele Python-økosystemet og alle verktøyene (som pytest). Dette betydde at vi kunne fortsette å bruke tolket Python-kode under utviklingen, slik at vi kan fortsette å jobbe med et veldig raskt mønster for å gjøre kodeendringer og teste den, i stedet for å vente på at koden skal kompileres. Det så ut som om vi gjorde en god jobb med å sitte på to stoler, for å si det sånn, og vi elsket det.

Kompilatoren, som vi kalte mypyc (siden den bruker mypy som front-end for å analysere typer), viste seg å være et meget vellykket prosjekt. Totalt sett oppnådde vi omtrent 4x speedup for hyppige mypy-kjøringer uten caching. Å utvikle kjernen i mypyc-prosjektet tok et lite team av Michael Sullivan, Ivan Levkivsky, Hugh Hahn og meg selv omtrent 4 kalendermåneder. Denne mengden arbeid var mye mindre enn det som ville vært nødvendig for å omskrive mypy, for eksempel i C++ eller Go. Og vi måtte gjøre mye færre endringer i prosjektet enn vi ville ha måttet gjøre når vi skrev det om på et annet språk. Vi håpet også at vi kunne bringe mypyc til et slikt nivå at andre Dropbox-programmerere kunne bruke det til å kompilere og øke hastigheten på koden deres.

For å oppnå dette ytelsesnivået måtte vi bruke noen interessante tekniske løsninger. Dermed kan kompilatoren fremskynde mange operasjoner ved å bruke raske C-konstruksjoner på lavt nivå. For eksempel blir et kompilert funksjonskall oversatt til et C-funksjonskall. Og et slikt kall er mye raskere enn å kalle en tolket funksjon. Noen operasjoner, for eksempel ordbokoppslag, involverte fortsatt bruk av vanlige C-API-anrop fra CPython, som bare var marginalt raskere når de ble kompilert. Vi var i stand til å eliminere den ekstra belastningen på systemet skapt av tolkning, men dette ga i dette tilfellet kun en liten gevinst når det gjelder ytelse.

For å identifisere de vanligste "langsomme" operasjonene, utførte vi kodeprofilering. Bevæpnet med disse dataene prøvde vi å enten justere mypyc slik at den ville generere raskere C-kode for slike operasjoner, eller skrive om den tilsvarende Python-koden ved å bruke raskere operasjoner (og noen ganger hadde vi rett og slett ikke en enkel nok løsning for det eller andre problemet) . Å omskrive Python-koden var ofte en enklere løsning på problemet enn å la kompilatoren automatisk utføre den samme transformasjonen. På lang sikt ønsket vi å automatisere mange av disse transformasjonene, men på den tiden var vi fokusert på å få fart på mypy med minimal innsats. Og når vi beveget oss mot dette målet, kuttet vi flere hjørner.

To be continued ...

Kjære lesere! Hva var ditt inntrykk av mypy-prosjektet da du fikk vite om dets eksistens?

Veien til typesjekking av 4 millioner linjer med Python-kode. Del 2
Veien til typesjekking av 4 millioner linjer med Python-kode. Del 2

Kilde: www.habr.com

Legg til en kommentar