Rruga për të kontrolluar 4 milion rreshta të kodit Python. Pjesa 2

Sot po publikojmë pjesën e dytë të përkthimit të materialit se si Dropbox organizoi kontrollin e tipit për disa milionë rreshta të kodit Python.

Rruga për të kontrolluar 4 milion rreshta të kodit Python. Pjesa 2

Lexoni pjesën e parë

Mbështetje e tipit zyrtar (PEP 484)

Ne kryem eksperimentet tona të para serioze me mypy në Dropbox gjatë Hack Week 2014. Hack Week është një ngjarje njëjavore e organizuar nga Dropbox. Gjatë kësaj kohe, punonjësit mund të punojnë për çfarë të duan! Disa nga projektet më të famshme teknologjike të Dropbox filluan në ngjarje si këto. Si rezultat i këtij eksperimenti, arritëm në përfundimin se mypy duket premtues, megjithëse projekti nuk është ende gati për përdorim të gjerë.

Në atë kohë, ideja e standardizimit të sistemeve të aludimit të tipit Python ishte në ajër. Siç thashë, që nga Python 3.0 ishte e mundur të përdoreshin shënime të tipit për funksionet, por këto ishin thjesht shprehje arbitrare, pa sintaksë dhe semantikë të përcaktuar. Gjatë ekzekutimit të programit, këto shënime, në pjesën më të madhe, thjesht u injoruan. Pas Hack Week, filluam të punonim për standardizimin e semantikës. Kjo punë çoi në shfaqjen PEP 484 (Guido van Rossum, Łukasz Langa dhe unë bashkëpunuam në këtë dokument).

Motivet tona mund të shiheshin nga dy anë. Së pari, ne shpresuam që i gjithë ekosistemi Python të mund të adoptonte një qasje të përbashkët për përdorimin e sugjerimeve të tipit (një term i përdorur në Python si ekuivalent i "shënimeve të tipit"). Kjo, duke pasur parasysh rreziqet e mundshme, do të ishte më mirë sesa përdorimi i shumë qasjeve të papajtueshme reciprokisht. Së dyti, ne donim të diskutonim hapur mekanizmat e shënimeve të tipit me shumë anëtarë të komunitetit Python. Kjo dëshirë diktohej pjesërisht nga fakti se ne nuk do të donim të dukeshim si "apostate" nga idetë bazë të gjuhës në sytë e masave të gjera të programuesve Python. Është një gjuhë e shtypur në mënyrë dinamike, e njohur si "duke shtypur rosë". Në komunitet, që në fillim, nuk mund të mos lindte një qëndrim disi i dyshimtë ndaj idesë së shtypjes statike. Por kjo ndjenjë përfundimisht u zbeh pasi u bë e qartë se shtypja statike nuk do të ishte e detyrueshme (dhe pasi njerëzit e kuptuan se ishte në të vërtetë e dobishme).

Sintaksa e tipit aluzion që u miratua përfundimisht ishte shumë e ngjashme me atë që mypy mbështeti në atë kohë. PEP 484 u lëshua me Python 3.5 në 2015. Python nuk ishte më një gjuhë e shtypur në mënyrë dinamike. Më pëlqen ta mendoj këtë ngjarje si një moment historik të rëndësishëm në historinë e Python.

Fillimi i migrimit

Në fund të vitit 2015, Dropbox krijoi një ekip prej tre personash për të punuar në mypy. Ata përfshinin Guido van Rossum, Greg Price dhe David Fisher. Që nga ai moment, situata filloi të zhvillohej jashtëzakonisht shpejt. Pengesa e parë për rritjen e mypy ishte performanca. Siç e la të kuptohet më lart, në ditët e para të projektit mendova të përkthej zbatimin e mypy në C, por kjo ide u anulua nga lista për momentin. Ne ishim të mbërthyer me drejtimin e sistemit duke përdorur interpretuesin CPython, i cili nuk është mjaft i shpejtë për mjete si mypy. (Projekti PyPy, një zbatim alternativ i Python me një përpilues JIT, nuk na ndihmoi as.)

Për fat të mirë, disa përmirësime algoritmike na kanë ardhur në ndihmë këtu. "Përshpejtuesi" i parë i fuqishëm ishte zbatimi i kontrollit në rritje. Ideja pas këtij përmirësimi ishte e thjeshtë: nëse të gjitha varësitë e modulit nuk kanë ndryshuar që nga ekzekutimi i mëparshëm i mypy, atëherë ne mund të përdorim të dhënat e ruajtura në memorie gjatë ekzekutimit të mëparshëm gjatë punës me varësi. Na duhej vetëm të kryenim kontrollin e tipit në skedarët e modifikuar dhe në skedarët që vareshin prej tyre. Mypy madje shkoi pak më tej: nëse ndërfaqja e jashtme e një moduli nuk ndryshonte, mypy supozoi se modulet e tjera që importuan këtë modul nuk kishin nevojë të kontrolloheshin përsëri.

Kontrolli në rritje na ka ndihmuar shumë gjatë shënimit të sasive të mëdha të kodit ekzistues. Çështja është se ky proces zakonisht përfshin shumë ekzekutime përsëritëse të mypy pasi shënimet shtohen gradualisht në kod dhe përmirësohen gradualisht. Ekzekutimi i parë i mypy ishte ende shumë i ngadaltë sepse kishte shumë varësi për të kontrolluar. Më pas, për të përmirësuar situatën, ne kemi zbatuar një mekanizëm memorie në distancë. Nëse mypy zbulon se cache-i lokal ka të ngjarë të jetë i vjetëruar, ai shkarkon fotografinë aktuale të cache-it për të gjithë bazën e kodeve nga depoja e centralizuar. Më pas kryen një kontroll në rritje duke përdorur këtë fotografi. Kjo na ka bërë një hap tjetër të madh drejt rritjes së performancës së mypy.

Kjo ishte një periudhë e adoptimit të shpejtë dhe të natyrshëm të kontrollit të tipit në Dropbox. Deri në fund të vitit 2016, ne kishim tashmë rreth 420000 rreshta të kodit Python me shënime të tipit. Shumë përdorues ishin entuziastë për kontrollin e tipit. Gjithnjë e më shumë ekipe zhvillimi po përdornin Dropbox mypy.

Gjithçka dukej mirë atëherë, por ne kishim ende shumë për të bërë. Ne filluam të kryejmë anketa periodike të përdoruesve të brendshëm për të identifikuar fushat problematike të projektit dhe për të kuptuar se cilat çështje duhet të zgjidhen së pari (kjo praktikë përdoret ende sot në kompani). Më e rëndësishmja, siç u bë e qartë, ishin dy detyra. Së pari, na duhej më shumë mbulim i llojit të kodit, së dyti, na duhej mypy për të punuar më shpejt. Ishte absolutisht e qartë se puna jonë për të përshpejtuar mypy dhe për ta zbatuar atë në projektet e kompanisë ishte ende larg përfundimit. Ne, plotësisht të vetëdijshëm për rëndësinë e këtyre dy detyrave, u nisëm për zgjidhjen e tyre.

Më shumë produktivitet!

Kontrollet në rritje e bënë mypy më të shpejtë, por mjeti nuk ishte ende mjaft i shpejtë. Shumë kontrolle shtesë zgjatën rreth një minutë. Arsyeja për këtë ishin importet ciklike. Kjo ndoshta nuk do të befasojë askënd që ka punuar me baza të mëdha kodesh të shkruara në Python. Ne kishim grupe qindra modulesh, secila prej të cilave importonte në mënyrë indirekte të gjitha të tjerat. Nëse ndonjë skedar në një cikli importi ndryshohej, mypy duhej të përpunonte të gjithë skedarët në atë cikli, dhe shpesh çdo modul që importonte module nga ai cikli. Një cikël i tillë ishte "ngatërrimi i varësisë" famëkeq që shkaktoi shumë telashe në Dropbox. Pasi kjo strukturë përmbante disa qindra module, ndërkohë që importohej, drejtpërdrejt ose tërthorazi, shumë teste, përdorej edhe në kodin e prodhimit.

Shqyrtuam mundësinë e “zbërthimit” të varësive rrethore, por nuk kishim burime për ta bërë këtë. Kishte shumë kode me të cilat nuk ishim të njohur. Si rezultat, ne dolëm me një qasje alternative. Ne vendosëm ta bëjmë mypy të funksionojë shpejt edhe në prani të "ngatërresave të varësisë". Ne e arritëm këtë qëllim duke përdorur demonin mypy. Një demon është një proces serveri që zbaton dy veçori interesante. Së pari, ruan informacione për të gjithë bazën e kodit në memorie. Kjo do të thotë që sa herë që ekzekutoni mypy, nuk keni nevojë të ngarkoni të dhëna të ruajtura në memorie të fshehtë që lidhen me mijëra varësi të importuara. Së dyti, ai analizon me kujdes, në nivel të njësive të vogla strukturore, varësitë ndërmjet funksioneve dhe subjekteve të tjera. Për shembull, nëse funksioni foo thërret një funksion bar, atëherë ka një varësi foo nga bar. Kur një skedar ndryshon, daemon së pari, në izolim, përpunon vetëm skedarin e ndryshuar. Më pas shikon ndryshimet e jashtme të dukshme në atë skedar, siç janë nënshkrimet e funksioneve të ndryshuara. Daemon përdor informacion të detajuar rreth importeve vetëm për të kontrolluar dyfish ato funksione që përdorin aktualisht funksionin e modifikuar. Në mënyrë tipike, me këtë qasje, ju duhet të kontrolloni shumë pak funksione.

Zbatimi i gjithë kësaj nuk ishte i lehtë, pasi zbatimi origjinal i mypy ishte i fokusuar shumë në përpunimin e një skedari në një kohë. Na u desh të përballeshim me shumë situata kufitare, shfaqja e të cilave kërkonte kontrolle të përsëritura në rastet kur diçka ndryshonte në kod. Për shembull, kjo ndodh kur një klase i caktohet një klasë e re bazë. Pasi bëmë atë që dëshironim, ne ishim në gjendje të reduktonim kohën e ekzekutimit të shumicës së kontrolleve shtesë në vetëm disa sekonda. Kjo na dukej si një fitore e madhe.

Edhe më shumë produktivitet!

Së bashku me memorien në distancë që diskutova më lart, daemon mypy zgjidhi pothuajse plotësisht problemet që lindin kur një programues shpesh kryen kontrollin e tipit, duke bërë ndryshime në një numër të vogël skedarësh. Megjithatë, performanca e sistemit në rastin më pak të favorshëm të përdorimit ishte ende larg nga optimali. Një fillim i pastër i mypy mund të zgjasë më shumë se 15 minuta. Dhe kjo ishte shumë më tepër nga sa do të ishim të kënaqur. Çdo javë situata përkeqësohej pasi programuesit vazhduan të shkruanin kod të ri dhe të shtonin shënime në kodin ekzistues. Përdoruesit tanë ishin ende të uritur për më shumë performancë, por ne ishim të lumtur që i takuam në gjysmë të rrugës.

Ne vendosëm të kthehemi te një nga idetë e mëparshme në lidhje me mypy. Përkatësisht, për të kthyer kodin Python në kodin C. Eksperimentimi me Cython (një sistem që ju lejon të përktheni kodin e shkruar në Python në kodin C) nuk na dha ndonjë shpejtësi të dukshme, kështu që vendosëm të ringjallim idenë për të shkruar përpiluesin tonë. Meqenëse baza e kodit mypy (e shkruar në Python) përmbante tashmë të gjitha shënimet e nevojshme të tipit, menduam se do të ishte e vlefshme të përpiqeshim të përdornim këto shënime për të shpejtuar sistemin. Unë krijova shpejt një prototip për të testuar këtë ide. Ai tregoi një rritje më shumë se 10-fish të performancës në mikro-benchmarks të ndryshëm. Ideja jonë ishte të përpilonim modulet Python në modulet C duke përdorur Cython dhe t'i kthenim shënimet e tipit në kontrolle të tipit të kohës së ekzekutimit (zakonisht shënimet e tipit injorohen në kohën e ekzekutimit dhe përdoren vetëm nga sistemet e kontrollit të tipit ). Ne në fakt planifikuam të përktheshim zbatimin e mypy nga Python në një gjuhë që ishte krijuar për t'u shtypur në mënyrë statike, që do të dukej (dhe, në pjesën më të madhe, do të funksiononte) tamam si Python. (Ky lloj migrimi ndër-gjuhësh është bërë diçka si një traditë e projektit mypy. Implementimi origjinal i mypy u shkrua në Alore, më pas kishte një hibrid sintaksor të Java dhe Python).

Përqendrimi në API-në e zgjerimit CPython ishte çelësi për të mos humbur aftësitë e menaxhimit të projektit. Nuk kishim nevojë të implementonim një makinë virtuale ose ndonjë bibliotekë që kishte nevojë mypy. Për më tepër, ne do të kishim ende akses në të gjithë ekosistemin e Python dhe të gjitha mjetet (si p.sh. pytest). Kjo do të thotë që ne mund të vazhdojmë të përdorim kodin e interpretuar të Python gjatë zhvillimit, duke na lejuar të vazhdojmë të punojmë me një model shumë të shpejtë të ndryshimit të kodit dhe testimit të tij, në vend që të presim që kodi të përpilohet. Dukej sikur po bënim një punë të shkëlqyer duke u ulur në dy karrige, si të thuash, dhe na pëlqeu.

Përpiluesi, të cilin e quajtëm mypyc (pasi përdor mypy si front-end për analizimin e llojeve), doli të ishte një projekt shumë i suksesshëm. Në përgjithësi, kemi arritur përafërsisht 4x shpejtësi për ekzekutime të shpeshta mypy pa memorie. Zhvillimi i bërthamës së projektit mypyc mori një ekip të vogël të Michael Sullivan, Ivan Levkivsky, Hugh Hahn dhe mua rreth 4 muaj kalendarik. Kjo sasi pune ishte shumë më e vogël se sa do të ishte e nevojshme për të rishkruar mypy, për shembull, në C++ ose Go. Dhe ne duhej të bënim shumë më pak ndryshime në projekt sesa do të duhej të bënim kur e rishkruam atë në një gjuhë tjetër. Ne gjithashtu shpresonim që të mund ta çonim mypyc në një nivel të tillë që programuesit e tjerë të Dropbox të mund ta përdornin atë për të përpiluar dhe përshpejtuar kodin e tyre.

Për të arritur këtë nivel të performancës, na u desh të aplikonim disa zgjidhje inxhinierike interesante. Kështu, përpiluesi mund të përshpejtojë shumë operacione duke përdorur konstruksione të shpejta dhe të nivelit të ulët C. Për shembull, një thirrje funksioni e kompiluar përkthehet në një thirrje funksioni C. Dhe një thirrje e tillë është shumë më e shpejtë se thirrja e një funksioni të interpretuar. Disa operacione, të tilla si kërkimi i fjalorit, ende përfshinin përdorimin e thirrjeve të rregullta C-API nga CPython, të cilat ishin pak më të shpejta kur përpiloheshin. Ne ishim në gjendje të eliminonim ngarkesën shtesë në sistem të krijuar nga interpretimi, por kjo në këtë rast dha vetëm një fitim të vogël për sa i përket performancës.

Për të identifikuar operacionet më të zakonshme "të ngadalta", ne kryem profilizimin e kodit. Të armatosur me këto të dhëna, ne u përpoqëm ose të shkulnim mypyc në mënyrë që të gjeneronte kodin C më të shpejtë për operacione të tilla, ose të rishkruanim kodin përkatës të Python duke përdorur operacione më të shpejta (dhe nganjëherë thjesht nuk kishim një zgjidhje mjaft të thjeshtë për atë ose një problem tjetër) . Rishkrimi i kodit Python ishte shpesh një zgjidhje më e lehtë për problemin sesa që kompajleri të kryente automatikisht të njëjtin transformim. Në terma afatgjatë, ne donim të automatizonim shumë prej këtyre transformimeve, por në atë kohë ishim të fokusuar në përshpejtimin e mypy me përpjekje minimale. Dhe duke ecur drejt këtij qëllimi, ne premë disa kënde.

Vazhdon…

Të nderuar lexues! Cilat ishin përshtypjet tuaja për projektin mypy kur mësuat për ekzistencën e tij?

Rruga për të kontrolluar 4 milion rreshta të kodit Python. Pjesa 2
Rruga për të kontrolluar 4 milion rreshta të kodit Python. Pjesa 2

Burimi: www.habr.com

Shto një koment