In prima parte a discutiei noastre cu Mihai am dezbatut relatia dintre hardware si software precum si challenge-uri si limitari ale hardware-ului curent. Apoi in partea a doua am continuat discutia concentrandu-ne mai mult pe software, cu trimiteri la starea pietii de software din Romania (si nu numai), provocari ale acestui domeniu, arhitectura software si pasi in procesul de dezvoltare. Revenim cu a treia parte a interviului despre performanta software-ului si optimizari.
Techtorials: Care sunt provocarile in cresterea performantei software ?
Crearea de software este o activitate complexa si costisitoare. Nu doar dezvoltarea propriu-zisa conteaza, dar si costul de “mentenanta”, adica costul de a adauga o noua functionalitate sau a modifica o functionalitate existenta.
Daca ne uitam retrospectiv la sistemele de operare Windows ’95, ’98, XP, Vista vom observa ca nu sunt chiar total diferite. In ciuda unor diferente de aspect, vom gasi inca multe similaritati care ne fac sa tragem concluzia ca fiecare versiune este doar o modificare a versiunii precedente. Bineinteles ca multe bucati sunt inlocuite complet, dar foarte rar o noua versiune a unui soft este rescrisa total. Aplicatiile mari dureaza ani de dezvoltare, un sistem de operare rescris complet ar fi deja “depasit” in momentul in care ar fi finalizat. “Softul se face din soft”, deci firmele sacrifica mult din performanta pentru a micsora costuri viitoare de mentenanta.
Mai apare si nevoia de compatibilitate. Uneori o versiune mai putin eficienta este pastrata doar pentru ca … programele vechi au nevoie de ea pentru a rula. E uimitor ca multe aplicatii care rulau pe Windows ’95 pot rula inca pe ultimele versiuni Windows.
Techtorials: Care este relatia dintre performanta software si costul de mentenanta ?
Un exemplu clasic este “problema anului 2000” care a adus costuri enorme pentru o problema aparent simpla: din economie de resurse, multe softuri scrise inainte de anul 2000 stocau doar ultimele 2 cifre ale anului. Se considera implicit ca primele doua cifre ale anului sunt “19”. Anul 2000 devenea insa reprezentat ca “00”, anul afisat devening “1900”. Aceasta nu este singura problema, diverse functionalitati care comparau anii ajungeau sa considere ca informatiile din anul 2000 erau mai vechi decat cele din 1999. Foarte multe functionalitati au trebuit rescrise pentru a putea lucra cu anul reprezentat pe 4 cifre.
In acest moment se foloseste pe scara larga reprezentarea data+timp ca numar de secunde trecute de la un anumit moment de referinta (pe sistemele tip Unix se foloseste anul 1970). Aceasta reprezentare nu are problema anului 2000, dar va avea problema anului 2038 cand contorul de timp se va “da peste cap” luand-o de la zero. Se trece deja la reprezentari pe 64 biti care vor rezolva problema pentru o perioada “ceva mai lunga” – 292 miliarde ani.
http://en.wikipedia.org/wiki/Year_2038_problem
Cu mintea de acum … am putea sa ne intrebam de ce n-au fost proiectate sistemele direct cu timpul reprezentat pe 64 biti ? Trebuie insa sa ne amintim ca in momentul proiectarii majoritatea sistemelor functionau pe maxim 32 de biti, unele chiar mai putin (16 sau 8 biti). O reprezentare pe 32 de biti ar fi insemnat cel putin de 3 ori mai mult efort de procesare si bineinteles de doua ori mai mult spatiu de memorie volatila (RAM) si stocare persistenata (Hard disk, benzi magnetice). Intotdeauna se face un compromis intre performanta si scalabilitate in viitor.
Techtorials: Relatia este insa si inversa: din nevoia reducerii costului de mentenanta, performanta are de suferit.
Am vazul in exemplul anterior ca software-ul trebuie sa fie pregatit continuu sa raspunda unor provocari care cer reprogramarea anumitor parti. Modularizarea si abstractizarea incearca sa izoleze astfel de viitoare modificari intr-o zona cat mai mica de program, dar asta creeaza nivele aditionale de comunicare, care au efect negativ asupra performantei. Este ca o firma in care orice decizie trebuie sa treaca prin multe nivele de decizie, fiecare nivel adaugand un timp suplimentar de procesare.
Practic, insasi limbajele de programare de nivel inalt (precum C, C++, Java) aduc cu ele pe langa costul mai mic de dezvoltare o anumita scadere a performantei. Practic, un algoritm se poate exprima optim in limbajul masina al procesorului in care ruleaza (care are asociat un “limbaj de asamblare”). In momentul in care incepem sa folosim “prefabricate” software mai complexe, este foarte probabil sa nu folosim minimul de instructiuni necesare. Bineinteles acest dezavantaj merita daca viteza de scriere a programului creste de 10-100 ori pentru probleme mari. Au ramas doar cateva domenii in care diferenta de performata justifica efectiv programarea la nivel de “limbaj de asamblare”, precum micro-controllere.
Limbajul de programare are insa o insemnatate destul de mica fata de alti factori. Am vorbit despre eficienta algoritmului de rezolvare a problemei intr-un episod anterior. Odata ales insa algoritmul cu cel mai bun raport Eficienta/Simplitate, mai exista cativa factori care impacteaza performanta in functie de modul de compilare (traducere din limbaj de programare in limbaj “masina”).
Tipurile de date:
De obicei procesoarele efectueaza cel mai rapid operatii cu numere intregi. Calculul cu numere zecimale (“cu virgula mobila”, float) dureaza mult mai mult. Insasi unitatea din procesor este mult mai complexa, de aceea procesoarele actuale multicore pot veni cu multe unitati de procesare “intregi”, dar cu o singura unitate de procesare in virgula mobila. Asta inseamna ca programul care foloseste multe calcule in virgula mobila nu va putea sa foloseasca mai multe nuclee in paralel.
Uneori solutia este foarte simpla: in loc sa folosesti numere cu virgula (10.20 RON), poti stoca centi (1020 bani). Este nevoie doar la afisare sa se puna virgula la locul dorit. Standardul international este stocarea in miimi de centi (10^5), pentru a nu fi nevoie de rotunjiri prea semnificative la dobanda de exemplu.
O mica paranteza mai tehnica, apropos de reprezentarea numerelor in virgula mobila: este chiar foarte periculos sa stochezi bani in virgula mobila. Ai putea crede ca in virgula mobila, 0.1 * 10 = 1, dar nu este exact asa. De fapt din 0.1 * 10 va rezulta 0.999999999…. De obicei programele rotunjesc si vor afisa 1, dar dupa multe operatii eroarea poate deveni semnificativa.
Explicatie: Calculatorul stocheaza numerele in virgula mobila folosing un intreg inmultit cu o putere a lui 2 (inclusiv negative). Daca in zecimal am spune 0.1 = 1 * 10^-1, in binar va trebui sa folosim puteri ale lui 2. La fel cum 1/3 = 0.33333…. are perioada si nu se poate reprezenta in zecimal ca si intreg*putere_a_lui_10, nici 0.10 nu se poate reprezenta exact ca intreg*putere_a_lui_2. De aici rezulta o eroare de reprezentare, astfel ca inmultirea cu 10 nu va mai da exact 1.
Arhitectura pentru care se compileaza programul (unde este cazul)
Diversele procesoarele au un set de instructiuni comune, dar si instructiuni specifice. Un program care doreste sa fie compatibil cu toate procesoarele incepand cu Pentium I ar trebui sa foloseasca doar instructiuni I386. Bineinteles ca asta inseamna ca programul nu va beneficia de instructiunile mai puternice ale procesoarelor avansate, ci doar de viteza sporita a instructiunilor de baza. Pe de alta parte, un program compilat cu toate optimizarile unui procesor specific, probabil nu va rula de loc pe un alt procesor. Este destul de complicat de facut un program sa decida sa foloseasca anumite instructiuni doar daca sunt disponibile. Practic trebuie pusa o instructiune suplimentara care redirecteaza programul la versiunea neoptimizata daca detecteaza ca procesorul nu suporta acea optiune. Asta de obicei se programeaza la nivel de asamblare si este facuta doar pentru aplicatii foarte CPU-intensive, precum programele de encodare video.
Programele compilate pentru 64biti nu vor functiona pe sisteme de operare in 32 biti. Pentru compatibilitate, majoritatea programelor vin compilate in 32 biti, pentru ca pot rula oricum si pe sisteme de 64 biti. Ca si consecinta, calculele in 64 biti vor dura ceva mai mult decat optimul. Totusi, calculele in 32 biti vor dura aproximativ la fel, poate chiar un pic mai mult. Explicatia vine din faptul ca sistemul va citi oricum 64 biti chiar daca are nevoie doar de 32 biti. Durata citirii este identica deci pentru 32 si 64 biti, deoarece magistrala este tot pe 64 biti. Totusi, toate adresele fiind pe 64 biti, cache-ul procesorului se umple mai repede, deci va fi ca si cand procesorul are cache mai mic. Mai apare si o dimensiune mai mare a programului din compilarea in 64 biti, salturile “in afara cache” devin mai dese. Totusi, pentru a putea accesa mai multe de 4GB RAM, aplicatiile si sistemele de operare trebuie sa treaca in 64 biti. In realitate limita pe sistemele de operare de 32 biti este chiar mai mica uneori, de 2-3GB RAM.
Optimizarile sistemului de operare
Sistemul de operare are si el un impact asupra performantei softului. In primul rand sistemul de operare decide modul in care va rula procesorul (32 biti sau 64 biti). Daca procesorul suporta operare in 64 biti dar sistemul de operare opereaza in mod 32 biti, nici programul nu va putea folosi instructiunile de 64 biti. Intrucat sistemele de operare in 32 biti functioneaza si pe 64 biti, dar nu si invers, sistemele de operare in 64 biti sunt cele care au specificat explicit asta in nume. Celelalte se poate considera ca lucreaza in 32 biti.
Din pacate instalarea unui sistem de 64 biti aduce limitari – driverele clasice de 32 biti nu mai functioneaza. Programele in 32 biti functioneaza ok, dar anumite componente hardware de cativa ani vechime ar putea sa nu mai poata fi folosite. Trebuie evaluata cu atentie trecerea la un sistem de operare pe 64 biti, mai ales ca programele nu vor functiona mult mai repede daca nu sunt si ele compilate pentru 64 biti.
Anumite parti de program folosesc serviciile sistemului de operare. Daca sistemul de operare lucreaza mai repede, ar putea rezulta si o crestere de performanta a programului. Totusi, majoritatea cererilor catre sistemul de operare se refera la accessul spre periferice lente precum hard-disk, deci faptul ca sistemul de operare poate calcula mai repede s-ar putea sa nu aduca mari avantaje.
Optmizari la compilare
In momentul compilarii, compilatorul poate face anumite optimizari. De obicei cresterea performantei se realizeaza prin cresterea consumului de memorie, dar nu numai. Compilatorul poate incerca sa gaseasca pentru mai multe instructiuni in limbaj de programare, o singura instructiune mai cuprinzatoare. Majoritatea compilatoarelor au diferite nivele de optimizare, un nivel mai mare face insa ca aceasta compilare sa dureze mai mult, si nu aduce intotdeauna beneficii vizibile.
Pentru calcule matematice intensive am vazut si cresteri de 10 ori a vitezei de calcul intre acelasi program compilat cu compilatorul default al sistemului si un compilator “comercial” cu optimizarea maxima activata. De obicei insa optimizarile nici macar nu dubleaza viteza programului.
O situatie interesanta apare in cazul limbajelor care se compileaza in “bytecode” precum Java si C#. Acest bytecode ruleaza in “masina virtuala”. La rulare, bytecode-ul se traduce in instructiuni masina pentru masina curenta (procesul se numeste compilare “Just in time” = JIT). Teoretic asta face ca acelasi bytecode sa poata fi transformat in cel mai eficient cod posibil pentru masina curenta. Sa spunem ca un program Java a fost compilat si testat pe o platforma i386 inainte de aparitia procesoarelor i386 pe 64 biti. Daca programul foloseste date pe 64 biti, pe un procesor de 32 biti el emuleaza operatia din mai multe instructiuni pe 32 biti. Fara a fi recompilat, daca este rulat pe o masina virtuala mai noua, pe un procesor de 64 biti, el poate efectua aceeasi operatie intr-o singura instructiune. Totusi, astfel de optimizari la runtime afecteaza timpul de start al programului, deci unele optimizari pot sa nu fie implementate (inca) in masina virtuala.
Cu toate ca exista multe optimizari care se pot face la compilare, un algoritm prost nu va putea fi “reparat” de catre optimizatorul compilatorului. Din experienta mea, optimizarile de compilare (inclusiv recompilare kernel in 64 biti) au rezultate destul de modeste, deoarece inca multe softuri folosesc calcule in 32 biti. In viitor insa diferenta se va simti mai mult, deoarece majoritatea programelor se pregatesc sa poate folosi volume uriase de date, si au nevoie sa faca din ce in ce mai multe calcule in 64 biti.
<va urma>