Idag publicerar vi den andra delen av översÀttningen av materialet om hur Dropbox organiserade typkontroll för flera miljoner rader Python-kod.
â
Officiellt stöd för typer (PEP 484)
Vi genomförde vÄra första seriösa experiment med mypy pÄ Dropbox under Hack Week 2014. Hack Week Àr ett evenemang som hÄlls av Dropbox under en vecka. Under denna tid kan anstÀllda arbeta med vad de vill! NÄgra av de mest kÀnda tekniska projekten frÄn Dropbox har startats just under sÄdana evenemang. Som ett resultat av detta experiment drog vi slutsatsen att mypy ser lovande ut, Àven om projektet Ànnu inte var redo för bred anvÀndning.
Vid den tiden flöt idĂ©n om att standardisera system för typinmatningar i Python i luften. Som jag redan nĂ€mnt, sedan Python 3.0 har det varit möjligt att anvĂ€nda typanoteringar för funktioner, men dessa var bara godtyckliga uttryck utan specifik syntaktik och semantik. Under programkörningen ignorerades dessa anoteringar i stor utstrĂ€ckning. Efter Hack Week började vi arbeta med standardiseringen av semantiken. Detta arbete ledde till skapandet av (detta dokument utarbetades gemensamt av Guido van Rossum, Ćukasz Langa och mig).
VĂ„ra motiv kan ses frĂ„n tvĂ„ perspektiv. För det första hoppades vi att hela Python-ekosystemet skulle kunna anta en gemensam metod för anvĂ€ndning av typinmatningar (type hints â en term som anvĂ€nds i Python som en analog till 'typannoteringar'). Detta, med alla eventuella risker i Ă„tanke, skulle vara bĂ€ttre Ă€n att anvĂ€nda mĂ„nga ömsesidigt oförenliga metoder. För det andra ville vi öppet diskutera mekanismerna för typanotering med mĂ„nga representanter frĂ„n Python-communityn. Delvis berodde denna önskan pĂ„ att vi inte ville framstĂ„ som 'avvikare' frĂ„n sprĂ„kets grundlĂ€ggande idĂ©er i ögonen pĂ„ den breda massan av Python-programmerare. Det Ă€r ett dynamiskt typat sprĂ„k, kĂ€nt för 'anktypning'. I communityn, i början, kunde man inte undvika en viss misstĂ€nksamhet mot idĂ©n om statisk typning. Men denna instĂ€llning har sĂ„ smĂ„ningom minskat â efter att det blev klart att statisk typning inte planeras att göras obligatorisk (och efter att folk insĂ„g att det faktiskt Ă€r anvĂ€ndbart).
Den antagna syntaktiska strukturen för typanvisningar liknade mycket den som mypy dÄ stödde. Dokumentet PEP 484 slÀpptes tillsammans med Python 3.5 Är 2015. Python var inte lÀngre ett sprÄk som enbart stödde dynamisk typning. Jag gillar att tÀnka pÄ detta som en viktig milstolpe i Pythons historia.
Början av migrationen
I slutet av 2015 bildades ett team av tre personer pÄ Dropbox för att arbeta med mypy. Det bestod av Guido van Rossum, Greg Price och David Fisher. FrÄn och med detta började situationen utvecklas mycket snabbt. Det första hinder som mypy stötte pÄ var prestanda. Som jag nÀmnt tidigare funderade jag i projektets tidiga skeden pÄ att översÀtta mypy:s implementation till C, men den idén ströks för tillfÀllet. Vi blev fast vid att vi anvÀnde CPython-tolkaren, som inte hade en hastighet tillrÀcklig för verktyg som mypy. (Projektet PyPy, en alternativ implementation av Python med JIT-kompilator, hjÀlpte oss inte heller.)
Som tur var fick vi hjÀlp av vissa algoritmiska förbÀttringar. Den första kraftfulla "acceleratorn" var implementeringen av inkrementell kontroll. Idén bakom denna förbÀttring var enkel: om alla beroenden för en modul inte hade förÀndrats sedan mypy senast kördes, sÄ kunde vi anvÀnda de data som cacheats under föregÄende session vid arbetet med beroenden. Vi behövde bara utföra typkontroll i de Àndrade filerna och i filer som berodde pÄ dem. Mypy gick till och med ett steg lÀngre: om modulen yttre grÀnssnitt inte hade förÀndrats, ansÄg mypy att andra moduler som importerade denna modul inte behövde kontrolleras igen.
Den inkrementella kontrollen har verkligen hjÀlpt oss nÀr vi har anvÀnt oss av annoteringar pÄ stora mÀngder befintlig kod. Problemet Àr att denna process vanligtvis involverar mÄnga iterativa körningar av mypy, eftersom annotationerna gradvis lÀggs till i koden och förbÀttras över tid. Den första körningen av mypy var fortfarande vÀldigt lÄngsam, eftersom den behövde kontrollera mÄnga beroenden. För att förbÀttra situationen implementerade vi en mekanism för fjÀrr-cache. Om mypy upptÀcker att den lokala cachen sannolikt Àr förÄldrad, laddar den ner den aktuella cachen för hela kodbasen frÄn ett centraliserat repository. DÀrefter utförs den inkrementella kontrollen med hjÀlp av denna snapshot. Detta har avancerat oss ytterligare ett steg mot att öka mypy:s prestanda.
Det var en period av snabb och naturlig implementering av typkontrollsystemet pÄ Dropbox. I slutet av 2016 hade vi redan omkring 420 000 rader Python-kod med typannoteringar. MÄnga anvÀndare tog entusiastiskt till sig typkontrollen. PÄ Dropbox anvÀndes mypy av allt fler utvecklingsteam.
Allt sÄg bra ut dÄ, men vi hade fortfarande mycket kvar att göra. Vi började genomföra regelbundna interna anvÀndarundersökningar för att identifiera problemomrÄden i projektet och förstÄ vilka frÄgor som behövde lösas först (denna praktik anvÀnds fortfarande i företaget idag). Det blev klart att tvÄ uppgifter var de viktigaste. För det första behövde vi mer kodtÀcke med typer, och för det andra behövde mypy att arbeta snabbare. Det var helt tydligt att vÄrt arbete med att snabba upp mypy och implementera det i företagets projekt fortfarande hade en lÄng vÀg kvar att gÄ. Med full insikt om vikten av dessa tvÄ uppgifter satte vi igÄng med att lösa dem.
Mer prestanda!
Inkrementella kontroller har pÄskyndat mypy, men verktyget Àr fortfarande inte tillrÀckligt snabbt. MÄnga inkrementella kontroller tog runt en minut. Orsaken till detta var cykliska importer. Detta förvÄnar förmodligen ingen som har arbetat med stora kodbaser skrivna i Python. Vi hade uppsÀttningar med hundratals moduler, dÀr var och en indirekt importerade alla andra. Om nÄgon fil i importcykeln Àndrades, tvingades mypy att bearbeta alla filer som ingick i denna cykel, och ofta Àven moduler som importerade moduler frÄn denna cykel. En av dessa cykler var den ökÀnda "beroendeknuten" som orsakade mÄnga problem i Dropbox. Vid ett tillfÀlle innehöll denna struktur flera hundra moduler, och den importerades, direkt eller indirekt, av mÄnga tester, samt anvÀndes i produktionskoden.
Vi övervÀgde möjligheten att "reda ut" cykliska beroenden, men vi hade inte resurserna för att göra det. Det fanns alldeles för mycket kod som vi inte kÀnde till. Till slut landade vi pÄ ett alternativt tillvÀgagÄngssÀtt. Vi bestÀmde oss för att fÄ mypy att fungera snabbt Àven med "beroendeknutar". Vi nÄdde detta mÄl med hjÀlp av mypy-demonen. Demonen Àr en serverprocess som implementerar tvÄ intressanta funktioner. För det första - den hÄller all information om hela kodbasen i minnet. Detta innebÀr att mypy inte behöver ladda cachad data som rör tusentals importerade beroenden vid varje körning. För det andra - den analyserar noggrant, pÄ en detaljerad strukturell nivÄ, relationerna mellan funktioner och andra entiteter. Till exempel, om funktionen foo anropar funktionen bar, finns det ett beroende foo frÄn bar. NÀr en fil Àndras, bearbetar demonen först, i isolering, bara den Àndrade filen. DÀrefter tittar den pÄ förÀndringarna i denna fil som Àr synliga utanför, pÄ sÄdant som Àndrade funktionssignaturer. Demonen anvÀnder detaljerad information om importer endast för att dubbelkolla de funktioner som verkligen anvÀnder den Àndrade funktionen. Vanligtvis behöver man kontrollera ganska fÄ funktioner med denna metod.
Genomförandet av allt detta var ingen lÀtt uppgift, eftersom den ursprungliga implementationen av mypy var starkt inriktad pÄ att bearbeta en fil i taget. Vi stötte pÄ mÄnga grÀnsytor dÀr det var nödvÀndigt med upprepade kontroller nÀr nÄgot förÀndrades i koden. Till exempel hÀnder detta nÀr en klass fÄr en ny basklass. Efter att vi gjort det vi ville kunde vi minska tiden för de flesta inkrementella kontroller till nÄgra sekunder. Detta kÀndes som en stor seger för oss.
Ănnu mer prestanda!
Tillsammans med den fjÀrrcache som jag nÀmnde tidigare har mypy-demonen i stort sett helt löst problemen som uppstÄr nÀr en programmerare ofta kör typkontroller medan de gör Àndringar i ett fÄtal filer. Dock var systemets prestanda i det minst gynnsamma anvÀndningsscenariot fortfarande lÄngt ifrÄn optimal. En ren körning av mypy kunde ta över 15 minuter. Detta var lÄngt mer Àn vad vi skulle vara nöjda med. Varje vecka blev situationen vÀrre, eftersom programmerarna fortsatte att skriva ny kod och lÀgga till anteckningar till den befintliga koden. VÄra anvÀndare lÀngtade fortfarande efter mer prestanda, och vi var mer Àn villiga att möta deras förvÀntningar.
Vi bestÀmde oss för att ÄtervÀnda till en av de tidiga idéerna om mypy. NÀrmare bestÀmt att konvertera Python-kod till C-kod. Experiment med Cython (ett system som gör det möjligt att översÀtta kod skriven i Python till C-kod) gav oss ingen synlig prestandaförbÀttring, sÄ vi beslutade oss för att vÀcka idén om att skriva en egen kompilator. Eftersom mypy:s kodbas (skriven i Python) redan innehöll alla nödvÀndiga typannoteringar, verkade det vÀrt att försöka anvÀnda dessa annoteringar för att snabba upp systemets arbete. Jag skapade snabbt en prototyp för att testa denna idé. Den visade mer Àn tiofaldig prestanda pÄ olika mikrobÀnkar. VÄr idé var att kompilera Python-moduler till C-moduler med hjÀlp av Cython, och att omvandla typannoteringar till typkontroller som utförs under programmets körning (vanligtvis ignoreras typannoteringar vid körning och anvÀnds endast av typkontrolleringssystem). Vi planerade faktiskt att översÀtta implementeringen av mypy frÄn Python till ett sprÄk som var statiskt typat och som skulle se ut (och i de flesta fall fungera) precis som Python. (Denna typ av sprÄkövergÄng har blivit nÄgot av en tradition för mypy-projektet. Den ursprungliga implementeringen av mypy skrevs i Alore, följd av en syntaxhybrid mellan Java och Python).
Fokus pÄ CPython:s API:er var nyckeln till att inte förlora möjligheter till projektledning. 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, inklusive alla verktyg (som pytest). Detta innebar att vi skulle kunna fortsÀtta anvÀnda tolkad Python-kod under utvecklingen, vilket skulle tillÄta oss att arbeta med en mycket snabb kodÀndrings- och testningscykel, istÀllet för att vÀnta pÄ kodkompilering. Det kÀndes som att vi framgÄngsrikt kunde sitta pÄ tvÄ stolar, och vi gillade det.
Kompilatoren som vi kallade mypyc (eftersom den anvÀnder mypy för typanalys som frontend) har visat sig vara ett ganska framgÄngsrikt projekt. Totalt sett har vi uppnÄtt ungefÀr en 4-faldig ökning av hastigheten vid frekventa körningar av mypy utan att anvÀnda cache. Utvecklingen av kÀrnan i mypyc tog en liten grupp, bestÄende av Michael Sullivan, Ivan Levkivskyi, Hugh Han och jag, cirka 4 kalender mÄnader. Denna arbetsinsats var mycket mindre omfattande Àn vad som skulle krÀvas för att skriva om mypy, till exempel i C++ eller Go. Och vi behövde ocksÄ göra betydligt fÀrre förÀndringar i projektet Àn vi skulle ha behövt om vi skrivit om det pÄ ett annat sprÄk. Dessutom hoppades vi att vi skulle kunna ta mypyc till en nivÄ dÀr andra programmerare frÄn Dropbox skulle kunna anvÀnda det för att kompilera och pÄskynda sin kod.
För att uppnÄ en sÄdan prestandanivÄ var vi tvungna att tillÀmpa nÄgra intressanta ingenjörslösningar. Kompilatorn kan pÄskynda utförandet av mÄnga operationer genom att anvÀnda snabba lÄg nivÄ C-konstruktioner. Till exempel översÀtts ett anrop av en kompilerad funktion till ett anrop av en C-funktion. Och ett sÄdant anrop utförs mycket snabbare Àn ett anrop av en tolkad funktion. Vissa operationer, som sökningar i ordböcker, Ätergick fortfarande till att anvÀnda vanliga anrop av C-API frÄn CPython, vilka efter kompileringen bara var nÄgot snabbare. Vi lyckades eliminera den extra belastningen pÄ systemet som skapades av tolkning, men detta gav endast en liten prestandaförbÀttring i det hÀr fallet.
För att identifiera de mest förekommande "lÄngsamma" operationerna utförde vi en profilering av koden. Med de insamlade uppgifterna försökte vi antingen justera mypyc för att generera snabbare C-kod för sÄdana operationer, eller skriva om motsvarande Python-kod med hjÀlp av snabbare operationer (ibland hade vi helt enkelt inte nÄgon enkel lösning för nÄgot problem). Att skriva om Python-koden visade sig ofta vara en enklare lösning pÄ problemet Àn att implementera en automatisk utförande av samma transformation i kompileraren. PÄ lÄng sikt ville vi automatisera mÄnga av dessa transformationer, men för tillfÀllet var vi fokuserade pÄ att with minimal effort accelerera mypy. Med den ambitionen skar vi nÄgra hörn.
FortsĂ€ttning följerâŠ
KÀra lÀsare! Vilka intryck fick projektet mypy nÀr du först fick kÀnnedom om dess existens?
KĂ€lla: habr.com
