Sot publikojmë pjesën e dytë të përkthimit të materialit mbi mënyrën se si Dropbox organizoi kontrollin e tipeve për disa miliona rreshta kodi Python.
â
Mbështetja zyrtare për tipet (PEP 484)
Eksperimentet e para serioze me mypy në Dropbox i zhvilluam gjatë Hack Week 2014. Hack Week është një aktivitet njëjavor që organizohet nga Dropbox. Gjatë kësaj kohe, punonjësit mund të punojnë mbi çfarë të duan! Disa nga projektet më të njohura teknologjike të Dropbox kanë nisur pikërisht në ngjarje të tilla. Si rezultat i këtij eksperimenti, arritëm në përfundimin se mypy dukej premtues, megjithëse në atë kohë ky projekt ende nuk ishte gati për përdorim të gjerë.
NĂ« atĂ« kohĂ« po qarkullonte ideja e standardizimit tĂ« sistemeve tĂ« sugjerimeve pĂ«r tipet nĂ« Python. Siç e kam pĂ«rmendur edhe mĂ« parĂ«, qĂ« nga Python 3.0 mund tĂ« pĂ«rdoreshin anotimet e tipeve pĂ«r funksionet, por ato ishin thjesht shprehje arbitrare, pa sintaksĂ« dhe semantikĂ« tĂ« pĂ«rcaktuar. GjatĂ« ekzekutimit tĂ« programit, kĂ«to anotime, nĂ« pjesĂ«n mĂ« tĂ« madhe, thjesht injoroheshin. Pas Hack Week, ne filluam tĂ« punonim pĂ«r standardizimin e semantikĂ«s. Kjo punĂ« çoi nĂ« krijimin e (nĂ« kĂ«tĂ« dokument punuam sĂ« bashku Guido van Rossum, Ćukasz Langa dhe unĂ«).
Motivet tona mund të shiheshin nga dy këndvështrime. Së pari, shpresonim që i gjithë ekosistemi Python të mund të pranonte një qasje të përbashkët për përdorimin e type hints (term që përdoret në Python si ekuivalent i «anotimeve të tipeve»). Duke marrë parasysh rreziqet e mundshme, kjo do të ishte më mirë sesa përdorimi i shumë qasjeve të papajtueshme mes tyre. Së dyti, donim të diskutonim hapur mekanizmat e anotimit të tipeve me shumë përfaqësues të komunitetit Python. Pjesërisht, kjo dëshirë buronte nga fakti që nuk donim të dukeshim si «tradhtarë» të ideve themelore të gjuhës në sytë e masës së gjerë të programuesve Python. Kjo është një gjuhë me tipizim dinamik, e njohur për «duck typing». Në komunitet, që në fillim, ishte e pashmangshme të shfaqej një qëndrim disi i dyshimtë ndaj idesë së tipizimit statik. Por me kalimin e kohës kjo qasje u zbut, pasi u bë e qartë se tipizimi statik nuk planifikohej të bëhej i detyrueshëm (dhe pasi njerëzit e kuptuan se ai ishte vërtet i dobishëm).
Sintaksa e type hints që u miratua përfundimisht ishte shumë e ngjashme me atë që në atë kohë mbështetej nga mypy. Dokumenti PEP 484 u publikua së bashku me Python 3.5 në vitin 2015. Python nuk ishte më një gjuhë që mbështeste vetëm tipizimin dinamik. Më pëlqen ta shoh këtë ngjarje si një pikë kthese të rëndësishme në historinë e Python.
Fillimi i migrimit
Në fund të vitit 2015, në Dropbox u krijua një ekip prej tre personash për të punuar mbi mypy. Ai përbëhej nga Guido van Rossum, Greg Price dhe David Fisher. Që nga ai moment, zhvillimet nisën të përshpejtoheshin jashtëzakonisht shumë. Pengesa e parë në rrugën e rritjes së mypy ishte performanca. Siç e kam lënë të kuptohet më sipër, në fazën e hershme të projektit kisha menduar ta kaloja implementimin e mypy në gjuhën C, por ajo ide ishte lënë për momentin mënjanë. Mbetëm të kufizuar nga fakti që sistemi ekzekutohej nga interpretuesi CPython, i cili nuk ofron shpejtësinë e nevojshme për mjete si mypy. (As projekti PyPy, një implementim alternativ i Python me JIT compiler, nuk na ndihmoi.)
PĂ«r fat, kĂ«tu na ndihmuan disa pĂ«rmirĂ«sime algoritmike. âPĂ«rshpejtuesiâ i parĂ« i fuqishĂ«m ishte zbatimi i kontrollit inkremental. Ideja e kĂ«tij pĂ«rmirĂ«simi ishte e thjeshtĂ«: nĂ«se tĂ« gjitha varĂ«sitĂ« e modulit nuk kishin ndryshuar qĂ« nga ekzekutimi i mĂ«parshĂ«m i mypy, atĂ«herĂ« gjatĂ« punĂ«s me varĂ«sitĂ« mund tĂ« pĂ«rdornim tĂ« dhĂ«nat e ruajtura nĂ« cache nga sesioni i mĂ«parshĂ«m. Na duhej vetĂ«m tĂ« bĂ«nim kontrollin e tipeve nĂ« skedarĂ«t e ndryshuar dhe nĂ« ata skedarĂ« qĂ« vareshin prej tyre. Mypy shkoi edhe pak mĂ« tej: nĂ«se ndĂ«rfaqja e jashtme e modulit nuk kishte ndryshuar, mypy konsideronte se modulet e tjera qĂ« e importonin atĂ« modul nuk kishin nevojĂ« tĂ« kontrolloheshin sĂ«rish.
Kontrolli inkremental na ndihmoi shumë gjatë anotimit të vëllimeve të mëdha të kodit ekzistues. Arsyeja është se ky proces zakonisht përfshin shumë ekzekutime të përsëritura të mypy, sepse anotimet shtohen gradualisht në kod dhe përmirësohen hap pas hapi. Ekzekutimi i parë i mypy mbetej ende shumë i ngadaltë, sepse gjatë tij duhej të kontrolloheshin shumë varësi. Për ta përmirësuar këtë situatë, ne zbatuam një mekanizëm të cache-it në distancë. Nëse mypy zbulon se cache-i lokal ka shumë gjasa të jetë i vjetruar, ai shkarkon një snapshot aktual të cache-it për të gjithë bazën e kodit nga një depo qendrore. Më pas, duke përdorur këtë snapshot, ai kryen kontrollin inkremental. Kjo na çoi edhe një hap të rëndësishëm përpara në rritjen e performancës së mypy.
Kjo ishte një periudhë e përhapjes së shpejtë dhe natyrshme të sistemit të kontrollit të tipeve në Dropbox. Deri në fund të vitit 2016, ne kishim tashmë rreth 420000 rreshta kodi Python me anotime tipesh. Shumë përdorues e pritën me entuziazëm kontrollin e tipeve. Në Dropbox, gjithnjë e më shumë ekipe zhvilluesish po përdornin mypy.
Në atë kohë gjithçka dukej mirë, por na mbetej ende shumë punë për të bërë. Nisëm të zhvillonim sondazhe periodike të brendshme me përdoruesit për të identifikuar pikat problematike të projektit dhe për të kuptuar cilat çështje duhej të zgjidheshin të parat (kjo praktikë përdoret në kompani edhe sot). Siç u bë e qartë, më të rëndësishmet ishin dy detyra. E para ishte zgjerimi i mbulimit të kodit me tipe, e dyta ishte që mypy të punonte më shpejt. Ishte krejt e qartë se puna jonë për përshpejtimin e mypy dhe për ta integruar atë në projektet e kompanisë ishte ende larg përfundimit. Duke e kuptuar plotësisht rëndësinë e këtyre dy detyrave, iu përveshëm zgjidhjes së tyre.
Më shumë performancë!
Kontrollet inkrementale e pĂ«rshpejtuan mypy, por ky mjet ende nuk ishte mjaftueshĂ«m i shpejtĂ«. ShumĂ« kontrolle inkrementale zgjatnin rreth njĂ« minutĂ«. Shkaku i kĂ«saj ishin importet ciklike. Kjo, me gjasĂ«, nuk do tĂ« habisĂ« askĂ«nd qĂ« ka punuar me baza tĂ« mĂ«dha kodi tĂ« shkruara nĂ« Python. Kishim grupe me qindra module, ku secili prej tyre importonte tĂ«rthorazi tĂ« gjithĂ« tĂ« tjerĂ«t. NĂ«se ndryshohej ndonjĂ« skedar brenda ciklit tĂ« importeve, mypy duhej tĂ« pĂ«rpunonte tĂ« gjithĂ« skedarĂ«t qĂ« bĂ«nin pjesĂ« nĂ« atĂ« cikĂ«l dhe shpesh edhe çdo modul qĂ« importonte module nga ai cikĂ«l. NjĂ« nga kĂ«to cikle ishte ânyja e famshme e varĂ«siveâ, e cila u bĂ« shkak pĂ«r shumĂ« probleme nĂ« Dropbox. NĂ« njĂ« moment, kjo strukturĂ« pĂ«rfshinte disa qindra module; ndĂ«rkohĂ« importohej, drejtpĂ«rdrejt ose tĂ«rthorazi, nga shumĂ« teste dhe pĂ«rdorej edhe nĂ« kodin e prodhimit.
Shqyrtuam mundësinë e «zgjidhjes» së varësive ciklike, por nuk kishim burime të mjaftueshme për ta bërë këtë. Kishte tepër shumë kod me të cilin nuk ishim të njohur. Në fund, kaluam te një qasje alternative. Vendosëm ta bëjmë mypy të funksionojë shpejt edhe kur ka «nyje varësish». Këtë e arritëm me demonin mypy. Demoni është një proces serveri që ofron dy mundësi interesante. Së pari, ai mban në memorie informacion për të gjithë bazën e kodit. Kjo do të thotë se në çdo ekzekutim të mypy nuk ka nevojë të ngarkohen të dhënat e ruajtura në cache që lidhen me mijëra varësi të importuara. Së dyti, ai analizon me kujdes, deri në nivel njësish shumë të imta strukturore, varësitë midis funksioneve dhe entiteteve të tjera. Për shembull, nëse funksioni foo thërret funksionin bar, atëherë ekziston një varësi foo nga bar. Kur ndryshon një skedar, demoni fillimisht përpunon në izolim vetëm skedarin e ndryshuar. Më pas ai shqyrton ndryshimet e këtij skedari që janë të dukshme nga jashtë, si p.sh. nënshkrimet e funksioneve që kanë ndryshuar. Demoni përdor informacion të detajuar mbi importet vetëm për të rikontrolluar ato funksione që e përdorin realisht funksionin e ndryshuar. Zakonisht, me këtë qasje, duhen kontrolluar shumë pak funksione.
Zbatimi i gjithë kësaj nuk ishte i lehtë, pasi implementimi fillestar i mypy ishte shumë i orientuar drejt përpunimit të një skedari në të njëjtën kohë. Na u desh të përballeshim me shumë raste kufitare, shfaqja e të cilave kërkonte rikontrolle sa herë që ndryshonte diçka në kod. Për shembull, kjo ndodh kur një klase i caktohet një klasë bazë e re. Pasi arritëm atë që synonim, mundëm ta ulim kohën e ekzekutimit të shumicës së kontrolleve inkrementale në vetëm pak sekonda. Për ne kjo ishte një fitore e madhe.
Edhe më shumë performancë!
SĂ« bashku me cache-in nĂ« distancĂ«, pĂ«r tĂ« cilin fola mĂ« sipĂ«r, demoni mypy pothuajse i zgjidhi plotĂ«sisht problemet qĂ« shfaqeshin kur njĂ« programues e niste shpesh kontrollin e tipeve duke bĂ«rĂ« ndryshime vetĂ«m nĂ« pak skedarĂ«. MegjithatĂ«, performanca e sistemit nĂ« skenarin mĂ« pak tĂ« favorshĂ«m tĂ« pĂ«rdorimit ende ishte larg optimalit. NjĂ« nisje e pastĂ«r e mypy mund tĂ« zgjaste mbi 15 minuta. Kjo ishte shumĂ« mĂ« tepĂ«r nga sa do tĂ« na kĂ«naqte. Ădo javĂ« situata pĂ«rkeqĂ«sohej, pasi programuesit vazhdonin tĂ« shkruanin kod tĂ« ri dhe tĂ« shtonin annotime nĂ« kodin ekzistues. PĂ«rdoruesit tanĂ« ende kĂ«rkonin mĂ« shumĂ« performancĂ«, dhe ne ishim tĂ« gatshĂ«m tâua ofronim me kĂ«naqĂ«si.
VendosĂ«m tâi ktheheshim njĂ« prej ideve tĂ« hershme pĂ«r mypy: transformimit tĂ« kodit Python nĂ« kod C. Eksperimentet me Cython (njĂ« sistem qĂ« lejon pĂ«rkthimin e kodit tĂ« shkruar nĂ« Python nĂ« kod C) nuk sollĂ«n ndonjĂ« pĂ«rshpejtim tĂ« dukshĂ«m, ndaj vendosĂ«m tĂ« ringjallnim idenĂ« e krijimit tĂ« kompiluesit tonĂ«. MeqĂ« baza e kodit tĂ« mypy (e shkruar nĂ« Python) tashmĂ« pĂ«rmbante tĂ« gjitha annotimet e nevojshme tĂ« tipeve, na u duk me vlerĂ« tĂ« provonim pĂ«rdorimin e tyre pĂ«r tĂ« pĂ«rshpejtuar sistemin. Shpejt krijova njĂ« prototip pĂ«r ta testuar kĂ«tĂ« ide. Ai tregoi nĂ« mikro-benchmark-e tĂ« ndryshme njĂ« rritje tĂ« performancĂ«s prej mĂ« shumĂ« se 10 herĂ«. Ideja jonĂ« ishte tĂ« kompiloheshin modulet Python nĂ« module C me anĂ« tĂ« Cython dhe qĂ« annotimet e tipeve tĂ« shndĂ«rroheshin nĂ« kontrolle tipesh tĂ« kryera gjatĂ« ekzekutimit tĂ« programit (zakonisht annotimet e tipeve shpĂ«rfillen gjatĂ« ekzekutimit dhe pĂ«rdoren vetĂ«m nga sistemet e kontrollit tĂ« tipeve). NĂ« thelb, po planifikonim ta kalonim implementimin e mypy nga Python nĂ« njĂ« gjuhĂ« qĂ« ishte krijuar si statikisht e tipizuar, por qĂ« do tĂ« dukej (dhe, nĂ« pjesĂ«n mĂ« tĂ« madhe, do tĂ« funksiononte) saktĂ«sisht si Python. (Kjo lloj migrimi ndĂ«rmjet gjuhĂ«ve Ă«shtĂ« bĂ«rĂ« diçka si traditĂ« pĂ«r projektin mypy. Implementimi fillestar i mypy ishte shkruar nĂ« Alore, ndĂ«rsa mĂ« pas pati njĂ« hibrid sintaksor tĂ« Java dhe Python.)
Përqendrimi te API-ja e zgjerimeve të CPython ishte çelësi që të mos humbnim mundësitë e menaxhimit të projektit. Nuk na duhej të implementonim një makinë virtuale apo çfarëdo biblioteke që i nevojitej mypy. Për më tepër, e gjithë ekosistemi i Python do të mbetej ende i disponueshëm, po ashtu edhe të gjitha mjetet përkatëse (si p.sh. pytest). Kjo do të thoshte se gjatë zhvillimit mund të vazhdonim të përdornim kod Python të interpretuar, gjë që na lejonte të ruanim një cikël shumë të shpejtë të ndryshimeve në kod dhe testimit të tij, në vend që të prisnim kompilimin. Dukej sikur po arrinim me sukses të përfitonim nga të dyja qasjet njëkohësisht, dhe kjo na pëlqente.
Kompilatori që e quajtëm mypyc (sepse përdor mypy si frontend për analizën e tipeve) doli të ishte një projekt shumë i suksesshëm. Në përgjithësi, arritëm afërsisht një përshpejtim 4 herë më të madh në ekzekutimet e shpeshta të mypy pa përdorur cache. Zhvillimi i bërthamës së projektit mypyc i mori rreth 4 muaj kalendarikë një ekipi të vogël, ku bënin pjesë Michael Sullivan, Ivan Levkivskyi, Hugh Han dhe unë. Ky vëllim pune ishte dukshëm më i vogël se ai që do të kërkohej për ta rishkruar mypy, për shembull, në C++ ose Go. Edhe ndryshimet që na u desh të bënim në projekt ishin shumë më të pakta se ato që do të nevojiteshin po të rishkruhej në një gjuhë tjetër. Përveç kësaj, shpresonim se do ta çonim mypyc në një nivel të tillë që programues të tjerë në Dropbox të mund ta përdornin për të kompiluar dhe përshpejtuar kodin e tyre.
Për të arritur këtë nivel performance, na u desh të zbatonim disa zgjidhje interesante inxhinierike. Për shembull, kompajleri mund të përshpejtojë ekzekutimin e shumë operacioneve duke përdorur konstruksione të shpejta të nivelit të ulët në C. Për shembull, thirrja e një funksioni të kompiluar përkthehet në një thirrje të funksionit C. Dhe një thirrje e tillë ekzekutohet shumë më shpejt se thirrja e një funksioni të interpretuar. Disa operacione, si kërkimi në fjalorë, ende reduktoheshin në përdorimin e thirrjeve të zakonshme të C-API nga CPython, të cilat pas kompilimit rezultonin vetëm pak më të shpejta. Arritëm të eliminonim ngarkesën shtesë mbi sistemin të krijuar nga interpretimi, por në këtë rast kjo solli vetëm një përfitim të vogël në aspektin e performancës.
PĂ«r tĂ« identifikuar operacionet âtĂ« ngadaltaâ mĂ« tĂ« zakonshme, bĂ«mĂ« profilizimin e kodit. TĂ« pajisur me tĂ« dhĂ«nat e marra, u pĂ«rpoqĂ«m ose ta pĂ«rshtatnim mypyc nĂ« mĂ«nyrĂ« qĂ« tĂ« gjeneronte kod C mĂ« tĂ« shpejtĂ« pĂ«r operacione tĂ« tilla, ose tĂ« rishkruanim kodin pĂ«rkatĂ«s Python duke pĂ«rdorur operacione mĂ« tĂ« shpejta (dhe ndonjĂ«herĂ« thjesht nuk kishim njĂ« zgjidhje mjaftueshĂ«m tĂ« thjeshtĂ« pĂ«r njĂ« problem tĂ« caktuar). Rishkrimi i kodit Python shpesh rezultonte njĂ« zgjidhje mĂ« e lehtĂ« sesa zbatimi i ekzekutimit automatik tĂ« tĂ« njĂ«jtit transformim nĂ« kompajler. NĂ« planin afatgjatĂ«, donim tĂ« automatizonim shumĂ« prej kĂ«tyre transformimeve, por nĂ« atĂ« moment synonim tĂ« pĂ«rshpejtonim mypy me sa mĂ« pak pĂ«rpjekje. Dhe, duke lĂ«vizur drejt kĂ«tij qĂ«llimi, bĂ«mĂ« disa kompromise.
To be continued...
TĂ« nderuar lexues! ĂfarĂ« pĂ«rshtypjesh ju la projekti mypy nĂ« kohĂ«n kur mĂ«suat pĂ«r ekzistencĂ«n e tij?
Burimi: habr.com
