I dag offentliggør vi den anden del af oversættelsen af materialet om, hvordan Dropbox organiserede typekontrol for flere millioner linjer Python-kode.
→
Officiel understøttelse af typer (PEP 484)
Vi foretog de første seriøse eksperimenter med mypy i Dropbox under Hack Week 2014. Hack Week er et arrangement, der afholdes af Dropbox i løbet af en uge, hvor medarbejderne kan arbejde på, hvad de ønsker! Nogle af de mest berømte teknologiprojekter i Dropbox startede netop ved sådanne arrangementer. Som resultat af dette eksperiment kom vi frem til, at mypy ser lovende ud, selvom dette projekt endnu ikke var klart til bred anvendelse.
På det tidspunkt luftede man ideen om at standardisere systemer til typehinting i Python. Som jeg allerede har nævnt, kunne man fra Python 3.0 bruge typeannoteringer til funktioner, men de var kun arbitrære udtryk uden specifik syntaks og semantik. Under programudførelsen blev disse annoteringer for det meste blot ignoreret. Efter Hack Week begyndte vi at arbejde på at standardisere semantikken. Dette arbejde førte til (dokumentet er blevet udarbejdet i samarbejde med Guido van Rossum, Lukáš Lalík og mig).
Vores motiver kunne ses fra to sider. For det første håbede vi, at hele Python-økosystemet kunne adoptere en fælles tilgang til brug af type hints (type hints - et udtryk, der bruges i Python som en analog til "typeannoteringer"). Dette ville, i betragtning af de mulige risici, være bedre end at bruge mange inkompatible tilgange. For det andet ønskede vi at diskutere mekanismerne bag typeannotering med mange repræsentanter fra Python-samfundet. En del af dette ønske var drevet af, at vi ikke ønskede at fremstå som "afvigere" fra sproglets grundprincipper i øjnene på de mange Python-udviklere. Dette er et dynamisk typet sprog, kendt for "andetype-typering". I samfundet kunne der i starten ikke undgås at opstå en vis mistro til ideen om statisk typet. Men denne indstilling svandt ind, efterhånden som det blev klart, at statisk typet ikke ville blive gjort obligatorisk (og efter at folk indså, at det faktisk er nyttigt).
Den vedtagne syntaks for type hints lignede meget den, som mypy på det tidspunkt understøttede. PEP 484-dokumentet blev frigivet sammen med Python 3.5 i 2015. Python var ikke længere bare et sprog med dynamisk typetydighed. Jeg kan godt lide at tænke på denne begivenhed som en vigtig milepæl i Pythons historie.
Starten på migrationen
I slutningen af 2015 blev der i Dropbox oprettet et team bestående af tre personer for at arbejde på mypy. Teamet bestod af Guido van Rossum, Greg Price og David Fisher. Fra det tidspunkt begyndte situationen at udvikle sig meget hurtigt. Det første forhindring for mypys vækst var ydeevnen. Som jeg tidligere nævnte, overvejede jeg i den tidlige fase af projektet at omkode mypy i C, men denne idé blev midlertidigt strøget. Vi var fanget i den situation, at systemet kørte på CPython, som ikke tilbød den hastighed, der var tilstrækkelig for værktøjer som mypy. (Projektet PyPy, en alternativ implementering af Python med JIT-kompilator, hjalp os heller ikke.)
Heldigvis kom der nogle algorithmiske forbedringer til hjælp. Den første kraftige "accelerator" var implementeringen af inkrementel kontrol. Ideen bag denne forbedring var enkel: hvis alle moduls afhængigheder ikke havde ændret sig siden den sidste kørsel af mypy, kunne vi bruge de data, der blev cachet under den tidligere session, mens vi arbejdede med afhængighederne. Vi skulle kun udføre typekontrol i de ændrede filer og i de filer, der var afhængige af dem. Mypy gik endda et skridt videre: hvis et modsændt interface for modulet ikke ændrede sig, anså mypy, at andre moduler, der importerede dette modul, ikke skulle kontrolleres igen.
Den inkrementelle kontrol hjalp os betydeligt, da vi annoterede store mængder eksisterende kode. Processen involverede typisk mange iterative kørsel af mypy, da annotationer blev tilføjet til koden gradvist. Den første kørsel af mypy var stadig meget langsom, da dens udførelse krævede kontrol af mange afhængigheder. For at forbedre situationen implementerede vi en mekanisme til fjerncache. Hvis mypy opdager, at den lokale cache sandsynligvis er forældet, henter den det aktuelle cache-snapshot for hele kodebasen fra et centraliseret repository. Derefter udfører den, ved hjælp af dette snapshot, en inkrementel kontrol. Dette har betydet endnu et stort skridt fremad i at øge mypys ydeevne.
Det var en periode med hurtig og naturlig implementering af typekontrolsystemet i Dropbox. Ved udgangen af 2016 havde vi allerede omkring 420.000 linjer Python-kode med typeannotationer. Mange brugere tog entusiastisk imod typekontrol. I Dropbox blev mypy brugt af stadig flere udviklingsteams.
Alt så godt ud dengang, men der var stadig meget, der skulle gøres. Vi begyndte at gennemføre periodiske interne brugerundersøgelser for at identificere problemområder i projektet og forstå, hvilke spørgsmål der skulle løses først (denne praksis anvendes stadig i virksomheden i dag). Det blev klart, at de vigtigste opgaver var to. Den første var et større dækning af koden med typer, den anden var, at mypy skulle arbejde hurtigere. Det var helt tydeligt, at vores arbejde med at fremskynde mypy og implementere det i virksomhedens projekter stadig var langt fra fuldført. Vi, fuldt ud bevidste om vigtigheden af disse to opgaver, gik i gang med at løse dem.
Mere ydeevne!
Inkrementelle kontroller har accelereret mypy, men dette værktøj var stadig ikke hurtigt nok. Mange inkrementelle kontroller tog omkring et minut. Årsagen til dette var cykliske imports. Det vil sandsynligvis ikke overraske dem, der har arbejdet med store kodebaser skrevet i Python. Vi havde sæt af hundreder af moduler, hver af dem indirekte importerede alle de andre. Hvis en fil i en importcyklus blev ændret, måtte mypy behandle alle filer i denne cyklus, og ofte også eventuelle moduler, der importerede moduler fra denne cyklus. En af disse cykler var den berygtede "afhængighedsknude", som forårsagede mange problemer i Dropbox. En gang indeholdt denne struktur flere hundrede moduler, som blev importeret, direkte eller indirekte, af mange tests, og den blev også brugt i produktionskoden.
Vi overvejede muligheden for at "udrede" cykliske afhængigheder, men vi havde ikke ressourcerne til at gøre det. Der var for meget kode, vi ikke var bekendte med. I sidste ende bevægede vi os mod en alternativ tilgang. Vi besluttede at gøre sådan, at mypy ville arbejde hurtigt selv i nærvær af "afhængighedsknuder". Vi opnåede dette mål ved hjælp af mypy-dæmonen. Dæmonen er en serverproces, der implementerer to interessante funktioner. For det første holder den information om hele kodebasen i hukommelsen. Dette betyder, at mypy ikke behøver at indlæse cachede data, der vedrører tusinder af importerede afhængigheder, ved hver kørsel. For det andet analyserer den nøje, på niveau med små strukturelle enheder, afhængigheder mellem funktioner og andre enheder. For eksempel, hvis funktionen foo kalder funktionen bar, så er der en afhængighed foo fra bar. Når en fil ændres, behandler dæmonen først, isoleret, kun den ændrede fil. Derefter ser den på ændringerne i denne fil, der er synlige udefra, såsom ændrede funktionssignaturer. Dæmonen bruger detaljeret information om imports kun til at genkontrollere de funktioner, der virkelig bruger den ændrede funktion. Normalt skal der kun kontrolleres meget få funktioner ved en sådan tilgang.
Implementeringen af alt dette var ingen nem opgave, da den oprindelige implementering af mypy var meget fokuseret på at bearbejde én fil ad gangen. Vi måtte stå over for mange grænsetilfælde, som krævede gentagne kontroller, når der blev foretaget ændringer i koden. For eksempel sker dette, når en klasse tildeles en ny baseklasse. Efter at vi havde lavet det ønskede, kunne vi reducere kørselstiden for de fleste inkrementelle kontroller til kun et par sekunder. Dette føltes som en stor sejr for os.
Endnu mere ydeevne!
Sammen med det fjerncache, jeg nævnte ovenfor, har mypy-daemonen næsten helt løst de problemer, der opstår, når en programmerer ofte kører typekontrol, mens der kun foretages ændringer i et lille antal filer. Dog var systemets ydeevne i den mindst gunstige anvendelse stadig langt fra optimal. Et rent kørsel af mypy kunne tage mere end 15 minutter. Og det var meget mere, end hvad vi ville have været tilfredse med. Hver uge blev situationen værre, da programmerne fortsatte med at skrive ny kode og tilføje annotationer til eksisterende kode. Vores brugere tørstede stadig efter mere ydeevne, og vi var glade for at imødekomme deres ønsker.
Vi besluttede at vende tilbage til en af de tidlige ideer omkring mypy. Nemlig at konvertere Python-kode til C-kode. Eksperimenter med Cython (et system, der tillader oversættelse af kode skrevet i Python til C-kode) gav os ikke synlige hastighedsforbedringer, så vi besluttede at genoplive ideen om at skrive vores egen compiler. Da mypy's kodebase (skrevet i Python) allerede indeholdt alle nødvendige typeannoteringer, syntes det som en værdifuld indsats at bruge disse annoteringer til at forbedre systemets ydelse. Jeg lavede hurtigt en prototype for at teste denne idé. Den viste mere end 10 gange bedre ydeevne i forskellige mikro-benchmark tests. Vores idé var at kompilere Python-moduler til C-moduler ved hjælp af Cython, og at omsætte typeannoteringer til typekontroller, der udføres under programudførelsen (normalt ignoreres typeannoteringerne under programudførelsen og bruges kun af typekontrolsystemer). Vi planlagde faktisk at omsætte implementeringen af mypy fra Python til et sprog, der var statisk typet, og som ville se ud (og for det meste fungere) præcis som Python. (Denne form for mellem sproglig migration er blevet noget af en tradition for mypy-projektet. Den oprindelige implementering af mypy var skrevet i Alore, derefter blev der udviklet en syntaktisk hybrid af Java og Python).
Fokus på CPython-extensions API'en var nøglen til ikke at miste mulighederne for projektstyring. Vi behøvede ikke at implementere en virtuel maskine eller nogen biblioteker, som mypy havde brug for. Desuden ville hele Python-økosystemet stadig være tilgængeligt, inklusiv alle værktøjer (såsom pytest). Det betød, at vi kunne fortsætte med at bruge tolked Python-kode under udviklingen, hvilket ville give os mulighed for at fortsætte med meget hurtige ændringer i koden og dens testning i stedet for at vente på kompilering af koden. Det føltes, som om vi klarerede os rigtig godt, så at sige at sidde på to stole, og det kunne vi lide.
Compilatoren, som vi kaldte mypyc (da den bruger mypy til typeanalyse i frontenden), viste sig at være et meget succesfuldt projekt. Generelt har vi opnået cirka firedobbelt hastighedsforøgelse af hyppige mypy-kørsler uden at benytte caching. Udviklingen af kernen i mypyc tog et lille team, som bestod af Michael Sullivan, Ivan Levkiwsky, Hugh Han, og mig, cirka 4 kalender måneder. Dette arbejdsmængde var langt mindre omfattende end det, der ville have været nødvendigt for at omskrive mypy, f.eks. til C++ eller Go. Derudover måtte vi lave langt færre ændringer i projektet end vi ville have skulle lave, hvis det blev omskrevet på et andet sprog. Vi håbede også, at vi kunne bringe mypyc op på et niveau, så andre programmører fra Dropbox kunne bruge det til at kompilere og accelerere deres kode.
For at opnå et sådant niveau af ydeevne måtte vi anvende nogle interessante ingeniørløsninger. Kompilatoren kan accelerere udførelsen af mange operationer ved at bruge hurtige, lave niveauer C-konstruktioner. For eksempel oversættes kaldet til den kompilerede funktion til et C-funktionsanrop. Og et sådant kald udføres meget hurtigere end et kald af en fortolket funktion. Nogle operationer, såsom opkald til ordbøger, involverede stadig almindelige kald til C-API fra CPython, som efter kompilering kun blev lidt hurtigere. Vi var i stand til at fjerne den ekstra belastning på systemet, der blev skabt af fortolkningen, men dette gav kun en lille gevinst i ydeevnen.
For identifying the most common 'slow' operations, we performed code profiling. Armed with the data obtained, we attempted to either tweak mypyc so that it generated faster C code for such operations, or rewrite the corresponding Python code using faster operations (and sometimes we simply did not have a straightforward solution to a particular problem). Rewriting the Python code often turned out to be a simpler 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 focused on speeding up mypy with minimal effort. And, moving toward this goal, we cut a few corners.
Fortsætter…
Kære læsere! What were your impressions of the mypy project when you first learned about its existence?
Kilde: habr.com
