El camí per comprovar 4 milions de línies de codi Python. Part 2

Avui publiquem la segona part de la traducció de material sobre com Dropbox va organitzar el control de tipus per a diversos milions de línies de codi Python.

El camí per comprovar 4 milions de línies de codi Python. Part 2

Llegeix la primera part

Suport de tipus oficial (PEP 484)

Vam realitzar els nostres primers experiments seriosos amb mypy a Dropbox durant la Hack Week 2014. La Hack Week és un esdeveniment d'una setmana organitzat per Dropbox. Durant aquest temps, els empleats poden treballar en el que vulguin! Alguns dels projectes tecnològics més famosos de Dropbox van començar en esdeveniments com aquests. Com a resultat d'aquest experiment, vam concloure que mypy sembla prometedor, tot i que el projecte encara no està preparat per a un ús generalitzat.

En aquell moment, la idea d'estandarditzar els sistemes d'indicacions de tipus Python estava a l'aire. Com he dit, des de Python 3.0 era possible utilitzar anotacions de tipus per a funcions, però aquestes eren només expressions arbitràries, sense sintaxi i semàntica definides. Durant l'execució del programa, aquestes anotacions es van ignorar, en la seva majoria, simplement. Després de la Hack Week, vam començar a treballar en l'estandardització de la semàntica. Aquest treball va provocar el sorgiment PEP 484 (Guido van Rossum, Łukasz Langa i jo vam col·laborar en aquest document).

Els nostres motius es podrien veure des de dues vessants. En primer lloc, esperàvem que tot l'ecosistema de Python pogués adoptar un enfocament comú per utilitzar suggeriments de tipus (un terme utilitzat a Python com a equivalent de "anotacions de tipus"). Això, donats els possibles riscos, seria millor que utilitzar molts enfocaments mútuament incompatibles. En segon lloc, volíem parlar obertament dels mecanismes d'anotació de tipus amb molts membres de la comunitat Python. Aquest desig va ser en part dictat pel fet que no voldríem semblar "apòstats" de les idees bàsiques del llenguatge als ulls de les grans masses de programadors de Python. És un llenguatge d'escriptura dinàmica, conegut com a "mecanografia d'ànec". A la comunitat, al principi, una actitud una mica sospitosa cap a la idea de la mecanografia estàtica no va poder evitar sorgir. Però aquest sentiment finalment va disminuir després que va quedar clar que l'escriptura estàtica no seria obligatòria (i després que la gent es va adonar que era realment útil).

La sintaxi de suggeriment de tipus que finalment es va adoptar era molt similar a la que mypy suportava en aquell moment. PEP 484 es va llançar amb Python 3.5 el 2015. Python ja no era un llenguatge escrit dinàmicament. M'agrada pensar en aquest esdeveniment com una fita significativa en la història de Python.

Inici de la migració

A finals de 2015, Dropbox va crear un equip de tres persones per treballar a mypy. Inclouen Guido van Rossum, Greg Price i David Fisher. A partir d'aquell moment, la situació va començar a desenvolupar-se molt ràpidament. El primer obstacle per al creixement de mypy va ser el rendiment. Com vaig suggerir més amunt, en els primers dies del projecte vaig pensar a traduir la implementació mypy a C, però aquesta idea s'ha eliminat de la llista de moment. Ens vam quedar atrapats amb l'execució del sistema amb l'intèrpret CPython, que no és prou ràpid per a eines com mypy. (El projecte PyPy, una implementació alternativa de Python amb un compilador JIT, tampoc ens va ajudar.)

Afortunadament, aquí ens han ajudat algunes millores algorítmiques. El primer "accelerador" potent va ser la implementació de la comprovació incremental. La idea darrere d'aquesta millora era senzilla: si totes les dependències del mòdul no han canviat des de l'execució anterior de mypy, podem utilitzar les dades guardades a la memòria cau durant l'execució anterior mentre treballem amb dependències. Només ens calia realitzar una comprovació de tipus als fitxers modificats i als fitxers que depenien d'ells. Mypy fins i tot va anar una mica més enllà: si la interfície externa d'un mòdul no canviava, mypy suposava que no calia tornar a comprovar altres mòduls que importaven aquest mòdul.

La comprovació incremental ens ha ajudat molt quan anotem grans quantitats de codi existent. La qüestió és que aquest procés sol implicar moltes execucions iteratives de mypy a mesura que les anotacions s'afegeixen gradualment al codi i es milloren gradualment. La primera execució de mypy encara va ser molt lenta perquè tenia moltes dependències per comprovar. Aleshores, per millorar la situació, vam implementar un mecanisme de memòria cau remota. Si mypy detecta que és probable que la memòria cau local estigui obsoleta, baixa la instantània de la memòria cau actual per a tota la base de codi des del dipòsit centralitzat. A continuació, realitza una comprovació incremental mitjançant aquesta instantània. Això ens ha fet un gran pas més per augmentar el rendiment de mypy.

Aquest va ser un període d'adopció ràpida i natural de la comprovació de tipus a Dropbox. A finals de 2016, ja teníem aproximadament 420000 línies de codi Python amb anotacions de tipus. Molts usuaris estaven entusiasmats amb la comprovació de tipus. Cada cop més equips de desenvolupament feien servir Dropbox mypy.

Aleshores tot semblava bé, però encara ens quedava molt per fer. Vam començar a realitzar enquestes internes periòdiques als usuaris per tal d'identificar les àrees problemàtiques del projecte i entendre quins problemes cal resoldre primer (aquesta pràctica encara s'utilitza a l'empresa avui dia). El més important, com va quedar clar, eren dues tasques. En primer lloc, necessitàvem més cobertura de tipus del codi, en segon lloc, necessitàvem que mypy funcionés més ràpidament. Estava absolutament clar que el nostre treball per accelerar mypy i implementar-lo als projectes de l'empresa encara estava lluny d'haver acabat. Nosaltres, plenament conscients de la importància d'aquestes dues tasques, ens vam posar a resoldre'ls.

Més productivitat!

Les comprovacions incrementals van fer que mypy fos més ràpid, però l'eina encara no era prou ràpida. Moltes comprovacions incrementals van durar aproximadament un minut. El motiu d'això van ser les importacions cícliques. Això probablement no sorprendrà a ningú que hagi treballat amb grans bases de codi escrites en Python. Teníem conjunts de centenars de mòduls, cadascun dels quals importava indirectament tots els altres. Si es canviava algun fitxer d'un bucle d'importació, mypy havia de processar tots els fitxers d'aquest bucle, i sovint qualsevol mòdul que importava mòduls d'aquest bucle. Un d'aquests cicles va ser el famós "embolic de dependència" que va causar molts problemes a Dropbox. Una vegada que aquesta estructura contenia diversos centenars de mòduls, mentre s'importava, directa o indirectament, moltes proves, també s'utilitzava en codi de producció.

Ens vam plantejar la possibilitat de “desencallar” dependències circulars, però no teníem els recursos per fer-ho. Hi havia massa codi que no estàvem familiaritzats. Com a resultat, vam plantejar un enfocament alternatiu. Vam decidir fer que mypy funcionés ràpidament fins i tot en presència d'"embolics de dependència". Hem aconseguit aquest objectiu utilitzant el dimoni mypy. Un dimoni és un procés de servidor que implementa dues capacitats interessants. En primer lloc, emmagatzema informació sobre tota la base de codis a la memòria. Això vol dir que cada vegada que executeu mypy, no haureu de carregar dades a la memòria cau relacionades amb milers de dependències importades. En segon lloc, analitza acuradament, a nivell de petites unitats estructurals, les dependències entre funcions i altres entitats. Per exemple, si la funció foo crida una funció bar, llavors hi ha una dependència foo d' bar. Quan un fitxer canvia, el dimoni primer, de manera aïllada, processa només el fitxer modificat. A continuació, examina els canvis visibles externament en aquest fitxer, com ara les signatures de funcions canviades. El dimoni utilitza informació detallada sobre les importacions només per comprovar les funcions que realment utilitzen la funció modificada. Normalment, amb aquest enfocament, heu de comprovar molt poques funcions.

Implementar tot això no va ser fàcil, ja que la implementació mypy original es va centrar molt en processar un fitxer a la vegada. Vam haver de fer front a moltes situacions límit, l'ocurrència de les quals requeria comprovacions repetides en els casos en què alguna cosa canviava al codi. Per exemple, això passa quan a una classe se li assigna una nova classe base. Un cop vam fer el que volíem, vam poder reduir el temps d'execució de la majoria de comprovacions incrementals a només uns segons. Això ens va semblar una gran victòria.

Encara més productivitat!

Juntament amb l'emmagatzematge a la memòria cau remot que he comentat anteriorment, el dimoni mypy va resoldre gairebé completament els problemes que sorgeixen quan un programador executa sovint la comprovació de tipus, fent canvis en un petit nombre de fitxers. Tanmateix, el rendiment del sistema en el cas d'ús menys favorable encara estava lluny de ser òptim. Un inici net de mypy pot trigar més de 15 minuts. I això va ser molt més del que hauríem estat contents. Cada setmana la situació empitjorava a mesura que els programadors continuaven escrivint codi nou i afegint anotacions al codi existent. Els nostres usuaris encara tenien gana de més rendiment, però vam estar contents de conèixer-los a mig camí.

Vam decidir tornar a una de les idees anteriors sobre mypy. És a dir, per convertir codi Python en codi C. Experimentar amb Cython (un sistema que permet traduir codi escrit en Python a codi C) no ens va donar cap acceleració visible, així que vam decidir reviure la idea d'escriure el nostre propi compilador. Com que la base de codi mypy (escrita en Python) ja contenia totes les anotacions de tipus necessàries, vam pensar que valdria la pena intentar utilitzar aquestes anotacions per accelerar el sistema. Ràpidament vaig crear un prototip per provar aquesta idea. Va mostrar un augment de més de 10 vegades en el rendiment en diversos micropunts de referència. La nostra idea era compilar mòduls de Python a mòduls C mitjançant Cython i convertir les anotacions de tipus en comprovacions de tipus en temps d'execució (normalment les anotacions de tipus s'ignoren en temps d'execució i només s'utilitzen els sistemes de verificació de tipus). De fet, teníem previst traduir la implementació mypy de Python a un llenguatge dissenyat per escriure estàticament, que semblaria (i, en la seva majoria, funcionarà) exactament com Python. (Aquest tipus de migració entre llengües s'ha convertit en una mena de tradició del projecte mypy. La implementació original de mypy es va escriure a Alore, després hi havia un híbrid sintàctic de Java i Python).

Centrar-se en l'API d'extensió CPython va ser clau per no perdre les capacitats de gestió de projectes. No calia implementar una màquina virtual o cap biblioteca que mypy necessitava. A més, encara tindríem accés a tot l'ecosistema de Python i a totes les eines (com ara pytest). Això significava que podríem seguir utilitzant codi Python interpretat durant el desenvolupament, la qual cosa ens va permetre seguir treballant amb un patró molt ràpid de fer canvis de codi i provar-lo, en lloc d'esperar que el codi es compile. Semblava que estàvem fent un gran treball asseguts en dues cadires, per dir-ho d'alguna manera, i ens va encantar.

El compilador, que vam anomenar mypyc (ja que utilitza mypy com a front-end per analitzar tipus), va resultar ser un projecte molt reeixit. En general, vam aconseguir una acceleració aproximadament 4x per a les execucions freqüents de mypy sense memòria cau. El desenvolupament del nucli del projecte mypyc va prendre un petit equip de Michael Sullivan, Ivan Levkivsky, Hugh Hahn i jo mateix durant uns 4 mesos naturals. Aquesta quantitat de treball era molt menor del que hauria estat necessari per reescriure mypy, per exemple, en C++ o Go. I hem hagut de fer molts menys canvis al projecte dels que hauríem hagut de fer en reescriure-lo en un altre idioma. També esperàvem que poguéssim portar mypyc a un nivell tal que altres programadors de Dropbox el poguessin utilitzar per compilar i accelerar el seu codi.

Per aconseguir aquest nivell de rendiment, hem hagut d'aplicar algunes solucions d'enginyeria interessants. Així, el compilador pot accelerar moltes operacions utilitzant construccions C ràpides i de baix nivell. Per exemple, una trucada de funció compilada es tradueix a una crida de funció C. I aquesta trucada és molt més ràpida que cridar una funció interpretada. Algunes operacions, com ara les cerques de diccionari, encara implicaven l'ús de trucades C-API habituals de CPython, que només eren una mica més ràpides quan es compilaven. Vam poder eliminar la càrrega addicional del sistema creada per la interpretació, però això en aquest cas només va donar un petit guany en termes de rendiment.

Per identificar les operacions "lentes" més habituals, vam realitzar un perfil de codi. Armats amb aquestes dades, vam intentar ajustar mypyc perquè generi un codi C més ràpid per a aquestes operacions, o reescriure el codi Python corresponent mitjançant operacions més ràpides (i de vegades simplement no teníem una solució prou senzilla per a aquest o un altre problema) . Reescriure el codi de Python sovint era una solució més fàcil al problema que fer que el compilador realitzés automàticament la mateixa transformació. A llarg termini, volíem automatitzar moltes d'aquestes transformacions, però en aquell moment ens vam centrar a accelerar mypy amb el mínim esforç. I en avançar cap a aquest objectiu, vam tallar diverses cantonades.

Continuar ...

Benvolguts lectors! Quines impressions vau tenir sobre el projecte mypy quan vau saber de la seva existència?

El camí per comprovar 4 milions de línies de codi Python. Part 2
El camí per comprovar 4 milions de línies de codi Python. Part 2

Font: www.habr.com

Afegeix comentari