Stien til at typetjekke 4 millioner linjer Python-kode. Del 2

I dag udgiver vi anden del af oversættelsen af ​​materiale om, hvordan Dropbox organiserede typekontrol for flere millioner linjer Python-kode.

Stien til at typetjekke 4 millioner linjer Python-kode. Del 2

Læs første del

Officiel type support (PEP 484)

Vi gennemførte vores første seriøse eksperimenter med mypy på Dropbox i løbet af Hack Week 2014. Hack Week er en en-uges begivenhed arrangeret af Dropbox. I denne tid kan medarbejderne arbejde med, hvad de vil! Nogle af Dropboxs mest berømte teknologiprojekter begyndte ved begivenheder som disse. Som et resultat af dette eksperiment konkluderede vi, at mypy ser lovende ud, selvom projektet endnu ikke er klar til udbredt brug.

På det tidspunkt var ideen om at standardisere Python-type antydningssystemer i luften. Som sagt var det siden Python 3.0 muligt at bruge typeannoteringer til funktioner, men det var blot vilkårlige udtryk, uden defineret syntaks og semantik. Under programafviklingen blev disse annoteringer for det meste simpelthen ignoreret. Efter Hack Week begyndte vi at arbejde med at standardisere semantikken. Dette arbejde førte til fremkomsten PEP 484 (Guido van Rossum, Łukasz Langa og jeg samarbejdede om dette dokument).

Vores motiver kunne ses fra to sider. For det første håbede vi, at hele Python-økosystemet kunne vedtage en fælles tilgang til brug af typehints (et udtryk brugt i Python som ækvivalent til "typeannoteringer"). Dette ville i betragtning af de mulige risici være bedre end at bruge mange indbyrdes uforenelige tilgange. For det andet ønskede vi åbent at diskutere typeannoteringsmekanismer med mange medlemmer af Python-fællesskabet. Dette ønske var delvist dikteret af det faktum, at vi ikke ville ønske at ligne "frafaldne" fra sprogets grundlæggende ideer i øjnene af de brede masser af Python-programmører. Det er et dynamisk indtastet sprog, kendt som "duck typing". I fællesskabet, i begyndelsen, kunne en noget mistænksom holdning til ideen om statisk skrivning ikke undgå at opstå. Men den følelse forsvandt til sidst, efter at det blev klart, at statisk skrivning ikke ville være obligatorisk (og efter at folk indså, at det faktisk var nyttigt).

Typehint-syntaksen, der til sidst blev vedtaget, lignede meget, hvad mypy understøttede på det tidspunkt. PEP 484 blev udgivet med Python 3.5 i 2015. Python var ikke længere et dynamisk skrevet sprog. Jeg kan godt lide at tænke på denne begivenhed som en væsentlig milepæl i Pythons historie.

Start af migration

I slutningen af ​​2015 oprettede Dropbox et team på tre personer til at arbejde på mypy. De omfattede Guido van Rossum, Greg Price og David Fisher. Fra det øjeblik begyndte situationen at udvikle sig ekstremt hurtigt. Den første hindring for mypys vækst var ydeevne. Som jeg antydede ovenfor, tænkte jeg i de tidlige dage af projektet på at oversætte mypy-implementeringen til C, men denne idé blev krydset af listen for nu. Vi blev hængende med at køre systemet ved hjælp af CPython-fortolkeren, som ikke er hurtig nok til værktøjer som mypy. (PyPy-projektet, en alternativ Python-implementering med en JIT-compiler, hjalp os heller ikke.)

Heldigvis er nogle algoritmiske forbedringer kommet os til hjælp her. Den første kraftfulde "accelerator" var implementeringen af ​​inkrementel kontrol. Ideen bag denne forbedring var enkel: Hvis alle modulets afhængigheder ikke har ændret sig siden den forrige kørsel af mypy, så kan vi bruge de data, der er gemt under den forrige kørsel, mens vi arbejder med afhængigheder. Vi behøvede kun at udføre typekontrol på de ændrede filer og på de filer, der var afhængige af dem. Mypy gik endda lidt længere: Hvis den eksterne grænseflade på et modul ikke ændrede sig, antog mypy, at andre moduler, der importerede dette modul, ikke behøvede at blive kontrolleret igen.

Inkrementel kontrol har hjulpet os meget, når vi annoterer store mængder eksisterende kode. Pointen er, at denne proces normalt involverer mange iterative kørsler af mypy, da annoteringer gradvist tilføjes til koden og gradvist forbedres. Den første kørsel af mypy var stadig meget langsom, fordi den havde mange afhængigheder at kontrollere. For at forbedre situationen implementerede vi derefter en ekstern caching-mekanisme. Hvis mypy registrerer, at den lokale cache sandsynligvis er forældet, downloader den det aktuelle cache-øjebliksbillede for hele kodebasen fra det centraliserede lager. Den udfører derefter en trinvis kontrol ved hjælp af dette øjebliksbillede. Dette har taget os endnu et stort skridt i retning af at øge ydeevnen af ​​mypy.

Dette var en periode med hurtig og naturlig indførelse af typekontrol hos Dropbox. Ved udgangen af ​​2016 havde vi allerede cirka 420000 linjer Python-kode med typeannoteringer. Mange brugere var begejstrede for typekontrol. Flere og flere udviklingsteams brugte Dropbox mypy.

Alt så godt ud dengang, men vi havde stadig meget at lave. Vi begyndte at udføre periodiske interne brugerundersøgelser for at identificere problemområder i projektet og forstå, hvilke problemer der skal løses først (denne praksis bruges stadig i virksomheden i dag). De vigtigste, som det blev klart, var to opgaver. For det første havde vi brug for mere typedækning af koden, for det andet havde vi brug for mypy til at arbejde hurtigere. Det var helt klart, at vores arbejde med at fremskynde mypy og implementere det i virksomhedsprojekter stadig langt fra var afsluttet. Vi, fuldt bevidste om vigtigheden af ​​disse to opgaver, gik i gang med at løse dem.

Mere produktivitet!

Inkrementelle kontroller gjorde mypy hurtigere, men værktøjet var stadig ikke hurtigt nok. Mange trinvise kontroller varede omkring et minut. Årsagen til dette var cyklisk import. Dette vil sandsynligvis ikke overraske nogen, der har arbejdet med store kodebaser skrevet i Python. Vi havde sæt af hundredvis af moduler, som hver indirekte importerede alle de andre. Hvis en fil i en importløkke blev ændret, skulle mypy behandle alle filerne i den pågældende løkke, og ofte alle moduler, der importerede moduler fra den løkke. En sådan cyklus var det berygtede "afhængighedsvirvar", der forårsagede en masse problemer hos Dropbox. Når først denne struktur indeholdt flere hundrede moduler, mens den blev importeret, direkte eller indirekte, mange tests, blev den også brugt i produktionskode.

Vi overvejede muligheden for at "udvikle" cirkulære afhængigheder, men vi havde ikke ressourcerne til at gøre det. Der var for meget kode, som vi ikke var bekendt med. Som et resultat kom vi med en alternativ tilgang. Vi besluttede at få mypy til at fungere hurtigt, selv i nærvær af "afhængighedsfiltre". Vi nåede dette mål ved at bruge mypy-dæmonen. En dæmon er en serverproces, der implementerer to interessante funktioner. For det første gemmer den information om hele kodebasen i hukommelsen. Det betyder, at hver gang du kører mypy, behøver du ikke at indlæse cachelagrede data relateret til tusindvis af importerede afhængigheder. For det andet analyserer han omhyggeligt, på niveau med små strukturelle enheder, afhængighederne mellem funktioner og andre enheder. For eksempel hvis funktionen foo kalder en funktion bar, så er der en afhængighed foo fra bar. Når en fil ændres, behandler dæmonen først isoleret set kun den ændrede fil. Den ser derefter på eksternt synlige ændringer af den fil, såsom ændrede funktionssignaturer. Dæmonen bruger kun detaljerede oplysninger om importer til at dobbelttjekke de funktioner, der rent faktisk bruger den modificerede funktion. Typisk skal du med denne tilgang kontrollere meget få funktioner.

Implementering af alt dette var ikke let, da den oprindelige mypy-implementering var stærkt fokuseret på at behandle én fil ad gangen. Vi skulle håndtere mange grænsesituationer, hvis forekomst krævede gentagne kontroller i tilfælde, hvor noget ændrede sig i koden. For eksempel sker dette, når en klasse tildeles en ny basisklasse. Når vi gjorde, hvad vi ville, var vi i stand til at reducere udførelsestiden for de fleste trinvise kontroller til blot et par sekunder. Det virkede som en stor sejr for os.

Endnu mere produktivitet!

Sammen med den eksterne caching, som jeg diskuterede ovenfor, løste mypy-dæmonen næsten fuldstændigt de problemer, der opstår, når en programmør ofte kører typekontrol og foretager ændringer i et lille antal filer. Systemydelsen i det mindst gunstige anvendelsestilfælde var dog stadig langt fra optimal. En ren opstart af mypy kan tage over 15 minutter. Og det var meget mere, end vi ville have været tilfredse med. Hver uge blev situationen værre, da programmører fortsatte med at skrive ny kode og tilføje kommentarer til eksisterende kode. Vores brugere var stadig sultne efter mere ydeevne, men vi var glade for at møde dem halvvejs.

Vi besluttede at vende tilbage til en af ​​de tidligere ideer vedrørende mypy. Nemlig at konvertere Python-kode til C-kode. At eksperimentere med Cython (et system, der giver dig mulighed for at oversætte kode skrevet i Python til C-kode) gav os ikke nogen synlig fremskyndelse, så vi besluttede at genoplive ideen om at skrive vores egen compiler. Da mypy-kodebasen (skrevet i Python) allerede indeholdt alle de nødvendige typeannoteringer, tænkte vi, at det ville være umagen værd at prøve at bruge disse annoteringer til at fremskynde systemet. Jeg lavede hurtigt en prototype for at teste denne idé. Det viste en mere end 10 gange stigning i ydeevne på forskellige mikro-benchmarks. Vores idé var at kompilere Python-moduler til C-moduler ved hjælp af Cython og at omdanne typeanmærkninger til køretidstypetjek (normalt ignoreres typeannoteringer under kørslen og bruges kun af typekontrolsystemer). Vi planlagde faktisk at oversætte mypy-implementeringen fra Python til et sprog, der var designet til at blive skrevet statisk, som ville se ud (og for det meste fungere) nøjagtigt som Python. (Denne form for migration på tværs af sprog er blevet noget af en tradition for mypy-projektet. Den originale mypy-implementering blev skrevet i Alore, derefter var der en syntaktisk hybrid af Java og Python).

Fokus på CPython extension API var nøglen til ikke at miste projektstyringskapaciteter. Vi behøvede ikke at implementere en virtuel maskine eller nogen biblioteker, som mypy havde brug for. Derudover ville vi stadig have adgang til hele Python-økosystemet og alle værktøjerne (såsom pytest). Dette betød, at vi kunne fortsætte med at bruge fortolket Python-kode under udviklingen, hvilket gav os mulighed for at fortsætte med at arbejde med et meget hurtigt mønster med at lave kodeændringer og teste den, i stedet for at vente på, at koden kompilerede. Det så ud til, at vi gjorde et godt stykke arbejde med at sidde på to stole, så at sige, og vi elskede det.

Compileren, som vi kaldte mypyc (da den bruger mypy som front-end til at analysere typer), viste sig at være et meget vellykket projekt. Samlet set opnåede vi cirka 4x speedup for hyppige mypy-kørsler uden caching. At udvikle kernen af ​​mypyc-projektet tog et lille team af Michael Sullivan, Ivan Levkivsky, Hugh Hahn og mig selv omkring 4 kalendermåneder. Denne mængde arbejde var meget mindre end hvad der ville have været nødvendigt for at omskrive mypy, for eksempel i C++ eller Go. Og vi skulle lave meget færre ændringer i projektet, end vi skulle have lavet, når vi skulle omskrive det til et andet sprog. Vi håbede også, at vi kunne bringe mypyc til et sådant niveau, at andre Dropbox-programmører kunne bruge det til at kompilere og fremskynde deres kode.

For at opnå dette niveau af ydeevne var vi nødt til at anvende nogle interessante tekniske løsninger. Således kan compileren fremskynde mange operationer ved at bruge hurtige C-konstruktioner på lavt niveau. For eksempel oversættes et kompileret funktionskald til et C-funktionskald. Og sådan et opkald er meget hurtigere end at kalde en fortolket funktion. Nogle operationer, såsom ordbogsopslag, involverede stadig brug af almindelige C-API-kald fra CPython, som kun var marginalt hurtigere, når de blev kompileret. Vi var i stand til at eliminere den ekstra belastning på systemet skabt af fortolkning, men dette gav i dette tilfælde kun en lille gevinst med hensyn til ydeevne.

For at identificere de mest almindelige "langsomme" operationer udførte vi kodeprofilering. Bevæbnet med disse data forsøgte vi enten at tilpasse mypyc, så det ville generere hurtigere C-kode til sådanne operationer, eller omskrive den tilsvarende Python-kode ved hjælp af hurtigere operationer (og nogle gange havde vi simpelthen ikke en simpel nok løsning til det eller et andet problem) . Omskrivning af Python-koden var ofte en nemmere løsning på problemet end at lade compileren automatisk udføre den samme transformation. På lang sigt ønskede vi at automatisere mange af disse transformationer, men på det tidspunkt var vi fokuseret på at fremskynde mypy med minimal indsats. Og når vi bevægede os mod dette mål, skar vi flere hjørner.

Fortsættes ...

Kære læsere! Hvad var dit indtryk af mypy-projektet, da du hørte om dets eksistens?

Stien til at typetjekke 4 millioner linjer Python-kode. Del 2
Stien til at typetjekke 4 millioner linjer Python-kode. Del 2

Kilde: www.habr.com

Tilføj en kommentar