Vägen till att typkontrollera 4 miljoner rader Python-kod. Del 2

Idag publicerar vi den andra delen av översättningen av material om hur Dropbox organiserade typkontroll för flera miljoner rader Python-kod.

Vägen till att typkontrollera 4 miljoner rader Python-kod. Del 2

Läs del ett

Officiellt typstöd (PEP 484)

Vi genomförde våra första seriösa experiment med mypy på Dropbox under Hack Week 2014. Hack Week är ett en veckas evenemang som arrangeras av Dropbox. Under denna tid kan medarbetarna arbeta med vad de vill! Några av Dropbox mest kända teknikprojekt började vid evenemang som dessa. Som ett resultat av detta experiment drog vi slutsatsen att mypy ser lovande ut, även om projektet ännu inte är redo för utbredd användning.

Vid den tiden låg idén om att standardisera antydningssystem av Python-typ i luften. Som sagt, sedan Python 3.0 var det möjligt att använda typkommentarer för funktioner, men dessa var bara godtyckliga uttryck, utan definierad syntax och semantik. Under programkörningen ignorerades dessa anteckningar för det mesta helt enkelt. Efter Hack Week började vi arbeta med att standardisera semantik. Detta arbete ledde till uppkomsten PEP 484 (Guido van Rossum, Łukasz Langa och jag samarbetade kring detta dokument).

Våra motiv kunde ses från två sidor. Först hoppades vi att hela Python-ekosystemet kunde anta ett gemensamt tillvägagångssätt för att använda typtips (en term som används i Python som motsvarighet till "typkommentarer"). Detta, med tanke på de möjliga riskerna, skulle vara bättre än att använda många ömsesidigt oförenliga tillvägagångssätt. För det andra ville vi öppet diskutera typanteckningsmekanismer med många medlemmar av Python-gemenskapen. Denna önskan dikterades delvis av det faktum att vi inte skulle vilja se ut som "avfällingar" från språkets grundidéer i ögonen på den breda massan av Python-programmerare. Det är ett dynamiskt skrivet språk, känt som "ankaskrivning". I samhället, i början, kunde en något misstänksam inställning till idén om statisk typning inte låta bli att uppstå. Men den känslan avtog så småningom efter att det stod klart att statisk skrivning inte skulle vara obligatoriskt (och efter att folk insåg att det faktiskt var användbart).

Typtipssyntaxen som så småningom antogs var mycket lik vad mypy stödde vid den tiden. PEP 484 släpptes med Python 3.5 2015. Python var inte längre ett dynamiskt skrivet språk. Jag tycker om att se den här händelsen som en viktig milstolpe i Pythons historia.

Start av migration

I slutet av 2015 skapade Dropbox ett team på tre personer för att arbeta med mypy. De inkluderade Guido van Rossum, Greg Price och David Fisher. Från det ögonblicket började situationen utvecklas extremt snabbt. Det första hindret för mypys tillväxt var prestanda. Som jag antydde ovan tänkte jag i början av projektet på att översätta mypy-implementeringen till C, men denna idé ströks bort från listan för nu. Vi fastnade för att köra systemet med CPython-tolken, som inte är tillräckligt snabb för verktyg som mypy. (PyPy-projektet, en alternativ Python-implementering med en JIT-kompilator, hjälpte oss inte heller.)

Lyckligtvis har några algoritmiska förbättringar kommit till vår hjälp här. Den första kraftfulla "acceleratorn" var implementeringen av inkrementell kontroll. Tanken bakom denna förbättring var enkel: om alla modulens beroenden inte har förändrats sedan föregående körning av mypy, då kan vi använda data som cachelagrades under föregående körning medan vi arbetar med beroenden. Vi behövde bara utföra typkontroll av de modifierade filerna och de filer som var beroende av dem. Mypy gick till och med lite längre: om det externa gränssnittet för en modul inte ändrades, antog mypy att andra moduler som importerade denna modul inte behövde kontrolleras igen.

Inkrementell kontroll har hjälpt oss mycket när vi antecknar stora mängder befintlig kod. Poängen är att denna process vanligtvis involverar många iterativa körningar av mypy eftersom annoteringar gradvis läggs till i koden och gradvis förbättras. Den första körningen av mypy var fortfarande väldigt långsam eftersom den hade många beroenden att kontrollera. Sedan, för att förbättra situationen, implementerade vi en fjärrcachemekanism. Om mypy upptäcker att den lokala cachen sannolikt är inaktuell, laddar den ned den aktuella cache-ögonblicksbilden för hela kodbasen från det centraliserade arkivet. Den utför sedan en inkrementell kontroll med denna ögonblicksbild. Detta har tagit oss ytterligare ett stort steg mot att öka prestandan hos mypy.

Detta var en period av snabb och naturlig användning av typkontroll på Dropbox. I slutet av 2016 hade vi redan cirka 420000 XNUMX rader Python-kod med typkommentarer. Många användare var entusiastiska över typkontroll. Fler och fler utvecklingsteam använde Dropbox mypy.

Allt såg bra ut då, men vi hade fortfarande mycket kvar att göra. Vi började genomföra periodiska interna användarundersökningar för att identifiera problemområden i projektet och förstå vilka problem som måste lösas först (denna praxis används fortfarande i företaget idag). De viktigaste, som det blev tydligt, var två uppgifter. För det första behövde vi mer typtäckning av koden, för det andra behövde vi mypy för att arbeta snabbare. Det var helt klart att vårt arbete med att påskynda mypy och implementera det i företagsprojekt fortfarande var långt ifrån avslutat. Vi, fullt medvetna om vikten av dessa två uppgifter, började lösa dem.

Mer produktivitet!

Inkrementella kontroller gjorde mypy snabbare, men verktyget var fortfarande inte tillräckligt snabbt. Många inkrementella kontroller varade i ungefär en minut. Anledningen till detta var cyklisk import. Detta kommer förmodligen inte att förvåna någon som har arbetat med stora kodbaser skrivna i Python. Vi hade uppsättningar med hundratals moduler, som var och en indirekt importerade alla andra. Om någon fil i en importslinga ändrades, var mypy tvungen att bearbeta alla filer i den slingan, och ofta alla moduler som importerade moduler från den slingan. En sådan cykel var den ökända "beroendehärvan" som orsakade mycket problem på Dropbox. När denna struktur väl innehöll flera hundra moduler, samtidigt som den importerades, direkt eller indirekt, många tester, användes den också i produktionskod.

Vi övervägde möjligheten att "lösa upp" cirkulära beroenden, men vi hade inte resurserna att göra det. Det var för mycket kod som vi inte var bekanta med. Som ett resultat kom vi fram till ett alternativt tillvägagångssätt. Vi bestämde oss för att få mypy att fungera snabbt även i närvaro av "beroendehärvor". Vi uppnådde detta mål med mypy-demonen. En demon är en serverprocess som implementerar två intressanta funktioner. För det första lagrar den information om hela kodbasen i minnet. Detta innebär att varje gång du kör mypy behöver du inte ladda cachad data relaterad till tusentals importerade beroenden. För det andra analyserar han noggrant, på nivån för små strukturella enheter, beroenden mellan funktioner och andra enheter. Till exempel om funktionen foo anropar en funktion bar, då finns det ett beroende foo från bar. När en fil ändras, bearbetar demonen först, isolerat, endast den ändrade filen. Den tittar sedan på externt synliga ändringar av den filen, såsom ändrade funktionssignaturer. Demonen använder endast detaljerad information om importer för att dubbelkolla de funktioner som faktiskt använder den modifierade funktionen. Med detta tillvägagångssätt måste du vanligtvis kontrollera väldigt få funktioner.

Att implementera allt detta var inte lätt, eftersom den ursprungliga mypy-implementeringen var starkt fokuserad på att bearbeta en fil i taget. Vi var tvungna att hantera många gränssituationer, vars förekomst krävde upprepade kontroller i de fall något ändrats i koden. Detta händer till exempel när en klass tilldelas en ny basklass. När vi väl gjorde vad vi ville kunde vi minska exekveringstiden för de flesta inkrementella kontroller till bara några sekunder. Det här verkade vara en stor seger för oss.

Ännu mer produktivitet!

Tillsammans med fjärrcachen som jag diskuterade ovan, löste mypy-demonen nästan helt de problem som uppstår när en programmerare ofta kör typkontroll och gör ändringar i ett litet antal filer. Systemprestandan i det minst gynnsamma användningsfallet var dock fortfarande långt ifrån optimal. En ren uppstart av mypy kan ta över 15 minuter. Och detta var mycket mer än vad vi skulle ha varit nöjda med. Varje vecka blev situationen värre när programmerare fortsatte att skriva ny kod och lägga till kommentarer till befintlig kod. Våra användare var fortfarande hungriga efter mer prestanda, men vi var glada över att träffa dem halvvägs.

Vi bestämde oss för att återgå till en av de tidigare idéerna angående mypy. Nämligen att konvertera Python-kod till C-kod. Att experimentera med Cython (ett system som låter dig översätta kod skriven i Python till C-kod) gav oss ingen synlig hastighet, så vi bestämde oss för att återuppliva tanken på att skriva vår egen kompilator. Eftersom mypy-kodbasen (skriven i Python) redan innehöll alla nödvändiga typkommentarer, tyckte vi att det skulle vara värt att försöka använda dessa annoteringar för att snabba upp systemet. Jag skapade snabbt en prototyp för att testa denna idé. Den visade en mer än 10-faldig ökning av prestanda på olika mikrobenchmarks. Vår idé var att kompilera Python-moduler till C-moduler med Cython och att förvandla typkommentarer till körtidstypkontroller (vanligtvis ignoreras typkommentarer vid körning och används endast av typkontrollsystem). Vi planerade faktiskt att översätta mypy-implementeringen från Python till ett språk som var designat för att skrivas statiskt, som skulle se ut (och för det mesta fungera) precis som Python. (Den här typen av migrering över flera språk har blivit något av en tradition av mypy-projektet. Den ursprungliga mypy-implementeringen skrevs i Alore, sedan fanns det en syntaktisk hybrid av Java och Python).

Att fokusera på CPython extension API var nyckeln till att inte tappa projektledningskapacitet. Vi behövde inte implementera en virtuell maskin eller några bibliotek som mypy behövde. Dessutom skulle vi fortfarande ha tillgång till hela Python-ekosystemet och alla verktyg (som pytest). Detta innebar att vi kunde fortsätta att använda tolkad Python-kod under utvecklingen, vilket gjorde att vi kunde fortsätta arbeta med ett mycket snabbt mönster för att göra kodändringar och testa den, snarare än att vänta på att koden ska kompileras. Det såg ut som att vi gjorde ett bra jobb med att sitta på två stolar så att säga, och vi älskade det.

Kompilatorn, som vi kallade mypyc (eftersom den använder mypy som en front-end för att analysera typer), visade sig vara ett mycket framgångsrikt projekt. Sammantaget uppnådde vi ungefär 4x speedup för frekventa mypy-körningar utan cachning. Att utveckla kärnan i mypyc-projektet tog ett litet team av Michael Sullivan, Ivan Levkivsky, Hugh Hahn och mig själv cirka 4 kalendermånader. Denna mängd arbete var mycket mindre än vad som skulle ha behövts för att skriva om mypy, till exempel i C++ eller Go. Och vi var tvungna att göra mycket färre ändringar i projektet än vi skulle ha behövt göra när vi skrev om det på ett annat språk. Vi hoppades också att vi kunde få mypyc till en sådan nivå att andra Dropbox-programmerare kunde använda den för att kompilera och snabba upp sin kod.

För att uppnå denna prestandanivå var vi tvungna att tillämpa några intressanta tekniska lösningar. Således kan kompilatorn påskynda många operationer genom att använda snabba C-konstruktioner på låg nivå. Till exempel översätts ett kompilerat funktionsanrop till ett C-funktionsanrop. Och ett sådant anrop är mycket snabbare än att anropa en tolkad funktion. Vissa operationer, som lexikonuppslag, involverade fortfarande att använda vanliga C-API-anrop från CPython, som bara var marginellt snabbare när de kompilerades. Vi kunde eliminera den extra belastningen på systemet som skapades genom tolkning, men detta gav i det här fallet bara en liten vinst i form av prestanda.

För att identifiera de vanligaste "långsamma" operationerna utförde vi kodprofilering. Beväpnade med dessa data försökte vi att antingen justera mypyc så att den skulle generera snabbare C-kod för sådana operationer, eller skriva om motsvarande Python-kod med snabbare operationer (och ibland hade vi helt enkelt inte en tillräckligt enkel lösning för det eller andra problemet) . Att skriva om Python-koden var ofta en enklare lösning på problemet än att låta kompilatorn automatiskt utföra samma transformation. På lång sikt ville vi automatisera många av dessa transformationer, men vid den tiden var vi fokuserade på att påskynda mypy med minimal ansträngning. Och när vi rörde oss mot detta mål skar vi flera hörn.

Fortsättning ...

Kära läsare! Vad var ditt intryck av mypy-projektet när du fick reda på dess existens?

Vägen till att typkontrollera 4 miljoner rader Python-kod. Del 2
Vägen till att typkontrollera 4 miljoner rader Python-kod. Del 2

Källa: will.com

Lägg en kommentar