Het pad naar het typechecken van 4 miljoen regels Python-code. Deel 2

Vandaag publiceren we het tweede deel van de vertaling van materiaal over hoe Dropbox typecontrole organiseerde voor enkele miljoenen regels Python-code.

Het pad naar het typechecken van 4 miljoen regels Python-code. Deel 2

Lees deel één

Officiële typeondersteuning (PEP 484)

We hebben onze eerste serieuze experimenten met mypy uitgevoerd bij Dropbox tijdens Hack Week 2014. Hack Week is een evenement van een week dat wordt georganiseerd door Dropbox. Gedurende deze tijd kunnen medewerkers werken aan wat ze willen! Enkele van de beroemdste technologieprojecten van Dropbox zijn begonnen op evenementen als deze. Als resultaat van dit experiment hebben we geconcludeerd dat mypy er veelbelovend uitziet, hoewel het project nog niet klaar is voor wijdverbreid gebruik.

Destijds hing het idee in de lucht om hintsystemen van het Python-type te standaardiseren. Zoals ik al zei, was het sinds Python 3.0 mogelijk om type-annotaties voor functies te gebruiken, maar dit waren slechts willekeurige expressies, zonder gedefinieerde syntaxis en semantiek. Tijdens de uitvoering van het programma werden deze annotaties voor het grootste deel eenvoudigweg genegeerd. Na de Hackweek zijn we begonnen met het standaardiseren van de semantiek. Dit werk leidde tot de opkomst PEP484 (Guido van Rossum, Łukasz Langa en ik hebben aan dit document samengewerkt).

Onze motieven konden van twee kanten worden bekeken. Ten eerste hoopten we dat het hele Python-ecosysteem een ​​gemeenschappelijke aanpak zou kunnen aannemen voor het gebruik van typehints (een term die in Python wordt gebruikt als het equivalent van "type-annotaties"). Dit zou, gezien de mogelijke risico's, beter zijn dan het gebruik van vele onderling onverenigbare benaderingen. Ten tweede wilden we type-annotatiemechanismen openlijk bespreken met veel leden van de Python-gemeenschap. Deze wens werd gedeeltelijk ingegeven door het feit dat we in de ogen van de brede massa Python-programmeurs niet als ‘afvalligen’ zouden willen lijken op basis van de basisideeën van de taal. Het is een dynamisch getypeerde taal, bekend als "duck-typering". In de gemeenschap kon er vanaf het allereerste begin een enigszins wantrouwige houding tegenover het idee van statisch typen ontstaan. Maar dat sentiment nam uiteindelijk af nadat duidelijk werd dat statisch typen niet verplicht zou zijn (en nadat mensen zich realiseerden dat het eigenlijk nuttig was).

De typehint-syntaxis die uiteindelijk werd aangenomen, leek sterk op wat mypy destijds ondersteunde. PEP 484 werd in 3.5 uitgebracht met Python 2015. Python was niet langer een dynamisch getypeerde taal. Ik beschouw deze gebeurtenis graag als een belangrijke mijlpaal in de geschiedenis van Python.

Begin van de migratie

Eind 2015 stelde Dropbox een team van drie mensen samen om aan mypy te werken. Onder hen waren Guido van Rossum, Greg Price en David Fisher. Vanaf dat moment begon de situatie zich extreem snel te ontwikkelen. Het eerste obstakel voor de groei van mypy waren prestaties. Zoals ik hierboven al aangaf, dacht ik er in de begindagen van het project over om de mypy-implementatie naar C te vertalen, maar dit idee werd voorlopig van de lijst geschrapt. We zaten vast aan het draaien van het systeem met behulp van de CPython-interpreter, die niet snel genoeg is voor tools als mypy. (Het PyPy-project, een alternatieve Python-implementatie met een JIT-compiler, heeft ons ook niet geholpen.)

Gelukkig zijn hier enkele algoritmische verbeteringen ons te hulp gekomen. De eerste krachtige ‘versneller’ was de implementatie van incrementele controle. Het idee achter deze verbetering was simpel: als alle afhankelijkheden van de module niet zijn veranderd sinds de vorige run van mypy, dan kunnen we de gegevens gebruiken die tijdens de vorige run in de cache zijn opgeslagen terwijl we met afhankelijkheden werken. We hoefden alleen typecontrole uit te voeren op de gewijzigde bestanden en op de bestanden die daarvan afhankelijk waren. Mypy ging zelfs nog een stap verder: als de externe interface van een module niet veranderde, ging mypy ervan uit dat andere modules die deze module importeerden, niet opnieuw gecontroleerd hoefden te worden.

Incrementele controle heeft ons enorm geholpen bij het annoteren van grote hoeveelheden bestaande code. Het punt is dat dit proces gewoonlijk veel iteratieve uitvoeringen van mypy met zich meebrengt, omdat annotaties geleidelijk aan de code worden toegevoegd en geleidelijk worden verbeterd. De eerste run van mypy verliep nog steeds erg traag omdat er veel afhankelijkheden moesten worden gecontroleerd. Om de situatie te verbeteren, hebben we vervolgens een mechanisme voor cachen op afstand geïmplementeerd. Als mypy detecteert dat de lokale cache waarschijnlijk verouderd is, downloadt het de huidige cache-momentopname voor de gehele codebase uit de gecentraliseerde repository. Vervolgens wordt een incrementele controle uitgevoerd op basis van deze momentopname. Dit heeft ons weer een grote stap gezet in de richting van het verbeteren van de prestaties van mypy.

Dit was een periode van snelle en natuurlijke adoptie van typecontrole bij Dropbox. Eind 2016 hadden we al ongeveer 420000 regels Python-code met typeannotaties. Veel gebruikers waren enthousiast over typecontrole. Steeds meer ontwikkelingsteams gebruikten Dropbox mypy.

Toen zag alles er goed uit, maar we hadden nog veel te doen. We zijn begonnen met het uitvoeren van periodieke interne gebruikersonderzoeken om de probleemgebieden van het project te identificeren en te begrijpen welke problemen eerst moeten worden opgelost (deze praktijk wordt vandaag de dag nog steeds gebruikt in het bedrijf). De belangrijkste waren, zoals duidelijk werd, twee taken. Ten eerste hadden we meer typedekking van de code nodig, en ten tweede hadden we mypy nodig om sneller te werken. Het was absoluut duidelijk dat ons werk om mypy te versnellen en in bedrijfsprojecten te implementeren nog lang niet voltooid was. Wij, ons volledig bewust van het belang van deze twee taken, gingen aan de slag om ze op te lossen.

Meer productiviteit!

Incrementele controles maakten mypy sneller, maar de tool was nog steeds niet snel genoeg. Veel aanvullende controles duurden ongeveer een minuut. De reden hiervoor was de cyclische import. Dit zal waarschijnlijk niemand verbazen die met grote codebases geschreven in Python heeft gewerkt. We hadden sets van honderden modules, die elk indirect alle andere importeerden. Als een bestand in een importlus werd gewijzigd, moest mypy alle bestanden in die lus verwerken, en vaak alle modules die modules uit die lus importeerden. Eén zo'n cyclus was de beruchte 'afhankelijkheidswirwar' die voor veel problemen bij Dropbox zorgde. Nadat deze structuur enkele honderden modules bevatte, werd deze, hoewel deze direct of indirect door veel tests werd geïmporteerd, ook gebruikt in productiecode.

We hebben de mogelijkheid overwogen om circulaire afhankelijkheden te ‘ontwarren’, maar we hadden niet de middelen om dat te doen. Er was te veel code waar we niet bekend mee waren. Naar aanleiding hiervan zijn wij tot een alternatieve aanpak gekomen. We hebben besloten om mypy snel te laten werken, zelfs als er sprake is van ‘afhankelijkheidsknopen’. We hebben dit doel bereikt met behulp van de mypy-daemon. Een daemon is een serverproces dat twee interessante mogelijkheden implementeert. Ten eerste slaat het informatie op over de gehele codebasis in het geheugen. Dit betekent dat elke keer dat u mypy uitvoert, u geen gegevens in de cache hoeft te laden die verband houden met duizenden geïmporteerde afhankelijkheden. Ten tweede analyseert hij zorgvuldig, op het niveau van kleine structurele eenheden, de afhankelijkheden tussen functies en andere entiteiten. Als de functie bijvoorbeeld foo roept een functie aan bar, dan is er sprake van een afhankelijkheid foo van bar. Wanneer een bestand verandert, verwerkt de daemon eerst afzonderlijk alleen het gewijzigde bestand. Vervolgens wordt gekeken naar extern zichtbare wijzigingen in dat bestand, zoals gewijzigde functiehandtekeningen. De daemon gebruikt alleen gedetailleerde informatie over importen om de functies te dubbelchecken die daadwerkelijk de gewijzigde functie gebruiken. Bij deze aanpak hoeft u doorgaans maar heel weinig functies te controleren.

Het implementeren van dit alles was niet eenvoudig, aangezien de oorspronkelijke mypy-implementatie sterk gericht was op het verwerken van één bestand tegelijk. We hadden te maken met veel grenssituaties, waarvan het optreden herhaalde controles vereiste in gevallen waarin er iets in de code veranderde. Dit gebeurt bijvoorbeeld wanneer aan een klasse een nieuwe basisklasse wordt toegewezen. Toen we eenmaal deden wat we wilden, konden we de uitvoeringstijd van de meeste incrementele controles terugbrengen tot slechts enkele seconden. Dit leek ons ​​een grote overwinning.

Nog meer productiviteit!

Samen met de caching op afstand die ik hierboven besprak, loste de mypy-daemon bijna volledig de problemen op die optreden wanneer een programmeur regelmatig typecontrole uitvoert en wijzigingen aanbrengt in een klein aantal bestanden. De systeemprestaties in de minst gunstige gebruikssituatie waren echter nog steeds verre van optimaal. Een schone start van mypy kan meer dan 15 minuten duren. En dit was veel meer dan waar we blij mee zouden zijn geweest. Elke week werd de situatie erger omdat programmeurs nieuwe code bleven schrijven en annotaties aan bestaande code toevoegden. Onze gebruikers hadden nog steeds honger naar meer prestaties, maar we waren blij ze halverwege te ontmoeten.

We besloten terug te keren naar een van de eerdere ideeën over mypy. Namelijk om Python-code naar C-code te converteren. Experimenteren met Cython (een systeem waarmee je in Python geschreven code naar C-code kunt vertalen) leverde ons geen zichtbare versnelling op, dus besloten we het idee nieuw leven in te blazen om onze eigen compiler te schrijven. Omdat de mypy-codebase (geschreven in Python) al alle benodigde type-annotaties bevatte, dachten we dat het de moeite waard zou zijn om te proberen deze annotaties te gebruiken om het systeem te versnellen. Ik heb snel een prototype gemaakt om dit idee te testen. Het toonde een meer dan tienvoudige prestatieverbetering op verschillende micro-benchmarks. Ons idee was om Python-modules naar C-modules te compileren met behulp van Cython, en typeannotaties om te zetten in runtime-typecontroles (meestal worden typeannotaties tijdens runtime genegeerd en alleen gebruikt door typecontrolesystemen). We waren eigenlijk van plan om de mypy-implementatie van Python te vertalen naar een taal die was ontworpen om statisch te worden getypt, die er precies zo uit zou zien (en voor het grootste deel zou werken) als Python. (Dit soort migratie tussen talen is een soort traditie geworden van het mypy-project. De oorspronkelijke mypy-implementatie is geschreven in Alore, daarna was er een syntactische hybride van Java en Python).

Focussen op de CPython-extensie-API was de sleutel om de projectmanagementmogelijkheden niet te verliezen. We hoefden geen virtuele machine of bibliotheken te implementeren die mypy nodig had. Bovendien zouden we nog steeds toegang hebben tot het hele Python-ecosysteem en alle tools (zoals pytest). Dit betekende dat we tijdens de ontwikkeling geïnterpreteerde Python-code konden blijven gebruiken, waardoor we konden blijven werken met een zeer snel patroon van het aanbrengen van codewijzigingen en het testen ervan, in plaats van te wachten tot de code was gecompileerd. Het leek erop dat we het uitstekend deden door als het ware op twee stoelen te zitten, en we vonden het geweldig.

De compiler, die we mypyc noemden (omdat deze mypy gebruikt als front-end voor het analyseren van typen), bleek een zeer succesvol project. Over het geheel genomen hebben we een snelheid van ongeveer 4x bereikt voor frequente mypy-runs zonder caching. Het ontwikkelen van de kern van het mypyc-project kostte een klein team van Michael Sullivan, Ivan Levkivsky, Hugh Hahn en mijzelf ongeveer vier kalendermaanden. Deze hoeveelheid werk was veel kleiner dan wat nodig zou zijn geweest om mypy te herschrijven, bijvoorbeeld in C++ of Go. En we hoefden veel minder wijzigingen in het project aan te brengen dan wanneer we het in een andere taal zouden herschrijven. We hoopten ook dat we mypyc naar een zodanig niveau konden brengen dat andere Dropbox-programmeurs het konden gebruiken om hun code te compileren en te versnellen.

Om dit prestatieniveau te bereiken, moesten we een aantal interessante technische oplossingen toepassen. De compiler kan dus veel bewerkingen versnellen door gebruik te maken van snelle C-constructies op laag niveau. Een gecompileerde functieaanroep wordt bijvoorbeeld vertaald in een C-functieaanroep. En zo'n oproep is veel sneller dan het aanroepen van een geïnterpreteerde functie. Bij sommige bewerkingen, zoals het opzoeken van woordenboeken, werd nog steeds gebruik gemaakt van reguliere C-API-aanroepen van CPython, die bij het compileren slechts marginaal sneller waren. We konden de extra belasting van het systeem, veroorzaakt door de interpretatie, elimineren, maar dit leverde in dit geval slechts een kleine winst op in termen van prestaties.

Om de meest voorkomende ‘langzame’ bewerkingen te identificeren, hebben we codeprofilering uitgevoerd. Gewapend met deze gegevens probeerden we mypyc aan te passen zodat het snellere C-code voor dergelijke bewerkingen zou genereren, of de overeenkomstige Python-code te herschrijven met behulp van snellere bewerkingen (en soms hadden we eenvoudigweg geen oplossing die eenvoudig genoeg was voor dat of een ander probleem). . Het herschrijven van de Python-code was vaak een eenvoudiger oplossing voor het probleem dan de compiler automatisch dezelfde transformatie te laten uitvoeren. Op de lange termijn wilden we veel van deze transformaties automatiseren, maar destijds waren we gefocust op het versnellen van mypy met minimale inspanning. En bij het bereiken van dit doel hebben we verschillende bochten genomen.

Wordt vervolgd ...

Beste lezers! Wat waren uw indrukken van het mypy-project toen u hoorde van het bestaan ​​ervan?

Het pad naar het typechecken van 4 miljoen regels Python-code. Deel 2
Het pad naar het typechecken van 4 miljoen regels Python-code. Deel 2

Bron: www.habr.com

Voeg een reactie