De weg naar typecontrole van 4 miljoen regels Python-code. Deel 2

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

De weg naar typecontrole van 4 miljoen regels Python-code. Deel 2

→ Lees het eerste deel

Officiƫle ondersteuning voor types (PEP 484)

We hebben onze eerste serieuze experimenten met mypy bij Dropbox uitgevoerd tijdens Hack Week 2014. Hack Week is een evenement dat Dropbox gedurende een week organiseert. Tijdens deze periode kunnen medewerkers aan alles werken wat ze willen! Enkele van de meest beroemde technologische projecten van Dropbox zijn begonnen tijdens dergelijke evenementen. Uit dit experiment hebben we afgeleid dat mypy veelbelovend lijkt, hoewel dit project nog niet klaar was voor gebruik op grote schaal.

In die tijd leefde het idee om systemen voor type hints in Python te standaardiseren. Zoals ik al zei, vanaf Python 3.0 konden typeannotaties voor functies worden gebruikt, maar dat waren slechts willekeurige uitdrukkingen, zonder specifieke syntaxis en semantiek. Tijdens de uitvoering van het programma werden deze annotaties meestal gewoon genegeerd. Na Hack Week zijn we begonnen met het standaardiseren van de semantiek. Dit werk leidde tot de ontwikkeling van PEP 484 (dit document is gezamenlijk opgesteld door Guido van Rossum, LukÔŔ Lana en mij).

Onze motivaties kunnen vanuit twee perspectieven worden bekeken. Ten eerste hoopten we dat het hele Python-ecosysteem een gemeenschappelijke aanpak zou kunnen aannemen voor het gebruik van type hints (type hints - een term die in Python wordt gebruikt als een equivalent voor 'typeannotaties'). Dit zou, rekening houdend met de mogelijke risico's, beter zijn dan het gebruik van vele wederzijds onverenigbare benaderingen. Ten tweede wilden we open discussie over typeannotatie-mechanismen met veel vertegenwoordigers van de Python-gemeenschap. Dit verlangen was deels ingegeven door het idee dat we niet als 'afvalligen' van de basisprincipes van de taal in de ogen van de bredere massa Python-ontwikkelaars wilden overkomen. Dit is een dynamisch getypeerde taal, bekend om 'duck typing'. In het begin was er in de gemeenschap een zekere achterdocht tegen het idee van statische type-annotatie. Maar die houding is uiteindelijk verzwakt, nadat het duidelijk werd dat statische type-annotatie niet verplicht zou worden gemaakt (en nadat mensen begrepen dat het daadwerkelijk nuttig was).

De uiteindelijk aangenomen syntaxis voor type hints leek veel op wat mypy op dat moment ondersteunde. Document PEP 484 kwam uit met Python 3.5 in 2015. Python was niet langer een taal die alleen dynamische typing ondersteunde. Ik beschouw dit evenement graag als een belangrijke mijlpaal in de geschiedenis van Python.

Start van de migratie

Eind 2015 werd er bij Dropbox een team van drie personen opgericht om aan mypy te werken. Dit team bestond uit Guido van Rossum, Greg Price en David Fisher. Vanaf dat moment begon de situatie zich zeer snel te ontwikkelen. Het eerste obstakel voor de groei van mypy was de prestaties. Zoals ik eerder al aangaf, dacht ik in de vroege fase van het project na over het omzetten van de implementatie van mypy naar de programmeertaal C, maar dit idee werd voorlopig van de lijst geschrapt. We zaten vast aan het feit dat we de CPython-interpreter gebruikten, die niet de snelheid biedt die nodig is voor tools zoals mypy. (Het PyPy-project, een alternatieve implementatie van Python met een JIT-compiler, hielp ons ook niet.)

Gelukkig kwamen er enkele algoritmische verbeteringen om ons te helpen. De eerste krachtige ā€˜versneller’ was de implementatie van incrementele controle. Het idee achter deze verbetering was eenvoudig: als alle afhankelijkheden van een module sinds de vorige uitvoering van mypy niet waren gewijzigd, konden we de tijdens de vorige sessie gecachete gegevens gebruiken bij het werken met deze afhankelijkheden. We hoefden alleen typecontroles uit te voeren in de gewijzigde bestanden en in de bestanden die daarvan afhankelijk waren. Mypy ging zelfs nog een stap verder: als de externe interface van de module niet was gewijzigd, beschouwde mypy het als onnodig om andere modules die deze module importeren opnieuw te controleren.

Incrementele controle hebben ons enorm geholpen bij het annoteren van grote hoeveelheden bestaande code. Dit proces omvat doorgaans vele iteratieve uitvoeringen van mypy, omdat annotaties geleidelijk aan de code worden toegevoegd en deze verbeteren. De eerste uitvoering van mypy was nog steeds erg traag, omdat er veel afhankelijkheden moesten worden gecontroleerd. Om de situatie te verbeteren, implementeerden we een mechanisme voor externe caching. Als mypy ontdekt dat de lokale cache waarschijnlijk verouderd is, laadt het de actuele snapshot van de cache voor de hele codebase uit een gecentraliseerd repository. Vervolgens voert het, met behulp van deze snapshot, de incrementele controle uit. Dit was weer een aanzienlijke stap in de richting van het verbeteren van de prestaties van mypy.

Het was een periode van snelle en natuurlijke implementatie van het type-controlesysteem bij Dropbox. Tegen het einde van 2016 hadden we al ongeveer 420.000 regels Python-code met type-annotaties. Veel gebruikers stonden enthousiast tegenover de type-controle. Bij Dropbox werd mypy steeds vaker gebruikt door verschillende ontwikkelteams.

Alles leek goed te gaan, maar we hadden nog veel werk te doen. We begonnen periodieke interne enquĆŖtes onder gebruikers uit te voeren om problemen binnen het project te identificeren en te begrijpen welke vragen eerst opgelost moesten worden (deze praktijk wordt nog steeds in het bedrijf toegepast). De twee belangrijkste taken bleken, zoals duidelijk werd, te zijn: een groter code-dekkingspercentage met types en een snellere werking van mypy. Het was volkomen duidelijk dat ons werk om mypy te versnellen en het in de bedrijfsprojecten te implementeren nog verre van voltooid was. Met het volle besef van het belang van deze twee taken, begonnen we deze aan te pakken.

Meer prestaties!

Incrementele controles hebben mypy versneld, maar dit hulpmiddel was nog steeds niet snel genoeg. Veel incrementele controles duurden ongeveer een minuut. De oorzaak hiervan waren cyclische importen. Dit zal waarschijnlijk niemand verbazen die met grote codebases in Python heeft gewerkt. We hadden sets van honderden modules, die allemaal indirect alle andere modules importeerden. Als een bestand in de cyclus van importen werd gewijzigd, moest mypy alle bestanden binnen die cyclus verwerken, en vaak ook alle modules die modules uit die cyclus importeerden. Een van zulke cycli was de beruchte 'spiraal van afhankelijkheden', die veel problemen veroorzaakte bij Dropbox. Op een gegeven moment bevatte deze structuur verschillende honderden modules, die zowel direct als indirect door veel tests werden geĆÆmporteerd en ook in de productiecode werden gebruikt.

We hebben overwogen om de cyclische afhankelijkheden 'uit te pluizen', maar we hadden daar niet de middelen voor. Er was te veel code waar we niet mee vertrouwd waren. Uiteindelijk kwamen we op een alternatief idee. We besloten mypy zo te laten functioneren dat het snel zou werken, zelfs met de 'spiraal van afhankelijkheden'. We bereikten dit doel met behulp van de mypy-daemon. De daemon is een serverproces dat twee interessante mogelijkheden biedt. Ten eerste - het houdt informatie over de hele codebase in het geheugen. Dit betekent dat mypy bij elke uitvoering de gecachete gegevens met betrekking tot duizenden geïmporteerde afhankelijkheden niet opnieuw hoeft te laden. Ten tweede - het analyseert nauwkeurig de afhankelijkheden tussen functies en andere entiteiten op het niveau van kleine structurele eenheden. Bijvoorbeeld, als een functie foo een functie aanroept bar, dan is er een afhankelijkheid foo van bar. Wanneer een bestand verandert, verwerkt de daemon eerst, in isolatie, alleen het gewijzigde bestand. Vervolgens kijkt hij naar de veranderingen in dat bestand die van buitenaf zichtbaar zijn, zoals gewijzigde functiehandtekeningen. De daemon gebruikt gedetailleerde informatie over importen alleen om de functies die daadwerkelijk de gewijzigde functie gebruiken opnieuw te verifiëren. Gewoonlijk hoeft bij deze aanpak slechts een klein aantal functies te worden gecontroleerd.

De implementatie hiervan was geen gemakkelijke taak, aangezien de oorspronkelijke implementatie van mypy sterk gericht was op het verwerken van ƩƩn bestand per keer. We moesten omgaan met veel randgevallen, waarvan de opkomst herhaalde controles vereiste wanneer er iets in de code veranderde. Dit gebeurt bijvoorbeeld wanneer een klasse een nieuwe basisklasse toegewezen krijgt. Nadat we bereikten wat we wilden, konden we de uitvoeringstijd van de meeste incrementele controles tot enkele seconden verlagen. Dit leek ons een grote overwinning.

Nog meer prestaties!

Samen met de externe caching waar ik eerder over sprak, heeft de mypy-daemon vrijwel volledig de problemen opgelost die zich voordoen wanneer een programmeur vaak typecontroles uitvoert en wijzigingen aanbrengt in een klein aantal bestanden. De systeemprestaties waren echter in de minst gunstige gebruikssituatie nog steeds verre van optimaal. Een schone mypy-start kon meer dan 15 minuten duren. Dit was veel meer dan we hadden gewild. Elke week verslechterde de situatie, omdat programmeurs bleven schrijven en annotaties aan bestaande code toevoegden. Onze gebruikers verlangden nog steeds naar meer prestaties, en wij waren graag bereid om hen tegemoet te komen.

We hebben besloten terug te keren naar een van de vroege ideeĆ«n over mypy. Namelijk — het omzetten van Python-code naar C-code. Experimenten met Cython (dit is een systeem dat code, geschreven in Python, omzet naar C-code) hebben ons niet het gewenste snelheidsvoordeel opgeleverd, dus hebben we besloten het idee op te pakken om onze eigen compiler te schrijven. Aangezien de codebasis van mypy (geschreven in Python) alle noodzakelijke type-annotaties al bevatte, leek het de moeite waard om te proberen deze annotaties te gebruiken om de werking van het systeem te versnellen. Ik heb snel een prototype gemaakt om dit idee te testen. Dit toonde meer dan tien keer betere prestaties op verschillende micro-benchmarks aan. Ons idee was om Python-modules te compileren naar C-modules met behulp van Cython en om type-annotaties om te zetten in type-controle die tijdens de uitvoering van het programma plaatsvinden (gewoonlijk worden type-annotaties genegeerd tijdens de uitvoering van programma's en alleen gebruikt door type-controlesystemen). We waren, feitelijk, van plan om de implementatie van mypy van Python naar een statisch getypeerde taal te vertalen, die eruit zou zien (en voor het grootste deel zou functioneren) zoals Python. (Deze vorm van intertaal migratie is iets als een traditie geworden voor het mypy-project. De oorspronkelijke implementatie van mypy was geschreven in Alore, daarna was er een syntactische hybride van Java en Python).

De focus op de CPython-extensie-API was cruciaal om geen mogelijkheden voor projectbeheer te verliezen. We hoefden geen virtuele machine of enige bibliotheken te implementeren die mypy nodig had. Bovendien was de volledige Python-ecosysteem voor ons nog steeds beschikbaar, evenals alle tools (zoals pytest). Dit betekende dat we door konden gaan met het gebruiken van geĆÆnterpreteerde Python-code tijdens de ontwikkeling, wat ons in staat stelde om door te gaan met een zeer snelle aanpak voor het aanbrengen van wijzigingen in de code en het testen ervan, in plaats van te wachten op codecompilatie. Het leek erop dat we inderdaad uitstekend in staat waren, zo te zeggen, om op twee stoelen tegelijk te zitten, en dat beviel ons.

De compiler die we mypyc hebben genoemd (aangezien hij mypy gebruikt voor type-analyse aan de voorkant), bleek een zeer succesvol project te zijn. Over het geheel genomen hebben we ongeveer 4 keer snellere uitvoeringen van mypy bereikt zonder caching. De ontwikkeling van de kern van het mypyc-project duurde ongeveer 4 kalendermaanden voor een klein team, bestaande uit Michael Sullivan, Ivan Levkivskiy, Hugh Han en mij. De benodigde inspanning was veel minder omvangrijk dan wat nodig zou zijn voor het herschrijven van mypy, bijvoorbeeld in C++ of Go. We hoefden ook veel minder wijzigingen in het project aan te brengen dan bij herschrijving in een andere taal. We hoopten bovendien dat we mypyc zo ver konden ontwikkelen dat andere programmeurs bij Dropbox het konden gebruiken om hun code te compileren en te versnellen.

Om een dergelijk prestatieniveau te bereiken, moesten we enkele interessante technische oplossingen toepassen. Zo kan de compiler de uitvoering van veel bewerkingen versnellen door gebruik te maken van snelle, laag-niveau C-constructies. Bijvoorbeeld, de aanroep van een gecompileerde functie wordt vertaald naar een aanroep van een C-functie. En zo'n aanroep verloopt veel sneller dan die van een geĆÆnterpreteerde functie. Sommige bewerkingen, zoals dictionary-zoekacties, hielden nog steeds in dat we gewone C-API-aanroepen vanuit CPython moesten gebruiken, die na compilatie slechts iets sneller waren. We konden de extra systeembelasting door interpretatie elimineren, maar dit resulteerde in dit geval slechts in een kleine prestatieverbetering.

Om de meest voorkomende 'langzame' bewerkingen te identificeren, hebben we codeprofilering uitgevoerd. Gewapend met de verkregen gegevens, hebben we geprobeerd om mypyc zo te 'tweaken' dat het snellere C-code genereert voor dergelijke bewerkingen, of om de betreffende Python-code te herschrijven met snellere operaties (en soms hadden we eenvoudigweg niet genoeg simpele oplossingen voor de een of andere uitdaging). Het herschrijven van Python-code bleek vaak een gemakkelijkere oplossing te zijn dan het realiseren van een automatische uitvoering van dezelfde transformatie in de compiler. Op de lange termijn wilden we veel van deze transformaties automatiseren, maar op dat moment waren we gericht op het versnellen van mypy met minimale inspanning. En terwijl we dit doel nastreefden, hebben we enkele hoeken afgesneden.

Wordt vervolgd…

Geachte lezers! Wat voor indruk maakte het mypy-project op je toen je er voor het eerst over hoorde?

De weg naar typecontrole van 4 miljoen regels Python-code. Deel 2
De weg naar typecontrole van 4 miljoen regels Python-code. Deel 2

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers šŸ”„ Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster