diplomski rad strategije i tehnike optimizacije programskog koda
Post on 02-Feb-2017
236 Views
Preview:
TRANSCRIPT
SVEUČILIŠTE U DUBROVNIKU
ODJEL ZA ELEKTROTEHNIKU I RAČUNARSTVO STUDIJ POSLOVNOG RAČUNARSTVA
DIPLOMSKI RAD STRATEGIJE I TEHNIKE OPTIMIZACIJE
PROGRAMSKOG KODA
Dubrovnik, prosinac 2011.
SVEUČILIŠTE U DUBROVNIKU
ODJEL ZA ELEKTROTEHNIKU I RAČUNARSTVO STUDIJ POSLOVNOG RAČUNARSTVA
DIPLOMSKI RAD STRATEGIJE I TEHNIKE OPTIMIZACIJE
PROGRAMSKOG KODA
Mentor: Diplomant:
doc.dr.sc. Mario Miličević Robert Primorac Komentor:
mr.sc. Krunoslav Žubrinić
Dubrovnik, prosinac 2011.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
I
SADRŽAJ
1 Uvod .................................................................................................................................... 1 2 Strategije optimizacije ........................................................................................................ 3
2.1 Performanse ................................................................................................................ 4 2.2 Performanse i optimiziranje koda .............................................................................. 5
2.2.1 Zahtjevi aplikacije .................................................................................................. 5 2.2.2 Dizajn aplikacije ..................................................................................................... 5 2.2.3 Dizajn klasa ............................................................................................................ 7 2.2.4 Interakcije s operacijskim sustavom ...................................................................... 7 2.2.5 Prevođenje programskog koda ............................................................................... 7 2.2.6 Strojna oprema ....................................................................................................... 7
3 Optimizacija programskog koda ....................................................................................... 10 3.1 Paretov princip ......................................................................................................... 10 3.2 Uobičajene neistine .................................................................................................. 11
3.2.1 Smanjivanje linija koda u jeziku više razine će poboljšati njegovo izvođenje .... 11 3.2.2 Neke operacije će vjerojatno biti brže od drugih ................................................. 12 3.2.3 Treba optimizirati dok se programira ................................................................... 13 3.2.4 Brzina programa je uvijek jednako važna kao i njegova ispravnost .................... 14
3.3 Kada optimizirati kod? ............................................................................................. 14 3.4 Optimizacije prevoditelja ......................................................................................... 14 3.5 Uska grla .................................................................................................................. 15 3.6 Najčešći izvori neefikasnosti .................................................................................... 15
3.6.1 Ulazno-izlazne operacije ...................................................................................... 15 3.6.2 Straničenje ............................................................................................................ 17 3.6.3 Pozivi prema sistemu ........................................................................................... 17 3.6.4 Greške u programskom kodu ............................................................................... 18
3.7 Mjerenje ................................................................................................................... 18 3.8 Iteracije ..................................................................................................................... 19 3.9 Proces optimizacije koda .......................................................................................... 19
3.9.1 Definiranje osnova ............................................................................................... 20 3.9.2 Prikupljanje podataka ........................................................................................... 20 3.9.3 Analiza rezultata ................................................................................................... 20 3.9.4 Konfiguriranje ...................................................................................................... 21 3.9.5 Testiranje i mjerenje ............................................................................................. 22
4 Tehnike optimizacije ......................................................................................................... 23 4.1 Logika ....................................................................................................................... 23
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
II
4.1.1 Zaustavljanje grananja .......................................................................................... 23 4.1.2 Lijena evaluacija .................................................................................................. 25 4.1.3 Trenutna lokacija .................................................................................................. 25
4.2 Optimizacija nizova znakova ................................................................................... 26 4.2.1 String.concat ......................................................................................................... 27 4.2.2 StringBuilder klasa ............................................................................................... 28 4.2.3 Kada koristiti StringBuilder klasu ........................................................................ 29 4.2.4 Pretvaranje integera u string ................................................................................. 31 4.2.5 Petlje i pojedinačni znakovi ................................................................................. 34
4.3 Optimizacija petlji .................................................................................................... 35 4.3.1 For petlja .............................................................................................................. 35 4.3.2 Foreach petlja ...................................................................................................... 36 4.3.3 Razlika u brzini između for i foreach ................................................................... 36 4.3.4 Dekrementacija u for petlji ................................................................................... 37 4.3.5 Fuzija petlji ........................................................................................................... 37 4.3.6 Odmotavanje petlji ............................................................................................... 38 4.3.7 Unswitching .......................................................................................................... 39 4.3.8 Manje posla unutar petlji ...................................................................................... 41
4.4 Polja .......................................................................................................................... 41 4.4.1 Polja umjesto kolekcija ........................................................................................ 42 4.4.2 Čvrsto definirana polja ......................................................................................... 42 4.4.3 Krnja polja ............................................................................................................ 43 4.4.4 Spljošćivanje polja ............................................................................................... 43
4.5 Tipovi podataka ........................................................................................................ 44 4.5.1 Dodatni podaci ..................................................................................................... 45 4.5.2 Keširanje ............................................................................................................... 45
4.6 Izrazi ......................................................................................................................... 46 4.6.1 Algebarski izrazi .................................................................................................. 46 4.6.2 Smanjivanje snage ................................................................................................ 47 4.6.3 Inicijalizacija pri prevođenju ................................................................................ 47 4.6.4 Eliminiranje podizraza ......................................................................................... 49
4.7 Metode ...................................................................................................................... 49 4.7.1 Ručno kreiranje inline metoda ............................................................................. 50 4.7.2 Statične metode .................................................................................................... 51 4.7.3 Manji broj parametara u metodama ..................................................................... 52 4.7.4 Performanse parametara metode i registri ............................................................ 53 4.7.5 Preopterećenje nativnih metoda ........................................................................... 54
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
III
4.8 Dretve ....................................................................................................................... 55 4.8.1 Smanjiti stvaranje dretvi ....................................................................................... 55 4.8.2 ThreadPool klasa .................................................................................................. 56 4.8.3 Paralelni poslovi ................................................................................................... 57
4.9 Obrada iznimki ......................................................................................................... 57 4.9.1 Ne koristiti iznimke da bi kontrolirali tok programa ............................................ 58 4.9.2 Koristiti validacije umjesto iznimki ..................................................................... 59 4.9.3 Ponovno bacanje iznimaka je skupo .................................................................... 60
5 Zaključak ........................................................................................................................... 61 6 Literatura ........................................................................................................................... 62 7 Sažetak .............................................................................................................................. 63 8 Abstract ............................................................................................................................. 64 9 Prilozi ................................................................................................................................ 65
9.1 Popis slika ................................................................................................................ 65
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
1
1 UVOD
Osnovni motiv pri odabiru ove teme diplomskog rada bio je praktičan problem koji mi se
pojavio na poslu. Kao programer u C# jeziku nikad nisam puno razmišljao o optimizaciji
koda sve do nedavno kada sam trebao ubrzati operaciju s 15 sekundi na 5. Bio sam malo
pogubljen jer nisam znao kako i od kuda početi. Ubrzo sam se našao na Google-u s
ključnom riječi „optimization C#“ i dobio hrpu podataka koji su bili jako interesantni. Ono
što me zadivilo bila je činjenica da to nije automatski posao kod kojeg se oslonite na
postojeći obrazac pa znate kako će izgledati ishod posla.
Postoje mnoge teorije optimizacije tako da je to polje jako široko. Ovaj diplomski rad se
dotiče strategija i tehnika optimizacije. Na većem broju primjera objasniti ću razloge zbog
kojih treba koristiti odgovarajuću tehniku te zbog čega će rezultat njezinog izvođenja biti
brži. Dok strategije optimizacije opisuju općenita područja i pojmove vezane uz
optimizaciju, tehnike optimizacije se odnose na konkretnu realizaciju.
Ako ste programer, molim vas da ne shvatite navedene primjere doslovno i odmah počnete
mijenjati vaš programski kod, jer kao što je William Allan Wulf, pionir optimizacije
prevoditelja jednom napisao „Više računalnih grijeha je počinjeno u ime efikasnosti (bez
njezinog nužnog dostizanja) nego u ime ičega drugoga – uključujući i gluposti [6].“
No isto bih tako želio da ipak razmislite malo o navedenim primjerima jer kao i meni, tako
će vjerojatno i vama pomoći.
Struktura ovog rada je slijedeća: Nakon uvodnog dijela, u drugom poglavlju opisana je
problematika optimizacije s naglaskom na različita tumačenja pojma performanse i
različite pristupe poboljšanju performansi. Metode optimizacije su opisane u trećem
poglavlju. Taj dio je vrlo važan jer pokazuje korake koje treba poduzeti prije optimizacije
koda da bi se izbjegao negativan utjecaj optimiziranja na ispravnost i čitljivost koda. U tom
dijelu se dotičem mjerenja kao jedinog sredstva prepoznavanja poboljšanja performansi.
Pojašnjavaju se uobičajeni izvori uskih grla te kako ih izbjeći. Četvrto poglavlje posvećeno
je konkretnim tehnikama poboljšanja performansi programskog koda na najnižoj razini. U
njemu su navedeni detaljno primjeri programskog koda u C# programskom jeziku. U tom
poglavlju neće se naći primjeri optimizacije na višoj razini, poput poboljšanja algoritama
ili načina za optimizaciju pretrage tablice. Naći će se primjeri optimizacije algebarskih
izraza, operacija s nizovima podataka, metode, i sl što će možda u konačnici ubrzati vašu
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
2
pretragu tablice. Smisao ove cjeline je spuštanje na što je moguće nižu razinu
programiranja te optimiziranje toga dijela, reflektirajući promjenu prema gore. Obrađene
su tehnike optimizacije koje se odnose na najučestalije probleme s kojima se programer
svakodnevno susreće. Kao ilustracija većine opisanih tehnika napisanu su konkretni
primjeri neoptimiziranog i optimiziranog koda u C# programskom jeziku, te su izmjereni i
prikazani rezultati ostvareni korištenjem odgovarajuće optimizacijske tehnike. U petom
zaključnom poglavlju dan je kratak osvrt na napisani diplomski rad.
Testovi su se izvršavali na prijenosnom računalu marke Acer, a model je Aspire 5920.
Najvažnije komponente koje bih naveo su dvojezgreni procesor Intel Core 2 Duo T7300 na
1.5 GHz, Mobile Intel PM965 Express Chipset, 2GB DDR2 memorije na 667MHz te čvrsti
disk veličine 160GB. Operacijski sustav koji se nalazi na prijenosniku je Microsoft
Windows 7. Programsko okruženje je Microsoft Visual Studio 2010 s .NET Framework-om
4.0.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
3
2 STRATEGIJE OPTIMIZACIJE
Koje su to strategije optimizacije? Što je to optimizacija? Ova pitanja se povlače već dulje
vremena u svijetu programiranja i dizajniranja aplikacija tako da se može reći da su to
povijesna pitanja. Šezdesetih godina prošloga stoljeća, računalni resursi su bili mali te je
program koji je koristio malo tih resursa, a uspješno obavljao svoju zadaću bio dobar
program. U to vrijeme efikasnost programa je bila izuzetno važna. Desetljeće nakon toga,
računala su postala znatno snažnija što je natjeralo razvojnike (engl. developers) aplikacija
da stanu i promisle da li im je efikasnost aplikacije stvarno toliko važna? Možda je važnije
da programski kod bude čitljiviji i bolje strukturiran? Zbog razmišljanja o takvim pitanjima
razvojnici su se fokusirali na fino strukturiranje aplikacija, dok su manje pažnje
posvećivali optimizaciji programskog koda. S evolucijom razvoja mikroračunala u
osamdesetim godinama dvadesetog stoljeća, optimizacija je opet postala važna, da bi
devedesetih godina opet pala u zaborav. Dvije tisućite se vratila zahvaljujući revoluciji
ugrađenih (engl. embedded) aplikacija koje se koriste na ručnim računalima i mobitelima.
Na slici 1 je vidljiv ciklus razvoja optimizacije računalnih aplikacija.
Slika 1 Ciklus odnosa optimizacije i snage računala
Razmišljanje o strategijama optimizacije može se sagledati odgovarajući na nekoliko
važnih pitanja o željenim performansama aplikacije:
• Što su to performanse?
Optimizacija je potrebna
Snažnija računala
Optimizacija nije potrebna pa se gleda čitljivost i struktura
Optimizacija je potrebna
Snažnija računala
Optimizacija nije potrebna pa se gleda čitljivost i struktura
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
4
• Koliko su nam performanse važne?
• Kako ostvariti željene performanse?
2.1 Performanse
Optimizacija koda je jedan od načina dobivanja na performansama. Pored same
optimizacije, postoje i drugi načini za postizanje boljih performansi, poput nabavke jačih
računala. To je rezultat činjenice što su korisnici zainteresirani za karakteristike i opcije
programa poput brzine izvođenja, a ne za kvalitetan programski kod. Performanse su im
važne samo onda kada imaju izravnu potreba za takvim kodom. Slijedeći primjer će
ilustrirati takav slučaj.
Imamo aplikaciju koja komprimira podatke. Ta aplikacija može komprimirati jednu
datoteku ili više datoteka. Komprimiranje više datoteka se vrši na način da se odabere
datoteka i da se komprimira, te da se svaka sljedeća datoteka "zalijepi" na prethodnu
datoteku. Na taj način dobivamo arhivu komprimiranih datoteka. Problem za korisnika je
taj da ako želi komprimirati 20 datoteka, mora 20 puta kliknuti na datoteku i reći da ju želi
komprimirati i dodati u postojeću arhivu. Da primjer bolje dočara odnos performansi i
brzine korištenja, reći ćemo da ova aplikacija može komprimirati neku datoteku kapaciteta
10 MB za 2 sekunde.
Druga aplikacija komprimira datoteku od 10 MB za 3 sekunde. Možemo reći da je ova
aplikacija po svojim performansama preko 30% sporija od prethodne. No ono što ova
aplikacija dopušta je odabiranje skupine datoteka i davanje komande da se od svih
odabranih datoteka napravi jedna arhivu. Korisnik je pošteđen mnoštva klikova mišem i
odabiranja kojoj arhivi će se dodati novoizabrana datoteka.
Što dobivamo u konačnici?
Dobivamo aplikaciju koja ima slabije performanse od prethodne kad govorimo o
komprimiranju, no zbog brzine korištenja su joj konačne performanse sa stajališta
upotrebljivosti za krajnjeg korisnika bolje.
Performanse su labavo povezane s brzinom izvođenja programskog koda do te mjere da,
ako se brzina izvođenja programskog koda pokušava previše optimizirati, to može naštetiti
drugim karakteristikama programa. Kada se programski kod ubrzava, treba biti svjestan da
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
5
će pritom možda biti potrebno žrtvovati druge karakteristike što u konačnici može naštetiti
ukupnim performansama programa. Zbog toga je pri optimizaciji potrebno odrediti što je
prioritet i ravnati se prema tome.
2.2 Performanse i optimiziranje koda
Jednom kada se izabere efikasnost kao prioritet, treba razmotriti nekoliko opcija prije nego
se krene s poboljšanjem brzine izvođenja ili smanjenja duljine programskog koda.
Efikasnost se mora sagledat iz nekoliko različitih točaka:
• Zahtjevi aplikacije;
• Dizajn aplikacije;
• Dizajn klasa;
• Interakcije sa operacijskim sustavom;
• Prevođenje koda;
• Strojna podrška;
• Optimiziranje koda.
2.2.1 Zahtjevi aplikacije
Performanse se jako često ističu kao zahtjev od strane korisnika, no korisnik ponekad ne
zna jasno izraziti što je to u programu što bi s njegovog stajališta trebalo poboljšati.
Nekada je to stvarno brzina izvođenja, ali to može biti i neka druga osobina poput
ergonomije korisničkog sučelja. Zbog toga prije nego se uloži vrijeme u rješavanje
problema performansi treba biti siguran da se rješava stvaran problem koji se treba
rješavati[1].
2.2.2 Dizajn aplikacije
Ovaj nivo sadržava važne temelje dizajna aplikacije u pogledu dekompozicije aplikacije na
sastavne dijelove. U slučaju objektno orijentiranog razvoja to su klase i skupine klasa.
Zbog pogrešnih odluka donesenih pri dekompoziciji i modeliranju, ponekad je jako teško
pisati programski kod i posebno poboljšavati performanse aplikacije.
Razmotrite primjer nekog sustava koji interpretira neku pojavu iz svijeta koji nas okružuje.
Na primjer, sustav koji računa opterećenost nekog objekta na N njegovih točaka. Ono što
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
6
se zahtjeva je vrlo brz izračun opterećenja i prezentiranje informacija korisniku. Za svaku
točku se treba računati opterećenje što zahtjeva preračun između različitih mjernih jedinica
koje se koriste za prikaz pojava u stvarnom svijetu u "inženjerski prikaz" razumljiv
korisniku sustava. Izračunati podaci moraju biti što točniji tako da će sa tehničke strane
izračun biti jako kompliciran. U konačnici, brojeve treba prikazati u decimalnom
brojevnom sustavu. Krivi smjer optimizacije bi bio ubrzanje matematičkih izračuna, uz
zanemarivanje preciznosti kako bi korisnici što prije dobili prikazane rezultate.
Ako je potrebno da program bude brz onda se on od samog početka treba dizajnirati s tim
na umu. Znači, od samog početka treba dizajnirati aplikaciju tako da bude orijentirana
boljim performansama a potom bi trebalo definirati i dizajnirati ostale module i klase. U
tom slučaju se ponekad moraju donijeti i teške odluke poput izbora nekog specifičnog
razvojnog alata ili programskog jezika s kojim razvojni tim možda nije dobro upoznat, ali
čije korištenje će omogućiti ostvarenje željenih performansi.
Dizajniranje aplikacije pomaže poboljšanju performansi na nekoliko načina:
• Ako se kao pojedinačni ciljevi postave očekivane performanse pojedinačnih
modula onda konačne performanse sustava postaju predvidljive. Ako svaki modul
dostigne svoje ciljeve, onda će i cijela aplikacija vrlo vjerojatno dostići svoje
ciljeve. Na ovaj način se mogu identificirati moduli koji ne dostižu ciljeve te ih se
može lakše redizajnirati bez promjene sustava kao cjeline.
• Samo razmišljanje o performansama tijekom početne faze dizajniranja, te
postavljanje konkretnih ciljeva u toj fazi eksplicitno povećava šansu da će se taj cilj
i dostići. Programeri će se lakše baviti rješavanjem problema kada imaju zadane
konkretne ciljeve, naročito ako su ti ciljevi numerički (npr. ova obrada mora
završiti u prosjeku za 3 sekunde; najgori slučaj koji se smije dogoditi je 5 sekundi, i
ne bi se smio javljati u više od 10% slučajeva). Što je cilj eksplicitniji lakše je raditi
na njegovom ostvarenju.
• Mogu se postaviti ciljevi koji nužno ne poboljšavaju efikasnost ali ju promoviraju
na duge staze. Efikasnost najbolje dolazi do izražaja u nekim drugim problemima.
Na primjer, ako se dostigne veliki stupanj modularnosti sustava na način da se
sustav lako može proširiti ili da se neki moduli lako mogu zamijeniti onda je
dostignut visoki stupanj efikasnosti, ali bez nekih eksplicitnih ciljeva. Ovo će doći
do izražaja ako se identificira modul koji sporo radi. Taj modul se lako može
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
7
zamijeniti s modulom koji radi brže te je na taj način dostignuta željena razina
efikasnost.
2.2.3 Dizajn klasa
Dizajniranje klasa predstavlja priliku za dizajniranje samih performansi. Jedna od ključnih
performansi na ovom nivou su izbor tipova podataka i algoritama koji često znaju utjecati
na korištenje memorije tijekom obrade a samim time i na brzinu izvršavanja programa. U
slučaju kritičnih obrada kod kojih su performanse izvođenja ključne, tijekom ova faze
razvojnom timu se mogu zadati konkretni algoritmi koje moraju koristiti kako bi se željene
performanse postigle.
2.2.4 Interakcije s operacijskim sustavom
Svaki program se izvodi u okruženju operacijskog sustava i tijekom rada vrši interakciju s
njim. Ako program koristi vanjske datoteke, dinamičku memoriju ili vanjske uređaje, ta
interakcija je znatno intenzivnija
Ako su performanse izvođenja odgovarajućih operacija slabe, potrebno je provjeriti da su
uzrok toga funkcije operacijskog sustava koje su spore ili neodgovarajuće za određenu
vrstu obrade. Uzrok greške može biti i samo razvojno okruženje odnosno problemi pri
prevođenju programskog koda u izvršnu verziju.
2.2.5 Prevođenje programskog koda
Dobar prevoditelj prevodi programski jezik u dobro optimizirani strojni kod, tako da izbor
dobrog prevoditelja rješava veliki dio optimizacije. Slične probleme imaju interpreterski
jezici koji tijekom izvođenja programa dinamički "u hodu" prevode programski kod. Kod
njih se lošije performanse ponekad teže zapažaju u slučaju kada ovise o trenutnom stanju
okruženja u kojem se program izvodi.
2.2.6 Strojna oprema
Nekada je najjednostavniji način poboljšanja performansi kupnja novog računala. Ako se
radi aplikacija koja je namijenjena tisućama korisnika onda česta promjena strojne opreme
nije najbolja opcija, ali ako se radi neku specifična aplikaciju za jednog ili nekoliko
poznatih korisnika onda ova opcija zna biti najjeftinija jer se štedi na trošku rada razvojnog
tima tijekom faze poboljšanja performansi. Pored toga, ušteda se javlja i zbog toga jer se u
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
8
budućem održavanju neće javljati problemi i greške koji su nastali isključivo kao
posljedica poboljšanja performansi. Pored toga, i drugi programi koji će se izvoditi na istoj
opremi vjerojatno imati će dobiti bolje performanse, bez ikakve intervencije u programski
kod. Ovakav način poboljšanja performansi danas je dosta često prisutan u slučajevima
kada je to opravdano, ali i onda kada to nije opravdana. Korisnici se često povode za
izjavama da npr. treba svake dvije godine zamijeniti računala kako bi sustav normalno
funkcionirao.
U nekim slučajevima dodavanje i pojačavanje strojne opreme neće pomoći poboljšanju
performansi aplikacije. Tipičan primjer takvog slučaja je ako se u obradi koriste algoritmi
velike složenosti. Npr. ako je algoritam eksponencijalne i više složenosti (npr. O(2n),
O(3n),…) pojačanje opreme neće znatnije poboljšati brzinu izvođenja već je potrebno
napisati novi algoritam niže razine složenosti.
Kada se govori o unaprjeđenju performansi poboljšanjem strojne opreme, postoje dvije
osnove tehnike kojima se povećavaju performanse, a to su skaliranje prema gore i
skaliranje prema van. Skaliranje je mogućnost aplikacije da zadrži sposobnost održavanja
performansi uz povećanje rada same aplikacije. Uobičajeni ciljevi performansi su
zadržavanje vremena odaziva i prohodnosti.
• Skaliranje prema gore znači poboljšanje trenutne strojne opreme [4]. Tu
govorimo o novom procesoru, memoriji disku ili mrežnom upravljaču. Kandidati za
zamjenu su one komponente o kojima performanse najviše ovise. Ponekad se
zamjenjuje čitav poslužitelj. Takva tehnika je dobra jer je relativno jeftina, a ne
donosi nikakve nove troškove održavanja. Ono što ne valja je da u jednom
trenutku, dodavanje nove jače strojne opreme neće pomoći performansama jer i
sama aplikacija mora svojom funkcionalnošću pratiti pojačanje strojne opreme.
• Skaliranje prema van znači dodavanje više poslužitelja trenutnoj skupini
poslužitelja i stvaranje tzv. farme poslužitelja [4]. Time se dobiva rasterećenje
trenutnog sustava odnosno razvodnjavanje posla na više paralelnih obrada. Ovakav
način se može koristiti samo u slučaju ako operacijski sustav i drugi korišteni
resursi podržavaju takav način rada
Skaliranje ima određene prednosti i nedostatke o kojima treba voditi računa:
• Skaliranje prema gore je najbolje za aplikacije koje paralelno izvršavaju poslove.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
9
• Skaliranje prema van je najbolje za poslove kojima se tijekom korištenja povećava
obujam korištenja (npr. farme web poslužitelja).
• Ako aplikacija obavlja poslove koji se simultano izvršavaju i koji su neovisni
jedan o drugome, te se aplikacija pokreće na poslužitelju s jednim procesorom,
onda se poslovi trebaju izvršavati asinkrono tako da jedna obrada ne mora čekati
završetak druge. Asinkrono procesiranje je puno bolje za poslove koji su usko
vezani uz veći broj ulazno-izlaznih operacija. U ovom slučaju najbolje je skalirati
prema gore i dodati još procesora.
• Ograničenja koja su postavljena operacijskim sustavima mogu biti prepreka
skaliranju prema gore. Na primjer u slučaju ako operacijski sustav podržava samo
dva procesora i 4GB memorije, onda nema smisla dodavati više od toga.
• Skaliranje prema van donosi nove troškove u vidu održavanja.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
10
3 OPTIMIZACIJA PROGRAMSKOG KODA
Optimizacija programskog koda je praksa modificiranja ispravnog programskog koda u
svrhu efikasnijeg izvršavanja istog [4]. Optimiziranje se odnosi na male promjene koje
zahvaćaju jednu klasu, funkciju ili kao što je vrlo često slučaj, samo par linija koda.
Optimiziranje se ne odnosi na velike promjene čak i ako te promjene donose poboljšanje.
Taj postupak nije najefektivniji način za poboljšanje performansi. Dobra arhitektura, dizajn
klasa ili pažljivo izabrani algoritmi će napraviti veće promjene od samog optimiziranja. To
najlakši način dobivanja na performansama jer je nabavka nove i bolje strojne opreme ili
boljeg prevoditelja puno lakša od optimiziranja. Potrebno je znatno više vremena da bi se
programski kod optimizirao, a takav kod je teže održavati. Postavlja se pitanje zbog čega
optimizirati programski kod?
Optimiziranje koda je primamljivo iz nekoliko razloga. Jedan od njih je neka vrsta prkosa.
Veliko je zadovoljstvo uzeti neku funkciju koja se izvršava za 20 milisekundi, optimizirati
par linija programskog koda, te na taj način smanjiti vrijeme izvršavanja na 2 milisekunde.
Također je primamljivo za programere jer pisanje dobrog i brzog koda je neka vrsta rituala
ka postajanju ozbiljnog programera. Nikoga osim drugog programera neće zanimati koliko
vam je kod brz. Bez obzira, unutar kulture programiranja, pisanje mikro efektivnog koda
dokazuje da ste ozbiljan programer.
3.1 Paretov princip
Paretov princip je poznat i kao 80/20 pravilo koje kaže da možemo dobiti 80 rezultata uz
20 posto optimalno uloženog truda [2]. To pravilo se može primijeniti na brojna područja
uključujući programiranje, a posebno je istinito pri optimizaciji koda.
Tim ljudi koji je napravio ALGOL programski jezik, otac svih novijih programskih jezika,
je dao savjet u obliku poznate Voltairove izjave „Najbolji je neprijatelj dobrome. [5]“. Što
ta izjava znači? Ukratko, rad usmjeren isključivo prema ostvarenju savršenstva priječi
završavanje. Prvo završite posao pa se nakon toga posvetite usavršavanju. Dio koji se treba
usavršiti obično je jako mali.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
11
3.2 Uobičajene neistine
Dosta stvari koje ste čuli o optimiziranju koda su neistinite. Navesti ću nekoliko
uobičajenih primjera te ću ih potkrijepiti s rezultatima koje sam izmjerio na konkretnim
primjerima programskog koda.
3.2.1 Smanjivanje linija koda u jeziku više razine će poboljšati njegovo
izvođenje
Ovo je naravno neistinito. Razmotrimo sljedeće linije koda gdje ćemo pokušati
inicijalizirati polje prirodnih brojeva na dva različita načina. Prvi način je čitljiviji i ima
manje linija koda. Drugi će biti suprotno od toga. Ovu inicijalizaciju ću pokrenuti
1.000.000 puta radi lakšeg mjerenja.
Kratka
int[] polje = newint[10]; for (int i = 0; i < 10; i++) { polje[i] = i; }
Duga
int[] drugoPolje = newint[10]; polje[0] = 0; polje[1] = 1; polje[2] = 2; polje[3] = 3; polje[4] = 4; polje[5] = 5; polje[6] = 6; polje[7] = 7; polje[8] = 8; polje[9] = 9;
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
12
Slika 2 Odnos duljine programskog koda i brzine izvođenja
Slika 2 s rezultatima mjerenja prikazuje da se dulji i nečitljiviji programski kod izvršava
gotovo tri puta brže od kraćeg i čitljivijeg koda. Zaključak je da ne treba tražiti vezu
između broja linija programskog koda i brzine izvršavanja na računalu. To dokazuje da
estetika u pisanju koda ne bi trebala biti važna ako se promatra samo brzina izvršavanja
programskog koda. S druge strane, estetika programskog koda je izuzetno važna sa
stajališta programera u slučaju kada je npr. potrebno održavati postojeći programski kod
koji je napisao neki drugi programer.
3.2.2 Neke operacije će vjerojatno biti brže od drugih
Kada govorimo o performansama ne bi trebali upotrebljavati riječ „vjerojatno“[1].
Performanse se uvijek moraju izmjeriti da bi se znalo da su neke promjene pomogle ili
odmogle aplikaciji. Rezultati mjerenja se mijenjaju svaki put kada promijenimo okruženje
poput korištenih vanjskih dll-ova, verzije frameworka ili prevoditelja, procesor, memoriju,
i sl. Rezultati koje smo dobili pri jednom mjerenju u jednom okruženju mogu vrlo lako biti
drukčiji u drugom okruženju.
Ta pojava nam daje nekoliko razloga zašto ne bismo poboljšavali performanse
optimiziranjem programskog koda. Ako želimo da program bude prenosiv, onda tehnike
koje se upotrebljavaju da bi se poboljšale performanse u jednom okruženju mogu smanjit
performanse u drugom okruženju. Ako promijenimo verziju prevoditelja, možda će ta
verzija bolje optimizirati kod nego vi ali joj je potreban standardno napisani kod. To
naravno u slučaju ručne optimizacije pada u vodu jer ste pokušali programski kod
optimizirati kod.
27
87
0
20
40
60
80
100
1 2
Duga
Kratka
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
13
Ako ste podešavali kod onda se implicitno slažete da ćete profilirati (engl profiling)
programski kod svaki put kada promijenite verziju prevoditelja ili verziju korištenih
vanjskih programskih biblioteka.
3.2.3 Treba optimizirati dok se programira
Postoje neke teorije koje kažu da ako pišete što manji i brži programski kod od samog
početka da će konačni proizvod biti malen i brz. Takav pristup najčešće vodi uzrečici „Od
šume se ne vidi stablo“. Zašto je to tako? Ako smo čitavo vrijeme zauzeti mikro
optimizacijama onda nećemo vidjeti globalne optimizacije koje su puno važnije od ovih
manjih. Neki uobičajeni problemi koji se javljaju kod optimiziranja od samog početka su
slijedeći:
• Gotovo je nemoguće identificirati uska grla prije nego se program počne izvoditi u
stvarnom okruženju. Programeri su jako loši u pogađanju kojih 20% koda izvršava
80% programa, tako da ako budu optimizirali dok programiraju, potrošiti će 80%
vremena optimizirajući kod koji ne treba optimizirati. Nakon toga im preostaje jako
malo vremena za optimizaciju onog koda koji je stvarno važan.
• U rijetkim slučajevima u kojima se uspije identificirati usko grlo, programeri znaju
pretjerati s optimizacijom tako da ostala uska grla koja su još neidentificirana
postaju još kritičnija. Optimizacija nakon što je sistem napravljen dopušta
identifikaciju svih uskih grla te optimalno raspoređivanje vremena na rješavanje
svih takvih mjesta.
• Fokusiranje na optimizaciju tijekom razvoja odvlači programere od krajnjeg cilja.
Programeri se upuštaju u rasprave i pisanje složenih algoritama koji krajnjem
korisniku ne znače puno. Brige poput ispravnosti rada, skrivanja informacija i
čitljivosti postaju sekundarni ciljevi, unatoč tome što su performanse stavka koja se
lakše ispravlja u kasnim fazama nego to troje.
Ukratko, osnovna loša strana prerane optimizacije je nedostatak perspektive. Ono što
najviše pati zbog toga je brzina i kvaliteta konačnog proizvoda te sami korisnici koji rade s
tom aplikacijom.
Ako se tijekom razvoja vrijeme predviđeno izradi osnovnih funkcionalnosti posveti
optimizaciji programa, onda će kao rezultat vjerojatno biti brz program koji nije nužno
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
14
kvalitetan [1]. Ako je iz nekog specifičnog razloga potrebno optimizirati program tijekom
izrade onda treba imati zadanu perspektivu pri tome.
3.2.4 Brzina programa je uvijek jednako važna kao i njegova ispravnost
U praksi brzina program nikada nije važnija od ispravnog programa. Na primjer za
programe koji se brinu za upravljanje zrakoplovom brzina je izuzetno važna. Ali sva
postignuta brzina neće nimalo pomoći ako u nekom svom dijelu program izračuna
pogrešne podatke. Kod takvih kritičnih primjena brzina i ispravnost su jednako i iznimno
važne. U većini svakodnevnih primjena brzina nije toliko važna koliko to ljudi na prvi
pogled smatraju. Pored toga sa sve jačom strojnom opremom, brzina postaje sva manje
važna nauštrb ispravnosti. Zbog toga se pri razvoju još u fazi dizajna rizici smanjenih
performansi moraju predvidjeti i planirati kako se ne bi došlo u situaciju da razvojni tim
optimizira nešto što nije potrebno optimizirati.
3.3 Kada optimizirati kod?
Dobro dizajnirajte program. Napravite ga da bude ispravan. Napravite ga da bude
modularan te da se lako mogu raditi izmjene na njemu. Kada je programski kod potpun i
ispravan, tek tada provjerite performanse. Ako je program velik i trom, poboljšajte ga tako
da bude brži i manji .
Nemojte optimizirati programski kod naprečac dok ne znate što trebate optimizirati. Prvo
pronađite kritična uska grla [1]. Čest je slučaj da u jednom radnom danu možete
identificirati nekoliko uskih grla u kodu te da par zahvata na tim mjestima može drastično
ubrzati rad projekta.
3.4 Optimizacije prevoditelja
Moderni prevoditelji su poprilično moćni kad je u pitanju optimizacija napisanog
programskog koda. Optimiziranje koje odrađuje prevoditelj odgovarajućim postavkama
može biti puno bolje od optimiziranja koda. Ako radite zamršene stvari u petljama ili
algoritmima s velikom složenošću, vaš prevoditelj će teško moći optimizirati taj kod. Kod
izbora algoritama pokušajte izabrati što je moguće jednostavniji algoritam, manje
složenosti. Kod rješavanja problema pokušajte izbjeći rekurziju bez obzira što je ona za vas
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
15
kao programera jednostavnija, pošto ona zbog načina prijenosa podataka pri rekurzivnim
pozivima znatno opterećuje računalne resurse tijekom izvođenja programa.
3.5 Uska grla
Usko grlo je resurs koji ograničava prohodnost i brzinu izvođenja programskog koda. U
većini slučajeva uska grla u našim aplikacijama su povezana s ključnim računalnim
resursima koje koriste aplikacije, poput procesora, radne memorije, diska ili računalne
mreže. Uska grla variraju od sloja do sloja. Kod dvoslojnih poslovnih aplikacija koje
koriste bazu podataka mogu se uočiti dvije lokacije na kojima se uska grla najčešće
javljaju:
• Web/Aplikacijski poslužitelj. Česti uzroci uskih grla uključuju neefikasno baratanje
sesijama i stanjima, konekcije, duge pozive prema web servisu ili bazi podataka te
komplicirana sučelja.
• Poslužitelj baze podataka. Česti uzroci uskih grla uključuju lošu logiku pri
dizajniranju baze, nedovoljno normalizirane tablice, loše indekse na tablicama,
premalo ili previše indeksa na tablicama i loše particionirane podatke po tablicama.
Ostali uzroci su najčešće neefikasni upiti ili procedure koje služe za komunikaciju
aplikacije s bazom podataka.
Ove dvije stavke se tiču svakog programera koji razvija takvu aplikaciju, neovisno o
programskom jeziku, platformi i bazi podataka koju koristi.
3.6 Najčešći izvori neefikasnosti
Za vrijeme optimizacije programskog koda pronalaze se kritični dijelovi koji se dugo
izvršavaju. Nakon optimizacije, ti dijelovi se mogu smanjiti, a njihovo izvođenje ubrzati.
Takvi dijelovi se otkrivaju profiliranjem programskog koda. Promatranjem nekih
elemenata programskog koda na prvi pogled možete uočiti potencijalno kritična mjesta te
ih ispitujete prve.
3.6.1 Ulaznoizlazne operacije
Neki od najvažnijih izvora neefikasnosti su nepotrebne ulazno-izlazne operacije (engl.
input-output, I/O). Na primjer, ako imate mogućnost izbora između obrade u memoriji
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
16
nasuprot obradi korištenjem čvrstog diska ili web servisa, svakako izaberite rad u memoriji
osim ako je upotreba radne memorije kritična.
Sljedeći primjer potkrijepljen grafom s rezultatima mjerenja prikazanom na slici 3
prikazuje koliko je memorija znatno brža od I/O pristupa.
Prva metoda pristupa datoteci i čita znak koji se nalazi na slučajno izabranoj poziciji u
rasponu od 0 do 100. Svaki put prije nego pročitamo znak, moramo se pozicionirati na
poziciju na kojoj se nalazi.
Random r = newRandom(100); StreamReader f = newStreamReader(@"C:\Prijevod.txt"); for (int i = 0; i < brojac; i++){ f.BaseStream.Seek((long)r.Next(100), SeekOrigin.Begin); char c = (char)f.Read(); }
Druga metoda učitava datoteku izravno u memoriju u obliku niza. Zatim pristupa
slučajnom indeksu niza.
Random r = newRandom(100); StreamReader f = newStreamReader(@"C:\Prijevod.txt"); string memorijskiBuffer = f.ReadToEnd(); for (int i = 0; i < brojac; i++) { char c = memorijskiBuffer[r.Next(100)]; } Datoteka je slučajno izabrana tekstualna datoteka na mom računalu te je veličine 688 B.
Slika 3 Odnos brzine memorije i čvrstog diska
Prema ovim podacima i klasama za čitanje koje koristim, metoda koja pristupa izravno
memoriji je 35 puta brža.
5
175
0
50
100
150
200
1 2
Memorija
Datoteka
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
17
Da smo kojim slučajem probali isto ovo napraviti uz dohvaćanje podataka s drugog
računala preko mreže, vrijeme izvršavanja bi bilo još veće jer bi trebalo uzeti u obzir i
vrijeme pristupa do ciljnog računala putem mreže. Rezultati koje smo dobili su dovoljni da
prilikom optimizacije detaljnije provjerimo sva mjesta u programu koja pristupaju čvrstom
disku i izbacimo sve pristupe koji nisu potrebni i mogu se odraditi u memoriji.
3.6.2 Straničenje
Operacija straničenja (engl. paging) koja se koristi za izmjenu memorijskih stranica je
znatno sporija od operacije koja radi samo na jednoj stranici memorije. Nekad mala
promjena uzrokuje velike razlike. U sljedećem primjeru, ćemo vidjeti koje promjene koda
su bile potrebne da bi se izbjeglo straničenje. Imamo sistem koji barata sa stranicama
veličine 4 KB, a programski koji izgleda ovako:
for ( column = 0; column < MAX_COLUMNS; column++ ) { for ( row = 0; row < MAX_ROWS; row++ ) { table[ row ][ column ] = BlankTableElement(); } }
Na prvi pogled ne vidimo problem, iako on postoji. Problem je u tome što je svaki element
tablice duljine 4.000 B. Ako tablica ima previše redaka, svaki puta kada program treba
pristupiti drugom retku, operacijski sustav će promijeniti stranice memorije. Na način na
koji je ova petlja strukturirana, svaki pristup će uzrokovati promjenu stranice na disku.
Ako restrukturiramo petlju na ovaj način,
for ( row = 0; row < MAX_ROWS; row++ ) { for ( column = 0; column < MAX_COLUMNS; column++ ) { table[ row ][ column ] = BlankTableElement(); } }
dobit ćemo bolje performanse u vidu ne tako čestog straničenja.
3.6.3 Pozivi prema sistemu
Pozivi prema sistemu su jako često skupi. Ove metode u sebi obično sadržavaju I/O
operacije prema čvrstom disku ili ulazno-izlaznim uređajima poput tipkovnice, monitora,
pisača te operacije za pristup memoriji. Ako su performanse problem onda je potrebno
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
18
saznati koliko su skupi pozivi prema sistemu. Ako su skupi onda se mogu razmotriti
sljedeće opcije:
• Napisati vlastite servise. Nekada je potreban samo djelić funkcionalnosti koje pruža
sistemska metoda, i svu potrebnu funkcionalnost možete sami izgraditi. Pisanje
vlastitih servisa vam daje nešto što je manje, brže i odgovara vašim potrebama.
Nedostatak je što morate biti upoznati s pisanjem sistemskih programa što može
predstavljati problem za određenu skupinu razvojnika.
• Izbjegavajte dublji odlazak u sistem. Napravite jednostavniju obradu, ako je to
moguće.
• Surađujte s autorima sistema kako bi oni prilagodili njegovu funkcionalnost vašim
potrebama te kako bi pozivi prema sistemu bili brži. Većina isporučitelja sistema je
zahvalno na informacijama korisnika i posebno razvojnih timova o njihovom
sustavu koje će im pomoć u poboljšavanju istog.
3.6.4 Greške u programskom kodu
Posljednji navedeni, ali nipošto najmanji ili najrjeđi izvor loših performansi su greške u
kodu. Uzroci takvih grešaka su brojni od nedovoljno kvalitetnog programskog koda, loše
iskonfiguriranih konfiguracijskih datoteka, loše dizajniranih klasa odnosno kompletne
strukture aplikacije, loše dizajnirane baze podataka sl.
3.7 Mjerenje
Zbog toga što mali dijelovi koda tijekom izvođenja programa obično koriste
neproporcionalnu količinu resursa, izmjerite programski kod da biste našli uska grla.
Nakon što ste našli uska grla, optimizirajte ih pa izmjerite programski kod ponovno i vidite
koliko poboljšanje ste stvarno dobili. Pretpostavka dobrih rezultata je da mjerenja u oba
slučaja vršite u identičnim ili barem jako sličnim uvjetima i okruženju.
Kod optimizacije iskustvo ne pomaže previše. Iskustvo osobe može doći iz nekog starijeg
jezika ili prevoditelja, a kad se te stvari promjene onda iskustvo postane nevažeće. Nikad
ne možete biti sigurni u efekte optimizacije dok ih ne izmjerite.
Ako nije vrijedno mjerenja da bi saznali je li efikasnije, onda nije vrijedno žrtvovanja
čitljivosti da bi dobili na performansama.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
19
Mjerenja performansi moraju biti precizna [4]. Profiler je jako koristan alat koji dolazi s
većinom suvremenih razvojnih alata, pa tako i s MS Visual Studiom 2010 na kojem su
izrađeni i izmjereni primjeri navedeni u ovom radu.
Bez obzira koristi li se gotovo rješenje za mjerenje performansi ili vlastito koje smo sami
izradili, važno je da se mjeri samo onaj dio programskog koda koji se optimizira.
Instanciranje .NET klase štoperice na početku tog dijela koda i zaustavljanje na kraju je
dobra praksa koja daje zadovoljavajuće rezultate. U nekim slučajevima dijelove
programskog koda je potrebno izvesti više puta (npr 100.000 ili 1.000.000) kako bi se
dobili mjerljivi rezultati.
3.8 Iteracije
Jednom kada identificirate usko grlo ostati ćete začuđeni koliko možete dobiti na
performansama optimizacijom koda. Teško da ćete korištenjem samo jedne tehnike ubrzati
programski kod deset puta ali ako kombinacijom više tehnika uz odgovarajući trud i
utrošak vremena možete dostići gotovo svaku brojku koja vam je potrebna odgovara.
Primjer iz stvarnog života je aplikacija koju sam radio. Bavi se planiranjem radnog
vremena. Jedan planer je imao dosta ljudi ispod sebe. Preko 100. Samo upisivanje u bazu
je teklo jako brzo ali čitanje je bilo izuzetno sporo. Zbog prirode problema, svako čitanje je
bilo popraćeno s velikim brojem validacija koje su gledale smije li radnik još raditi, da li je
radio prekovremeno ili je bio na službenom putu, itd. Ispisivanje 100 radnika za mjesec
unaprijed je trajalo preko 15 sekundi. Kombiniranjem tehnika optimizacije uspio sam
smanjiti to vrijeme na ispod 5 sekundi. Velika pomoć u svemu tome bio je Profiler koji je
došao uz Visual Studio. On je otkrio da je su metode za izvršavanje upita nad bazom
podataka i spajanje nizova metode koja se najviše pozivaju i najdulje traju. Tehnike koje
sam upotrijebio bile su rad sa memorijom umjesto čvrstim diskom te korištenje
StringBuilder klase o kojoj će biti riječi u narednim poglavljima.
3.9 Proces optimizacije koda
Optimizacija koda je iterativni proces koji se upotrebljava kako bi se identificirala i
eliminirala uska grla u aplikaciji. Iterativan je jer ponavljamo korake dok nam aplikacija ne
dostigne odgovarajuće performanse.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
20
Na slici 4 prikazan je jednostavan proces optimizacije sa četiri koraka.
Slika 4 Ciklus optimizacije programskog koda
Nakon definiranja osnova što se optimizacijom želi postići, slijedi prikupljanje podataka o
trenutnom stanju i funkcioniranju programa. Prikupljeni podaci se analiziraju te se vrši
promjena programskom koda vodeći računa o dobivenim rezultatima i definiranom cilju.
Nakon svakog skupa promjena ponovno testiramo i ponovno mjerimo da bi se uvjerili da
program i dalje ispravno funkcionira te da se poboljšanje performansi kreće u željenom
smjeru
3.9.1 Definiranje osnova
Prije nego se započne s optimizacijom potrebno je odredit osnovne postavke optimizacije
koja uključuje ciljeve optimizacije, plan testiranja i opis metrike koja će se koristiti pri
mjerenju ostvarenih rezultata.
3.9.2 Prikupljanje podataka
Prilikom prikupljanja podataka za analizu vrlo je važno da se svi podaci prikupljaju iz istog
izvora te da se ne mijenja okruženje unutar kojeg se podaci prikupljaju. Opterećenje
sustava tijekom prikupljanja podataka treba biti konstantno ili približno konstantno. Na
takav način podaci koji se budu prikupljali i razlike među njima oslikavati će ostvarene
rezultate optimizacije, a neće uključivati druge vanjske faktore.
3.9.3 Analiza rezultata
U ovom koraku se analiziraju prikupljeni podaci koji se upotrebljavaju kako bi se otkrila
uska grla. Da bi identificirali osnovni problem potrebno je početi tragati od mjesta na
Prikupljanje podataka
Analiziranje rezultataKonfiguriranje
Testiranje i mjerenje
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
21
kojem su simptomi prvi put opaženi. Obično najočitija opažanja nisu korijen problema.
Kada analiziramo podatke trebamo imati na umu slijedeće [4]:
• Podaci koje se prikupe obično su samo indikator problema, a ne izvor problema.
Indikatori performansi izvođenja programa izraženi u milisekundama ili sekundama
mogu dati upute kako izolirati specifične skupine funkcionalnosti koje je potrebno
detaljnije proučiti u aplikaciji.
• Nagle promjene performanse aplikacije možda nisu jako važni već oslikavaju
promjene okruženja na koje optimizacijom ne možete utjecati.
• Zbog toga se treba pobrinuti da se testovi ne pokreću istovremeno s nekim drugim
aplikacijama. Pored toga testove treba izvoditi u odgovarajućem trajanju, te uzimati
u obzir prosjeke vremena izvođenja.
• Ako prikupljeni podaci nisu potpuni, onda će i analiza biti nepotpuna i netočna.
• Potrebno je imati mogućnost identificiranja i izoliranja dijelova aplikacije koje je
potrebno detaljnije optimizirati
• Ako se tijekom analize identificiraju uska grla koja znatnije utječu na performanse,
neka vam ona budu prioritet.
• Tijek analize je potrebno dokumentirati kako bi se naknadno isti postupak mogao
ponoviti.
3.9.4 Konfiguriranje
Aplikacija se optimizira tako da se aktiviraju odgovarajuće opcije na razini sistema,
platforme ili promjene dijelovi programskog koda unutar same aplikacije. Dokumentacija
analize iz prethodnog koraka može tome pomoći. Pri konfiguriranju valja voditi računa o
dvije stvari:
• Potrebno je raditi jednu po jednu promjenu. Promjene moraju biti individualne.
Istovremeno izvođenje više promjena može znatno otežati identifikaciju promjene
koja je pridonijela povećanju performansa.
• Probleme je potrebno rješavati po određenom redu ovisno o postavljenim ciljevima
i očekivanim rezultatima. Prvo se treba posvetite problemima za koje se smatra da
će njihovo rješavanja najviše pridonijeti poboljšanju performansi.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
22
3.9.5 Testiranje i mjerenje
Optimizacija je iterativan proces [4]. Nakon što su napravljene promjene u jednoj iteraciji,
potrebno je ponovno testirati aplikaciju i izmjeriti utjecaj napravljenih promjena na
performanse. Tako se dobiva informacija o tome jesu li napravljene promjene pomogle ili
odmogle. Taj proces treba ponavljati sve dok ostvareni rezultati ne dostignu zadane ciljeve.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
23
4 TEHNIKE OPTIMIZACIJE
Optimizacija koda je dosta popularna tema u programiranju već dugi niz godina. Zbog
toga, jednom kada ste odlučili poboljšati performanse i želite to odraditi na nivou
programskog koda, na raspolaganju je bogat izbor isprobanih tehnika optimizacije.
Ovo poglavlje se fokusira na poboljšanje brzine te u sebi sadrži malo savjeta o tome kako
smanjiti veličinu programskog koda. Performanse se obično odnose na veličinu i brzinu, ali
veličina se obično smanjuje redizajniranjem klasa, a ne optimiziranjem programskog koda.
Optimiziranje koda se češće odnosi na male izmjene unutar programskog koda nego na
velike izmjene.
Glavna svrha ovog poglavlja je ilustriranje optimizacije koda koje se mogu primijeniti u
praktičnim situacijama. Neke knjige i članci predstavljaju tehnike optimizacije kao grubu
metodu procjene te sugeriraju kako će primjena određene tehnika proizvesti tražene
rezultate. Gruba metoda procjene se loše spaja sa optimizacijom. Jedina metoda procjene
koja je valjana je mjerenje rezultata. Zbog toga će ovo poglavlje sadržavati i listu stvari
koje možete praktično isprobati. Neke od tih tehnika vjerojatno neće raditi u vašem
konkretnom razvojnom okruženju ali druge će raditi jako dobro.
4.1 Logika
Veliki dio programiranja se sastoji od manipuliranja programskom logikom. Ovaj dio
objašnjava kako manipulirati logičkim izrazima da bi poboljšali performanse.
4.1.1 Zaustavljanje grananja
Recimo da imamo sljedeći izraz:
if ( x < 5 && x < 10) Jednom kada smo shvatili da je x manji od 5, nije potrebno raditi ostatak provjere. Neki
programski jezici i prevoditelji daju mogućnost provjere poznate kao prekidanja provjere
kratkim spojem (engl. short-circuit evaulation). To znači da će prevoditelj tijekom
prevođenja sam generirati dio programskog koda koji će automatski zaustaviti testiranje
ako je odgovor poznat. Ako programski jezik ne podržava takvu provjeru, onda bi u
razvoju trebalo izbjegavati korištenje operatora „i“ i „ili“. Umjesto toga sami programeri
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
24
trebaju dodati programski kod koji će oponašati funkcionalnost tih operatora. Sljedeći izraz
demonstrira primjer takvog programskog koda:
if ( x < 5){ if ( x < 10){
Princip prekidanja testiranja kada znate odgovor je jako dobar u mnogo drugih slučajeva.
Petlja za pretragu je jedna od njih. Ako skenirate polje brojeva i tražite negativni broj te
trebate samo znati postoji li negativni broj u tom polju, onda se može provjeriti svaka
pojedina vrijednost te u slučaju kada je takav broj pronađen, postaviti varijablu na true.
Primjer takvog programskog koda je slijedeći:
bool imaLiNegativnih = false; for (int i = 0; i < m; i++) { if (polje[i] < 0) { imaLiNegativnih = true; } } Bolji pristup bi bio algoritam koji zaustavlja pretragu u onom trenutku kada je pronađen
negativan broj.
Slika 5 Zaustavljanje ispitivanja kada je odgovor poznat
41
21
91 91
0
10
20
30
40
50
60
70
80
90
100
1/2 1/4
Break
Standard
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
25
Na slici 5 su vidljivi rezultati ovakve optimizacije. Pretraga se odvija na polju sa
10.000.000 članova. U prvom slučaju negativan broj smješten je na polovicu polja, a u
drugom se nalazi u prvoj četvrtini polja. Duljina izvođenja u prvom slučaju je više nego
dvostruko kraća. Kada se negativan broj nalazi u prvoj četvrtini polja razlika u ubrzanju je
još vidljivija tako da se programski kod odvija gotovo četiri i po puta brže.
4.1.2 Lijena evaluacija
Ako program koristi lijenu evaluaciju (engl. laizy evaluation) onda izbjegava raditi sve do
trenutka kada je ta obrada stvarno potrebna. Ovaj princip je jako sličan strategiji „u pravom
trenutku“ (engl. just in time, JIT) koja obradu odrađuje što je moguće bliže onom trenutku
kada je to potrebno.
Primjerice, imamo program koji koristi tablicu s 5.000 vrijednosti. Pri pokretanju programa
ta tablica se izgenerira. Dok program radi, on koristi samo jedan mali dio te tablice. Zbog
toga bi imalo više smisla da se s tim malim dijelom tablice radi onda kada su podaci
potrebni, a ne odjednom. Nakon završetka obrade, tablica se može spremiti za daljnju
uporabu postupkom privremenog spremanja (engl. caching).
4.1.3 Trenutna lokacija
Ovo je princip kojim se različite akcije grupiraju i odrađuju odjednom [3]. Ovakav pristup
se često upotrebljava u programima koji učestalo pristupaju jednim te istim podacima.
Primjerice, metoda ima visok stupanj trenutnog lokaliteta ako se poziva puno puta u nekoj
vremenskoj jedinici. Imajući ovaj princip u vidu, možemo optimizirati razne programe.
Zamislimo da imamo program koji čita 100 datoteka, radi izvještaj za svaku datoteku i
onda piše 100 datoteka. Ovaj program se može napravit tako da se u petlju stave slijedeće
operacije.
Čitaj Napravi izvještaj Piši Čitaj Napravi izvještaj Piši ... Drugi način je napraviti tri odvojene petlje te svakoj petlji pridružiti jednu operaciju.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
26
Čitaj Čitaj Napravi izvještaj Napravi izvještaj Piši Piši ... Ovom tehnikom prvo čitamo 100 datoteka, zatim radimo 100 izvještaja te na kraju pišemo
100 datoteka. Jedan razlog zbog kojeg će ovo povećati performanse je način na koji I/O
predviđa zadatke.
Ako se čita 100 datoteka u petlji, sistem će predvidjeti naredne akcije te će moći poduzeti
mjere keširanja što će u konačnici rezultirati boljim performansama. Teorija prevoditelja
opisuje sve medije za pohranjivanje podataka kao što su memorija, keš procesora, čvrsti
diskovi i sl. Računala imaju hijerarhiju memorija te će se program izvršavati brže ukoliko
se predvidi ovakvo ponašanje.
4.2 Optimizacija nizova znakova
Optimizacija nizova je vrlo važna stavka kada se optimizira jedan sustav. Razlog tomu je
raširenost nizova. Sustavi koji su potpora nekom poslovnom procesu moraju na neki način
komunicirati s korisnicima. Jedini načina je komunikacija putem tekstualnih informacija
koje se prikazuju u obliku niza znakova. Stoga bilo kakva optimizacija nad nizovima na
niskom nivou će rezultirat bržim sustavom u konačnici.
Da bi mogli optimizirat rad sa nizovima prvo je potrebno razumjeti na koji su način nizovi
zastupljeni u .NET-u. Kao prvo, nizovi su objekti tipa System.String ali su također po
nekim definicijama i vrijednosti isto kao i osnovni tipovi podataka int, long ili float.
Razlog za to su neki operatori koji se koriste nad ovim tipom objekata. Tako da, iako je
string referenca na objekt, operatori jednakosti (== i !=) su definirani tako da uspoređuju
vrijednosti samog objekta a ne reference. Također bi valjalo napomenuti da operator +
zbraja nizove a operator [] pristupa točno određenom znaku u nizu. Niz definiramo
ključnom riječi string. Vrijednost postavljamo pridruživanjem teksta u dvostrukim
navodnicima.
string str = "Tekst";
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
27
Jednom kada kreiramo objekt tipa string on je nepromjenjiv. Sve metode koje rade s
nizovima vraćaju novi objekt tipa string i to je cijela bit optimizacije nizova znakova. U
slučaju da imamo niz1 i niz2 te ih spojimo operatorom += dobit ćemo novi objekt koji je
kombinacija ta dva niza te će niz1 sada pokazivati na novi objekt.
String niz1 = "crven"; String niz2 = "bijeli"; niz1 += niz2; // "crvenbijeli"
Postoji više načina za spajanje nizova. Jedni su sporiji, a drugi brži. Valja napomenuti da
brzina ovisi o broju spajanja. Nizovi znakova se mogu spajati na različite načine:
1. Operator +;
2. Metoda String.Concat();
3. Metoda String.Format();
4. Metode klase System.Text.StringBuilder();
Ono što ću pokušati dokazati u narednim primjerima je da najbolji način ovisi o konkretnoj
situaciji odnosno o količini manipulacija sa nizovima.
4.2.1 Korištenje String.Concat metode
4.2.1.1 Spajanje dva niza
Spajanje dva niza se vrši pomoću operatora + ili metode String.Concat. Ova dva načina su
identična jer ako pri spajanju koristimo operator +, C# prevoditelj će taj operator pretvoriti
u poziv metode String.Concat tako je ovdje razlika samo u sintaksi.
4.2.1.2 Spajanje tri niza
I u ovom slučaju najbolji izbor pri spajanju nizova je operator +.
4.2.1.3 Spajanje četiri niza
Ovdje se već može osjetiti razlika. Možemo nastaviti spajati nizove pomoću operatora +,
međutim, ako spajamo nizove na takav način jednom, neće se primijetiti velika razlika kao
u slučaju ako uzastopno nadodajemo nizove korištenjem operatora +. Već se u ovom
primjeru može razmišljati o korištenju metoda klase StringBuilder.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
28
4.2.1.4 Spajanje pet nizova
Kod spajanja pet nizova operatorom + performanse programa se drastično pogoršavaju.
Razlog leži u implementaciji ugrađenoj unutar .NET frameworka. Ako pogledamo
String.Concat metodu vidjeti ćemo da postoje opterećene (engl. overaload) metode. Za sve
slučajeve u kojima se vrši spajanje do četiri niza, postoje jasno definirane metode koja
primaju 2, 3 ili 4 parametra. Međutim, ako predamo više od 4 niza, pozvat će se
preopterećena metoda koja prima polje nizova [3], i. ta metoda radi sporije od prethodne tri
metode.
Iz gore navedenih scenarija jasno se vidi da je String.Concat metoda namijenjena za
spajanje od dva do četiri niza. Spajanje većeg broja nizova rezultirati će sporijim
spajanjem.
Primjer:
Sljedeći izraz će uvijek biti brži od bilo koje druge metode spajanja nizova. Razlog tomu je
optimiziranost same String.Concat metode koja najbrže radi kad se spajaju do četiri niza.
string rezultat = "prvi" + "drugi" + "treci" + "cetvrti";
Ako želimo postići isti rezultat korištenjem metoda klase StringBuilder dobiti će se sporiji
kod.
StringBuilder strBuil = newStringBuilder();
strBuil.Append("prvi"); strBuil.Append("drugi"); strBuil.Append("treci"); strBuil.Append("cetvrti");
Čak i u slučaju da se klasa StringBuilder inicijalizira sa zadanim kapacitetom, bit će
vidljiva sporost ovakvog pristupa.
4.2.2 StringBuilder klasa
Nakon što je pojašnjen način funkcioniranja String.Concat metode, te su navedene njezine
prednosti, pojasniti ću StringBuilder klasu. Ova klasa služi za manipuliranje velikim
nizovima znakova te velikom količinom istih. Razlog zbog čega je ova klasa napravljena i
za što se koristi krije se u samoj definiciji klase koja služi za pohranu niza znakova
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
29
System.String. Jednom kada inicijaliziramo niz znakova i pridružimo mu vrijednost, ta
varijabla je nepromjenjiva. Svaki puta kada koristimo metode nizova mi kreiramo novi
objekt u memoriji što uzrokuje novu alokaciju memorije za naš niz.
U situacijama u kojima moramo odraditi puno ponavljajućih operacija nad nizom,
preopterećenost koja se asocira s kreiranjem novog objekta može biti skupa u pogledu
resursa. Klasa StringBuilder se koristi kada se želi mijenjati sadržaj niza znakova, a da se
ne kreira novi objekt u memoriji. Ova klasa je izuzetno pogodna za ponavljajuća spajanja
kakva se događaju u petlji.
4.2.3 Kada koristiti StringBuilder klasu
Da bi odgovorili na ovo pitanje potrebno je prvo odraditi neke testove. U slijedećim
testovima uspoređivati će se performanse metoda StringBuilder klase s metodom
String.Concat. Ovim pristupom ću pokazati razlike između ova dva načina spajanja nizova
znakova koje uvelike ovise o količini nizova koji se spajaju kao i o ispravnoj inicijalizaciji
StringBuilder klase.
Razlog zbog čega je klasa StringBuilder znatno brža od metode String.Concat pri većem
broju spajanja leže u različitosti između StringBuilder klase i System.String klase.
StringBuilder klasa:
• Ima promjenjive međuspremnike;
• Može se mijenjati bez kopiranja.
String klasa:
• Ima nepromjenjive međuspremnike;
• Ne može se mijenjati;
• Svaka operacija vraća novi string;
• Kopiranja u petljama mogu povećati potrebnu količinu memoriju.
Početna pretpostavka je da će klasa StringBulder imati bolje performanse od metode
String.Concat pri spajanju pet ili više nizova. Pored toga, klasa StringBuilder koja se bude
inicijalizirala s kapacitetom, biti će brža od klase StringBuilder koja se inicijalizira s
regularnim konstruktorom. Razlog zbog čega je to tako leži u načinu na koji operacijski
sustav upravlja memorijom i kolekcijama. Ako ne inicijaliziramo kapacitet onda će klasa
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
30
svaki put kad dodamo još informacija pokušati realocirati novi komad memorije u koji će
smjestiti nove podatke. To izaziva povećane zahtjeve za memorijom. Ako predamo
parametar kapaciteta, onda govorimo klasi koliko će elemenata maksimalno moći pohraniti
prije nego realokacija dodatne memorije bude potrebna. U tom slučaju se odjednom na
početku alocira sva potrebna memorija i višestruki pristup nije potreban.
Zamislite StringBuilder s kapacitetom kao jednu praznu ljusku. Sve što dodajemo raste
unutar ljuske za razliku od StringBuildera koji nije inicijaliziran s kapacitetom, kod njega
bi se ljuska trebala svaki put nanovo izraditi.
Razlike u metodama spajanja na uzorku od n nizova te ponovljene 100.000 puta radi
dobivanja usporedivog vremena su vidljive na slici 6. Od početka je vidljivo da je
String.Concat() metoda najbolja do četiri niza. Nadalje, StringBuilder s kapacitetom je
uvijek bolji od StringBuilder-a bez kapaciteta. U slučaju dodavanja većeg broja nizova
tendencije linija bi se nastavile.
Slika 6 Odnos brzina različitih metoda pri spajanju više nizova
4.2.3.1 Korištenje pojedinačnih znakova
U dosadašnjim primjerima vidjeli smo da metoda Append, klase StringBuilder, prima
nizove no ta metoda može primati i druge tipove podataka. Ta metoda ima sveukupno 19
preopterećenih metoda. Ovo je vrlo korisno znati kada pridodajemo neke druge tipove
podataka. Primjer koji slijedi bazira se na pretpostavci da će Append metoda klase
StringBuilder će brže nadodati pojedinačni znak 'i' nego niz znakova ''i''.
0
50
100
150
200
250
300
2 4 6 8 10 12 14 16 18 20
String.Concat()
StringBuilder()
StringBuilder(capacity)
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
31
Prvo treba objasniti razliku između 'i' i ''i''. Prva vrijednost je pojedinačni znak tipa char te
će zauzimati samo 2 B memorije dok je druga tipa string te će zauzimat 20+(n/2)*4 B
memorije (n predstavlja broj znakova u nizu). Također valja istaknuti najvažniju razliku a
to je da je pojedinačni znak vrijednost, a niz znakova je referenca na objekt.
Rad s vrijednostima je uvijek brži od rada sa objektima stoga će korištenje vrijednosti u
Append metodi rezultirati kraćim vremenom izvođenja.
Kao što je vidljivo na slici 7, na 1.000.000 iteracija, metoda Append koji kao argument
prima karakter je brža od poziva metode Append koja prima niz.
Slika 7 Ispravna upotreba metode Append() kod rada s pojedinačnim znakovima
4.2.4 Pretvaranje integera u string
Postupak pretvaranja cijelog broja u niz znakova je vrlo jednostavan postupak koji je
podržan različitim metodama unutar .NET frameworka. Svaka cjelobrojna varijabla ima
metodu koja vraća njezinu vrijednost u obliku niza. Ovo je potrebno iz više razloga od
kojih je jedna potreba za prikazivanjem vrijednosti broja na zaslonu. Vizualne kontrole
koje prikazuju brojeve i slova poput tekstualnog polja ili labele mogu prikazati samo
vrijednosti tipa string. Svi znakovi koji se iscrtavaju moraju se iz svog izvornog oblika
pretvoriti u niz znakova.
4.2.4.1 Metoda koja se inače upotrebljava.
int cijeliBroj = 4; string broj = cijeliBroj.ToString();
U prethodnom isječku programskog koda inicijalizirana je varijabla tipa cijeli broj. Nakon
toga je inicijalizirana varijablu tipa string kojoj je pridružena vrijednost dobivena pozivom
ToString() metodu nad cijelim brojem. Ova pretvorba se odvija izuzetno brzo ali pitanje
38
56
0
10
20
30
40
50
60
1 2
Vrijednost
Referenca
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
32
koje se postavlja pri optimizaciji je može li još brže? S ovim problemom se sreće svaki
programer pri radu s grafičkim korisničkim sučeljem zbog toga jer se ova metoda poziva
puno puta—svaki put kada je potrebno neku neznakovnu vrijednost prikazati na zaslonu U
narednim isječcima programskog koda i grafovima pokazati ću kako se ova operacija može
ubrzati i do četiri puta.
Pretpostavka od koje polazimo je da većinu vremena pri pretvaranju cijelog broja u
znakovni niz troši algoritam izračuna koji se pri svakom pozivu izvršava. Ako unaprijed
definiramo veliku količinu brojeva i njihove znakovne ekvivalente, postupak bi se mogao
znatno ubrzati.
Prvi korak je definiranje zasebne klase koja će u sebi sadržavati dvije metode. Jedna
metoda će inicijalizirati onoliko brojeva koliko ćemo koristiti, a druga metoda će tražiti te
brojeve.
Objekt koji će sadržavati naše vrijednosti biti će tipa Dictionary te će izgledati ovako:
static Dictionary<int, string> stringovi = new Dictionary<int,string>();
Rječnik pojmova će biti statičan tako da će se samo jednom napraviti i nakon toga biti
dostupan tijekom trajanja čitave sesije.
Nakon toga je potrebno napraviti metodu koja će biti ekstenzija na naš tip cjelobrojnog
podatka, tako da će biti dostupna iz svih programa sustava. Metoda će izgledati ovako:
public static string VratiString(this int broj) { if (broj >= 0 && broj <= MAX) { return stringovi[broj]; } return broj.ToString(); }
Ako se parametar definira na takav način, programeri će prilikom razvoja programa u
Visual Studiu vidjeti napravljenu ekstenziju na način kako je to prikazano na slici 8. Tu
metodu će programeri moći izravno koristiti.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
33
Slika 8 Podrška od strane VisualStudia pri izradi ekstenzijskih(engl. extension) metoda
U našem slučaju u petlji od 65.535 brojeva inicijalizirati će se varijabla tipa string, te će za
svaki korak iteracije pozvati napravljena metoda. Isto će se ponoviti i za ToString()
metodu.
Za našu metodu niz se inicijalizira na slijedeći način::
string broj = cijeliBroj.VratiString(); Za standardnu ToString() metodu niz se inicijalizira ovako:
string broj = cieliBroj.ToString();
Rezultat koji se dobiva pri izvođenju 65.535 iteracija potvrđuje hipotezu. Naš rječnik će
prije dohvatiti definirane vrijednosti nego što će algoritam pretvoriti cijeli broj u niz.
Naravno ovo vrijedi samo za onaj raspon brojeva koji je definiran na početku izvođenja
programa.
Rezultat provedenog istraživanja vidljiv je na slici 9. Pretraživanje po rječniku je gotovo
četiri puta brže od algoritma pretvaranja brojeva u nizove.
Slika 9 Razlika u performansama nativne ToString() metode i naše implementacije
0
5
10
15
20
25
1 2
Lookup
Standard
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
34
4.2.5 Petlje i pojedinačni znakovi
Najgora moguća kombinacija su petlje i pretvaranje pojedinačnih znakova u niz da bi se
lakše provjerila jednakost nekog znaka s drugim znakom [3]. Dosta gotovih primjera
programskog koda na koje sam naišao koriste ovu lošu tehniku koja radi nepotrebne
alokacije memorije na gomili (engl. heap). Takve petlje se mogu pojednostavniti. Iako u
radu postoji poseban odlomak posvećen petljama mislim da je samo korištenje petlje
pokazatelj koliko je korištenje ToString() metode u nizu pogubno za performanse
aplikacije.
Inicijalno imamo dvije petlje koje služe za prolaz kroz niz znakova. Svaka petlja
uspoređuje trenutni znak s nekim drugim znakom—u našem slučaju znakom 'd'. Prva
petlja je puno brža jer radi manje alokacija na gomili uspoređujući znakove direktno.
Druga petlja radi sporije jer svaki put mora napraviti novi objekt tipa string. Ovo je očita
greška koju mnogi programeri često rade jer ne shvaćaju način funkcioniranja i alokacije
objekata.
Prva petlja:
for (int i = 0; i < niz.Length; i++){ if (niz[i] == 'g'){ brojac++; } } Druga petlja: for (int i = 0; i < niz.Length; i++){ if (niz[i].ToString() == "g"){ brojac++; } } Na slici 10 je vidljiva razlika u korištenju metode ToString() u usporedbi s indeksiranim
pristupom pojedinačnom znaku. Prva petlja je ujedno i brža jer se uspoređuju vrijednosne
varijable. Vrijednosne varijable za razliku od referentnih smještaju se na stog koji ne
zahtjeva čišćenje memorije od strane GarbageCollectora. Stog je ujedno i brži. Druga
petlja svaki znak prvo mora pretvoriti u niz. Za to je potrebna alokacija memorije na gomili
a potom se interno poziva metoda za uspoređivanje nizova. Degradacija u performansama
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
35
je golema i prvi algoritam je gotovo pet puta brži. U ovom primjeru je odrađeno 100.000
iteracija nad nizom duljine 45 znakova.
Slika 10 Pokazatelj skupoće korištenja ToString() metode
4.3 Optimizacija petlji
U ovom poglavlju opisati će se dvije petlje koje se danas najčešće koriste u .NET
frameworku s naglaskom na razlike u performansama. Petlje o kojima ćemo govoriti su for
i foreach.
4.3.1 For petlja
U računalnoj znanosti for petlja je programska struktura koja dopušta da se programski kod
izvede uzastopce zadani broj puta. Ova struktura je klasificirana kao iteracijski izraz. Od
drugih struktura (poput while petlje) razlikuje se po eksplicitno definiranim brojačem koji
pokazuje odgovarajući korak iteracije. Ove petlje se koriste kada je broj iteracija poznat.
Sintaksno u C# programskom jeziku postoji više vrsta for petlji a ja ću obraditi for petlju s
tri izraza. Ova petlja je nasljedstvo programskoj jezika C te je karakteriziraju tri izraza;
inicijalizator, validator i brojač. Klasičan primjer ove petlje bi bio sljedeći:
for (int i = 0; i < 10; i++) { //tijelo petlje } Petlja se izvršava deset puta. Brojač je na početku nula i u svakom prolazu kroz petlju se
povećava za jedan. Kada vrijednost brojača dođe do deset, izvođenje se prekida.
63
292
0
50
100
150
200
250
300
350
1 2
Index
ToString()
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
36
4.3.2 Foreach petlja
Ovaj izraz je idiom za iteraciju kroz stavke kolekcije. Foreach petlja se obično koristi
umjesto obične for petlje. Za razliku od ostalih petlji foreach petlja nema eksplicitno
definiran brojač nego radi na način da „radi dok ne odradiš za sve stavke“ umjesto klasično
definiranih petlji koje kažu „odradi n puta“. Ovakvom sintaksom izbjegava se tzv. „off-by-
one“ greška. Klasičan primjer foreach petlje je sljedeći izraz:
foreach (Stavka s in kolekcija.Stavke) { //tijelo petlje }
Prolazi se kroz sve elemente kolekcije stavki. U svakom prolazu trenutni element se
smješta u objekt tipa Stavke.
4.3.3 Razlika u brzini između for i foreach
For i foreach petlje u programskom jeziku C# imaju različite karakteristike, a prema tome
i različite performanse. Pošto foreach petlja koristi više varijabli, koristi i više memorije na
stogu. Zbog toga možemo pretpostaviti da će u izvršavanju biti sporija od for petlje. Na
jednostavnom primjeru izmjeriti ćemo njihove performanse kako bismo potvrdili našu
pretpostavku da for petlja iterira kroz polja brže nego foreach kroz listu. Također ću
pokazati da for petlja brže iterira kroz listu od foreach petlje.
Napisat ćemo dvije metode. Obje metode će primati argument tipa int[]. Razlika će biti u
tome što će prva metoda iterirati for petljom a druga metoda foreach petljom.
Na slici 11 je vidljivo da je for petlja brža pri izvođenju od foreach petlje za otprilike 10%.
Slika 11 Razlika u brzini for i foreach petlji
1116
1274
1000105011001150120012501300
1 2
for
foreach
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
37
4.3.4 Dekrementacija u for petlji
U narednim odlomcima ću pokušati primijeniti dekrementaciju i testiranje na nulu.
Umjesto da inkrementiramo od nule do maksimalne vrijednosti, dekrementirati ćemo od
maksimalne vrijednosti do nule. Ova optimizacija je takozvana mikro optimizacija te
proizlazi iz načina na koji x86 čipovi testiraju neki broj na nulu [3]. Sve što trebamo
napraviti je promijeniti klasični izraz kojeg susrećemo u for petlji:
for (int a = 0; a < max; a++) u
for (int a = max ‐ 1; a >= 0; ‐‐a) Ovakva optimizacija će rezultirati malo bržim izvođenjem petlje. Na 10.000.000 stavaka
dobit ćemo malo ubrzanje od otprilike 5% što je prikazano na slici 12.
Slika 12 Prikaz specifičnosti x86 arhitekture na performanse kretanja kroz petlju
4.3.5 Fuzija petlji
Ova je tehnika s kojom se više petlji zamjenjuje jednom petljom. Podobna je u situacijama
kad više polja ima jednaku duljinu. Zanimljivo je da u ovakvim situacijama ova tehnika
može povećati brzinu izvođenja preko 30%. U slijedećem primjeru ćemo spojiti dvije
petlje te ćemo grafom prikazati ubrzanje koje smo dobili.
Ako imamo dva polja cijelih brojeva definirana kao:
int[] poljeA = newint[max]; int[] poljeB = newint[max]; Te ako želimo obaviti operaciju nad svakim elementom od oba dva polja, možemo to
obaviti odvojeno. Po jedna for petlja za svako polje. U svakoj od tih for petlji obavljamo
operacije nad našim poljem. Isto tako možemo stavit oba sva izraza, koja smo mislili
koristiti odvojeno, u istu for petlju. Ja ću radi primjera i jednostavnosti obaviti samo
730
740
750
760
770
1 2
Dekrementacija
Inkrementacija
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
38
inicijalizaciju elemenata. PoljeA ću inicijalizirati na 1, a poljeB ću inicijalizirati na 2.
Inicijalizacija polja bi u neoptimiziranom primjeru izgledala ovako:
for (int i = 0; i < max; i++) poljeA[i] = 1; for (int i = 0; i < max; i++) poljeB[i] = 2; Zatim ćemo spojiti ove dvije petlje u jednu te ćemo dobiti sljedeći izraz;
for (int i = 0; i < max; i++) { poljeA[i] = 1; poljeB[i] = 2; }
Rezultati su vidljivi na slici 13. Pri 10.000.000 iteracija ćemo dobiti znatno, gotovo
dvostruko ubrzanje imajući u vidu da smo samo inicijalizirali elemente polja.
Slika 13 Spajanje petlji kao tehnika optimizacije
4.3.6 Odmotavanje petlji
Odmotavanje petlji je tehnika koja se sama od sebe nameće programeru. Cilj odmotavanja
petlji je povećanje brzine izvođenja same petlje na način da se smanji ili eliminira broj
instrukcija koje kontroliraju tok petlje [3]. Takve instrukcije su pokazivačka aritmetika ili
testiranje na kraja petlje za svaku iteraciju . Kad kažemo da se petlja odmota mislimo na
sljedeće:
0
50
100
150
200
1 2
Spojene petlje
Odvojene petlje
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
39
for (int i = 0; i < a.Length; i += 2) { if (a[i] == 3) { // ... } if (a[i + 1] == 3) { // ... } }
U prethodnom primjeru smo ispitivali da li je broj u polju jednak broju 3. Umjesto da
prođemo kroz sva polja jedan po jedan mi ćemo preskakati po dva polja, a unutar grananja
ćemo svaki put uvećavati iterator za jedan. Ovakav način optimizacije će biti najbolji ako
unaprijed znamo broj iteracija, inače postoji mogućnost da dobijemo iznimku pri
pristupanju indeksu koji ne postoji. Primjer bi bio polje od 10 elemenata i odmotavanje sa
odstupanjem od 3. Lako je uočiti da jednom kad bi počeli ići dalje od devetog elementa da
ne bi mogli pristupiti 11 ili 12 elementu. Da bi to izbjegli trebali bi provodit ispitivanja.
Takva ispitivanja bi koštala resursa te bi takvo odmotavanje bilo skuplje ali opet brže.
Na slici 14 prikazani su rezultati mjerenja na 10.000.000 iteracija. Iz prikazanih rezultata
se može vidjeti da su performanse optimiziranog programskog koda za oko 50% bolje od
neoptimiziranog.
Slika 14 Odmotavanje petlji kao tehnika optimizacije
4.3.7 Unswitching
Switching se odnosi na pravljenje odluka unutar petlje svaki puta kada se izvrši. Ako se
odluka ne mijenja dok se petlja izvršava može se obaviti takozvani „Unswitch“. To je
0
2
4
6
8
10
12
1 2
Odmotana
Uobičajena
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
40
tehnika kojom se odluke stavljaju izvan petlje radije nego u petlji. Evo primjera koda koji
zavisno o tipu sumiranja sumira brojeve u jednu ili drugu varijablu:
for (int i = 0; i < brojac; i++){ if (TIP_SUME == 1) { sumaNeto += i; } else { sumaBruto += i; } }
U prethodnom odlomku programskog koda, dio koji testira da li je suma tipa 1 će se odviti
za svaku iteraciju te će biti ista za vrijeme cijelog iteriranja. Programski kod se može
napisati i drugačije, tako da se testiranje na tip sume odvija samo jedanput što će
doprinijeti brzini izvršavanja
if (TIP_SUME == 1) { for (int i = 0; i < brojac; i++){ sumaNeto += i; } } else{ for (int i = 0; i < brojac; i++){ sumaBruto += i; } } Rezultati mjerenja prikazani na slici 15 pokazuju da se optimizirani programski kod izvodi
gotovo dvostruko brže od neoptimiziranog.
Slika 15 Unswitching kao tehnika optimizacije.
57
107
020406080
100120
1 2
Unswitch
Standard
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
41
4.3.8 Manje posla unutar petlji
Jedan od ključeva u pisanju efektivnih petlji je smanjivanje posla koji se treba obaviti. Ako
možete obaviti izraz, test ili bilo što drugo izvan petlje onda to uradite [1]. To je dobra
programerska praksa.
Recimo da imamo varijablu unutar neke klase. Ta klasa je referencirana unutar druge klase
koja se upotrebljava unutar petlje. Na našem primjeru ćemo pokušati 1.000.000 puta
pomnožiti broj s koeficijentom popusta.
for (int i = 0; i < brojac; i++){ suma = i * stopa.Popust.Koeficijent; }
Iz gore navedenog dijela programskog koda vidljivo je da svaki put kada trebamo
pomnožiti korak naše iteracije sa koeficijentom, moramo ići kroz dva objekta. Da smo
kojim slučajem novoj varijabli izvan petlje pridodali vrijednost koeficijenta te radili s tom
varijablom, dobili bi veliko poboljšanje performansi.
decimal koeficijent = stopa.Popust.Koeficijent; for (int i = 0; i < brojac; i++){ suma = i * koeficijent; }
Dodatna varijabla koju smo uveli i inicijalizirali je van petlje ubrzala nam je samu petlju
dvostruko što je vidljivo na rezultatima mjerenja prikazanima na slici 16.
Slika 16 Izbacivanje posla iz petlje.
4.4 Polja
Polja daju osnovnu funkcionalnost grupiranje više elemenata istoga tipa. Svaki programski
jezik implementira polja na neki način, a karakteristike implementacije polja nevezana za
vrstu .NET jezika su slijedeće:
97
206
0
100
200
300
1 2
Izvan petlje
Unutar petlje
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
42
• Polja imaju stalni kapacitet. Kapacitet polja ostaje ista nakon inicijalizacije. Ako
želite povećati kapacitet morate stvoriti novo polje željenog kapaciteta te nakon
toga kopirati prethodno polje u novo polje.
• Polja podržavaju indeksirani pristup. Ako želite pristupiti elementu polja možete
mu pristupiti putem njegovog indeksa.
• Polja podržavaju numerirani pristup. Možete pristupiti elementima polja tako što
ćete prolaziti kroz elemente foreach petljom.
• Memorija zauzeta poljem je neprekidna. .NET virtualni stroj (Common Language
Runtime, CLR) alocira polja na način da nema fragmenata u memoriji. Time se
osigurava veću brzina pristupa elementima polja.
U nastavku ću objasniti na koji način upotrebljavati polja da bi se dobilo na
performansama. Sama optimizacija polja se može obaviti vodeći računa o nekoliko
elemenata:
• Odabirite polja umjesto kolekcija;
• Upotrebljavajte čvrsto definirana polja u vidu tipova podataka;
• Upotrebljavajte krnja polja umjesto višedimenzionalnih polja.
4.4.1 Polja umjesto kolekcija
Od svih kolekcija, polja su najbrža. U slučaju da ne trebate neku posebnu funkcionalnost
poput dinamičke ekstenzije nad kolekcijama, onda bi bilo bolje da upotrebljavate obična
polja. Također, pri korištenju čvrsto definiranih polja izbjegavaju se operacije pakiranje i
raspakiravanje (engl. boxing and unboxing).
4.4.2 Čvrsto definirana polja
Koristite čvrsto definirana polja gdje god možete [1]. Ovim izbjegavate konverziju između
tipova podataka. U slučaju da definirate polja objekata i date mu vrijednost tipa integer
onda će se morati izvršiti pakiranje. To je proces u kojem sustav implicitno pretvara
podatak iz jednog tipa u drugi.
Na 1.000.000 iteracija ćemo inicijalizirati polje od 1.000.000 elemenata. Prvo polje će biti
tipa int[] a drugo polje će biti tipa object[]. Vrijednost koju ćemo pridruživati jednim i
drugim poljima će biti tipa int. U drugom polje će pri svakoj inicijalizaciji morati obaviti
pretvaranje iz tipa object u tip int. Na slici 17 je vidljiva golema razlika između ta dva
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
43
pristupa i koja ilustrira zbog čega je važno koristiti čvrsto definirana polja kad god je to
moguće.
Slika 17 Izbjegavanje pakiranja(engl. boxing).
4.4.3 Krnja polja
Krnja polja su polja sastavljena od drugih polja. Zašto ne reći višedimenzionalna polja?
Višedimenzionalna polja uključuju iste dimenzije. Ako definirate višedimenzionalno polje
4*5 onda znate da će svaki redak imati 5 stupaca. Krnja polja nisu takva i ona mogu imati
retke različitih duljina. Postavlja se pitanje namjene krnjih polja. MSIL (Microsoft
Intermediate Language) u koji se prevode instrukcije svih .NET programskih jezika ima
specifične instrukcije koje omogućuju optimizirani rad s jednodimenzionalnim poljima.
Nasuprot tome postoje višedimenzionalna polja kojima se pristupa istim kodom kojim se
pristupaju ostalim tipovima kolekcija i taj programski kod nije optimiziran za specifični tip
podataka. Slijedeći primjer prikazuje način na koji se definira krnje polje.
string[][] Address = newstring[2][]; // Krnje polje nizova Address[0] = newstring[1]; Address[1] = newstring[2]; Address[0][0] = "Mrezni servisi"; Address[1][0] = "Programsko inzenjerstvo"; Address[1][1] = "Baze podataka I";
4.4.4 Spljošćivanje polja
Spljošćivanje polja je tehnika kojom se dvodimenzionalna polja pretvaraju u
jednodimenzionalna. Slijedeći programski kod prikazuje način na koji se upotrebljavaju
spljoštena polja.
Inicijalizacija:
int[] polje = newint[duljina * visina];
0
20
40
60
80
100
int[] object[]
Čvrsto definirana
Pakiranje
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
44
Pristupanje nekom elementu polja:
polje[(Y * duljina) + X];
U dvodimenzionalnim poljima elementima pristupamo putem njihovih koordinata X i Y.
Zbog toga se pri pristupu pojedinom članu provode dvije provjere. Na primjeru ću pokazati
kako je pristup elementima dvodimenzionalnog polja dimenzija 1.000*1.000 elemenata
sporiji od pristupa jednodimenzionalnom polju dimenzija 1.000.000 elemenata.
Slika 18 prikazuje vrijeme trajanja pristupa elementima na ta dva načina. Vrijeme trajanja
pristupa podacima jednodimenzionalnog polja je gotovo 50% brže od pristupa podacima
dvodimenzionalnog polja.
To je korisno znati kod izrade matematičkih programa koji naveliko koriste
dvodimenzionalne matrice. Dvodimenzionalna polja su zbog provjera i operacija nad njima
sporija pri korištenju od jednodimenzionalnih polja. Sporost je prvenstveno uzrokovana
vremenom potrebnim za pristup indeksu odgovarajućeg elementa.
Slika 18 Spljoštivanje polja kao tehnika optimizacije.
4.5 Tipovi podataka
Promjene tipova podataka može znatno utjecati na smanjenje veličine programa i
povećavanju njegove brzine. Nekoliko načina kako se putem tipova podataka i polja mogu
povećati performanse pojedinog programa su slijedeći:
• Zbrajanje i množenje je puno brže korištenjem cijelih brojeva nego decimalnim
brojevima.
• Manje dimenzije polja omogućuju veću brzinu izvođenja programa.
• Manje pristupanja elementima polja osigurava veću brzinu izvođenja programa.
7
11
024681012
1 2
Spljoštena
Dvodimenzionalna
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
45
4.5.1 Dodatni podaci
Korištenje dodatnih podataka u obliku indeksa znači dodavanje informacija koje će
olakšati korištenje toga tipa podatka. Dodatni podaci se mogu dodati tipu podatka ili se
mogu čuvati u paralelnoj strukturi.
4.5.1.1 Duljina niza
Primjeri korištenja dodatnog podatka mogu se naći u različitim vrstama nizova. U
programskom jeziku C, niz znakova je polje pojedinačnih znakova zaključeno sa znakom
\0. Da bi se izračunala duljina niza moramo proći kroz svaki znak sve dok ne dođemo do
znaka \0. U tom trenutku ćemo znati koliko je dugačak niz. U slučaju C# programskog
jezika postoji podatak na početku niza koji nam govori poje je duljine taj niz. Za razliku od
jezika C gdje treba izbrojati pojedinačne znakove, u programskom jeziku C# potrebno je
samo pročitati prvi podatak niza.
Sa stajališta ekonomičnosti korištenja tipa podatka, znatno je efikasnije održavati podatak
o duljini niza nego ga računati svaki puta kada je taj podatak potreban.
4.5.1.2 Neovisne, paralelne strukture indeksa
Nekada je puno lakše i efikasnije manipulirati sa indeksima tipova podataka nego sa
samim podacima. Ako su informacije u našim tipovima podataka velike i zahtjevne pri
pomicanju (npr. Prijenos na drugu lokaciju čvrstog diska ili na drugu mrežnu lokaciju),
onda će sortiranje i traženje po indeksu biti puno brže nego direktno manipuliranje
podacima. U slučaju da je svaki podatak velik, možemo kreirati dodatnu strukturu koja
sadrži pokazivače na detaljne informacije. U ovom slučaju sve pretrage i sortiranje će se
vršiti u memoriji, a čvrstom disku se treba pristupiti samo jedanput, kada znamo točnu
lokaciju datoteke. Taj princip na primjer koriste relacijske baze podataka kako bi se
ubrzalo pretraživanje indeksiranih podataka.
4.5.2 Keširanje
Keširanje podataka znači čuvanje istih na način da se do njih lako može doći kada
zatrebaju [1]. Ako npr. PC-kasa svaki put kada radite novi račun mora ići u bazu i
dohvaćati artikle to će možda usporiti rad programa. Ali ako dohvatite sve artikle u
memoriju, onda brzo i efikasno možete pretraživati po njoj. U slučaju da netko unese novi
artikal i mi ga zatrebamo, a artikal nije u kešu onda će naravno biti potrebno otići u bazu
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
46
podataka. No to su granični slučajevi u keširanju zbog kojih performanse neće patiti kada
gledamo cjelinu.
Poboljšanje performansi uslijed keširanja informacija ovisi o tome koliko je sama
informacija tražena. Općenito gledajući, ako je potrebno više vremena da se generira novi
element te ako se taj element često generira onda je pametno keširati ga. Što je jeftinije
doći do novog elementa i sačuvati ga u privremenoj memoriji to je primjena tog postupka
vrjedniji. Kao što je slučaj s drugim vrstama optimizacije, tako je i kod ove složenost
izrade i podložnost greškama vrlo izražena.
Princip keširanja podataka danas se u velikoj mjeri koristi kod dohvaćanja podataka iz web
aplikacija. Pošto je dohvat podataka iz baze vrlo zahtjevna operacija, a u web aplikaciji u
kratkom vremenu može doći velik broj upita, podaci se pri prvom upitu keširaju. Nakon
toga određeno vrijeme će svi upiti dohvaćati podatke, ne izravno iz baze već iz privremene
memorije u koju su oni prethodno spremljeni. Na takav način se omogućuje da web
aplikacije uspješno opslužuju velik broj paralelnih upita.
4.6 Izrazi
Dosta posla u programima se odrađuje putem matematičkih i logičkih izraza. Složeni izrazi
su skuplji gledajući resurse koje troše. U ovom potpoglavlju su prikazani načini
optimizacije matematičkih i logičkih izraza.
4.6.1 Algebarski izrazi
Algebarski izrazi se mogu koristiti kako bi se skuplje operacije zamijenile jeftinijima.
Primjerice, sljedeći izrazi su logički jednaki.
( !a && !b ) !( a && b ) Ako se izabere druga operacija štedi se jedna operacija negacije. Iako ušteda jedne
operacije negacije na prvi pogled može izgledati nevažna, u praksi se događa upravo
suprotno. Zamislite matematički program koji mora napravit 10.000.000 ovakvih
operacija.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
47
Na slici 19 je prikazana razlika između programskog koda koji u izrazima upotrebljava
jednu odnosno dvije negacije. Program koji koristi samo jednu negaciju gotovo 50%
efikasniji od programa koji koristi dvije negacije.
Slika 19 Prikaz skupoće korištenja izraza na primjeru negacije
4.6.2 Smanjivanje snage
Smanjivanje snage znači zamjenu skupljih operacija jeftinijima. Neke od mogućih zamjena
su slijedeće:
• Zamijenite množenje nizom zbrajanja.
• Zamijenite potenciranje nizom množenja.
• Zamijenite double, float i decimal tipove podataka int-om
• Zamijenite množenje i dijeljenje cijelih brojeva zbrajanjem s posmicanjem bitova.
4.6.3 Inicijalizacija pri prevođenju
Ako se neka varijabla koristi u dosta poziva metoda, te ako se ta varijabla nikad ne mijenja
onda bi je bilo dobro definirati kao konstantu. To se također odnosi na operacije poput
množenja, dijeljenja, zbrajanja i sl. Bilo koju operaciju koja se puno poziva te se svaki put
mora izračunati, a nikada se ne mijenja je najbolje izračunati unaprijed i definirati ju kao
konstantu. Primjer bi bio računanje broja PI svaki put kada nam zatreba. Broj PI se može
izračunati putem različitih nizova no najbolje bi bilo izračunati ga odmah i staviti u
konstantu.
Recimo da imamo metodu koja računa PI putem Newtonove formule:
2!
2 1
73
110
0
20
40
60
80
100
120
Negacije Negacije
Jedna
Dvije
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
48
Tu metodu ćemo realizirati putem rekurzije. Prva metoda poziva drugu metodu koja je
rekurzivna i poziva samu sebe određen broj puta. Rekurzivna metoda zbraja podatke dok
prva metoda množi dobiveni rezultat s dva i vraća konačan rezultat.
static double PI(){ return 2 * F(1); } static double F(int i){ if (i > 60) { return i; } else { return 1 + (i / (1 + (2.0 * i))) * F(i + 1); } }
Ova metoda je vrlo spora zbog rekurzivnog algoritma. Nerekurzivni algoritam bi donekle
ubrzao trajanje operacije, ali ostaje činjenica da ćemo kod npr. 100.000 poziva ove metode
potrošiti znatno više vremena nego ako jednom izračunamo rezultat i pohranimo ga kao
konstantu. Na slici 20 je vidljiva golema razlika između ta dva pristupa.
Slika 20 Povećanje performansi predkalkuliranjem vrijednosti i korištenjem konstanti
Na programeru je odluka o tome hoće li se podatak računati dok radite ili će se prvo
izračunati, sačuvati i nakon toga koristiti kada zatreba. Ako se rezultati upotrebljavaju
mnogo puta možda je bolje jednom ih izračunate i sačuvati, te ih naknadno koristiti kada
zatrebaju. Ova odluka se manifestira na nekoliko načina. Najjednostavniji način je da se
izračuna ono što je potrebno van petlje, kao što je pokazano u poglavlju o petljama. Na
mnogo kompliciranijem nivou, možda će biti potrebno napraviti cijelu tablicu već
izračunatih podataka kada se program pokrene (prethodno opisani primjer pri pretvorbi
cijelih brojeva u znakovni niz podataka). Podaci se mogu zapisati u datoteku, bazu
podataka ili se izravno uvrstiti u program (engl. embed).
2
528
0100200300400500600
1 2
Konstanta
Metoda
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
49
Optimiziranje tehnikom izračunavanja prije korištenja ima nekoliko oblika:
• Računanje rezultata prije nego se program pokrene i inicijalizacija konstanti tim
rezultatima.
• Računanje rezultata prije nego se program pokrene i pridruživanje tih rezultata
običnim varijablama.
• Računanje rezultata prije nego se program pokrene, pisanje podataka u datoteku i
čitanje datoteke pri pokretanju programa.
• Računanje rezultata jedanput kada se program pokrene i korištenje istih tijekom
rada programa.
• Računanje što je više moguće van petlje smanjujući posao unutar petlje.
• Računanje podataka kad su prvi put potrebni i korištenje istih tijekom rada
programa.
4.6.4 Eliminiranje podizraza
Ako u programu postoji izraz koji se ponavlja više puta, bolje ga je pridružiti varijabli i
koristiti varijablu umjesto izraza. Na takav način smanjuje se višestruka evaluacija samog
izraza. Pogledajmo primjer programskog koda u kojem se više puta koristi isti izraz.
a = b * c + g; d = b * c * d;
Taj programski kod se može promijeniti na slijedeći način čime se broj operatora u
izrazima smanjuje za jednu operaciju množenja. U prethodnom potpoglavlju ovog
poglavlja koje se bavi algebrom u optimizaciji, opisano je što znači ušteda pri korištenju
operatora.
tmp = b * c; a = tmp + g; d = tmp * d;
4.7 Metode
Metode ili funkcije su skup instrukcija koje se često izvršavaju. To je poprilično jasno te na
prvi pogled izgleda kao da se ne može baš puno optimizirati. U slijedećim odlomcima ću
pokazati suprotno. Ono što je primamljivo kod optimizacije metode je to što se metode
potencijalno mogu pozivati veliki broj puta. Ostvareno malo poboljšanje performansi
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
50
kritičnih metoda koje se pozivaju mnogo puta može donijeti znatno kraće manje vrijeme
izvršavanja na razini cjelokupnog sustava.
4.7.1 Ručno kreiranje inline metoda
U raznim verzijama C i C++ jezika, inline metoda je metoda nad kojom prevoditelj
izvršava inline ekspanziju. To znači da programer zahtjeva od prevoditelja da umetne
cijelo tijelo metode na svako mjesto gdje je ta metoda pozvana radije nego da generira kod
na mjestu poziva metode. C# ne dopušta programeru da definira metodu na taj način jer je
prevoditelj dovoljno „pametan“ da sam shvati treba li izvršiti takvu operaciju nad
metodom. Ono što programer može napraviti da zaobiđe takvo ponašanje prevoditelja je
ručno kopiranje tijela funkcije na svako mjesto gdje se funkcija izvršava.
U našem primjeru ćemo napraviti 3 metode.
1. Prva metoda će biti ručno kreirana inline metoda koja će inkrementirati vrijednost
dvadeset puta.
2. Druga metoda će inkrementirati vrijednost deset puta.
3. Treća metoda će inkrementirati vrijednost deset puta a zatim će pozvati drugu
metodu.
static int InkrementirajInline(int v) { // Ovo je metoda koja ce se jedanput pozvati // Sadrzava 20 inkrementiranja v++; v++; v++; v++; v++; v++; v++; v++; v++; v++; v++; v++; v++; v++; v++; v++; v++; v++; v++; v++; return v; } static int Inkrementiraj2(int v) { // Ova metoda radi deset inkrementiranja // zatim radi jos deset inkrementiranja putem druge metode v++; v++; v++; v++; v++; v++; v++; v++; v++; v++; v = Inkrementiraj1(v); return v; }
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
51
static int Inkrementiraj1(int v) { // Ova metoda radi deset inkrementiranja v++; v++; v++; v++; v++; v++; v++; v++; v++; v++; return v; } Na primjeru 1.000.000 poziva metode InkrementirajInline i Inkrementiraj2 vidljiva je
razlika u vremenu poziva. Dobivamo rezultat koji je vidljiv na slici 21. Inline metoda je
brža za 32 nanosekunde po pozivu.
Slika 21 Performanse pri korištenju inline metoda
U ovom primjeru radi se o tome da u strukturnom programiranju, programeri koriste
funkcije kako bi organizirali složene programe, ali u samoj jezgri računala izvršavaju se
pojedinačne instrukcije a ne skupine opisane funkcijom. Prevoditelj prevodi poziv funkcije
u nizove instrukcija koje se izvršavaju na stogu (engl. stack). To objašnjava na
konceptualnom nivou zbog čega su inline metode brže. JIT (Just In Time) prevoditelj će pri
prevođenju pokušati napraviti metodu inline ali u osnovi to radi samo sa malim metodama.
4.7.2 Statične metode
Kada je metoda u svome potpisu ima ključnu riječ static onda se kaže da je ta metoda
statična. Statična metoda se ne izvršava nad određenom instancom metode te je nemoguće
unutar takve metode referirati objekt s ključnom riječi this. Metoda instance operira nad
instancom objekta a toj instanci se može pristupiti sa ključnom riječi this.
Statične metode su u osnovi uvijek brže od metoda instanci. Postoji nekoliko razloga zašto
je to tako. Metode instanci će u najboljem slučaju biti pozvana s ključnom riječi this tako
da će takva metoda uvijek imat vrijeme poziva sporije za onoliko koliko je potrebno za
izračun referenca metode. Također, metode instanci su u .NET CIL međujeziku
implementirane s instrukcijom virtualnog poziva (engl. call virtual) callvirt, a ta instrukcija
107
139
0
50
100
150
1 2
Inline
Uobičajeno
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
52
zahtjeva dulje vrijeme izvođenja. Iako je samo izvršavanje statične metode i metode
instance isto, razlika je u pozivanju metode. Razlika koju dobijemo pri testiranju,
prikazana na slici 22 odnosi se na pozivanje metode.
Slika 22 Dominacija statičnih metoda pri mjerenju brzine poziva
4.7.3 Manji broj parametara u metodama
Jedan od načina na koji se metoda može optimizirati je smanjenje broja parametara čime se
smanjuje korištenja memorije na stogu (engl. stack).
static int Metoda1(int a, int b, int c, int d, int e, int f) static int Metoda2(int a, int b, int c)
Vrlo je važno tijekom testiranja rezultata optimizacije izbaciti jednaku iznimku unutar obje
metode. To je potrebno kako bi se prevoditelj zbunio te kako ne bi sam optimizirao metodu
te je napravio inline.
Slika 23 Utjecaj smanjivanja broja parametara na brzinu poziva metode
Prva metoda prima neke parametre koje ne koristi te će na 10.000.000 iteracija biti sporija
za gotovo 10% po pozivu odnosno oko 3.5 nanosekundi po pozivu metode. Na slici 23 vidi
se koliko je puta brža druga metoda, mjereno u milisekundama.
310
320
330
340
350
1 2
Statična
Instancirana
354
389
330
340
350
360
370
380
390
400
1 2
Točan broj paramaetara
Višak parametara
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
53
Razlika u vremenu poziva metoda postoji jer Metoda1 mora kopirati tri dodatna,
nepotrebna parametra svaki put kada se poziva.
4.7.4 Performanse parametara metode i registri
U ovom odlomku nas zanima da li i na koji način redoslijed parametara utječe na
registraciju varijabli. Također nas zanima može li se iskoristiti u svrhu ubrzavanja
izvođenja programskog koda. Pokušati ću izmjeriti razlike između loše poredanih i dobro
poredanih parametara.
Polazimo od pretpostavke da će dobro poredani parametri ubrzati rad programa samo u
slučaju da te parametre često koristimo unutar metode.
Ovo je moguće jer pri prevođenu koda prevoditelj koristi optimizaciju koja se sastoji od
pozivanja metode koja se zove FASTCALL. Ova metoda stavlja prva dva parametra u
registar te na taj način korištenje istih postaje brže.
Napravit ćemo dvije metode. Prva metoda Prva2 će na 10.000.000 iteracija testirati prva
dva parametra na neku vrijednost. Druga metoda Zadnja2 će na isti broj iteracija testirati
dva posljednja parametra na neku vrijednost.
static bool Prva2(int a, int b, int c, int d, int e, int f) { // Ova mtoda testira prva dva parametra unutar petlje. for (int i = 5; i < 10000000; i++) { if (a == i) { return true; } if (b == i) {
return true; } } if (c == 1 || d == 1 || e == 1) { return true; } return false; }
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
54
static bool Zadnja2(int a, int b, int c, int d, int e, int f) { // Ova metoda testira zadnja dva parametra unutar petlje. for (int i = 5; i < 10000000; i++) { if (e == i) { return true; } if (f == i) { return true; } } if (a == 1 || b == 1 || c == 1) { return true; } return false; } Programska logika ovih metoda je potpuno nevažna. Treba samo obratiti pažnju da u prvoj
metodi testiramo prva dva parametra, a u drugoj metodi dva posljednja parametra. Kada
se metode prevedu u C# jeziku, njihovi parametri se pri pozivu metode stavljaju na stog.
Interno, metoda koristi parametre sa stoga. Zbog metode FASTCALL prva dva parametra
se stavljaju u registar. U strojnom jeziku, registri su ekstremno brza memorija, odnosno
keš. Primjer gdje se to koristi je iteratorska varijabla i koja se obično sprema u registar radi
bržih operacija nad njom. U našem primjeru zbog pristupa tim parametrima iz registara, a
ne sa stoga dobiveno je ubrzanje od gotovo 10%.
Slika 24 Utjecaj redoslijeda parametra na brzinu izvođenja metode
4.7.5 Preopterećenje nativnih metoda
Postoji mogućnost preopterećenja nativnih metoda koje nekad možemo iskoristiti za
dobivanje performansi. To se obavlja pomoću metode Equals. Metoda Equals se nalazi
96
105
90
95
100
105
110
1 2
Prva dva
Zadnja dva
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
55
unutar System.Object. Da bi se koristila standardna implementacija metode Equals, naša
vrijednost mora biti zapakirana i predana kao instanca tipa System.ValueType. Potom
metoda Equals koristi refleksiju da bi obavila uspoređivanje. Iz ovoga se može zaključiti
da bi pretvorba između različitih tipova podataka te korištenje refleksije lako moglo biti
sporije od naše vlastite implementacije koja radi s točno određenim tipom podataka. Kao
rezultat toga, implementacija metode za naš specifični tip podatka može biti brža.
public struct Rectangle{ public double Length; public double Breadth; public override bool Equals(object ob){ if (ob isRectangle) return Equals((Rectangle)ob); else returnfalse; } private bool Equals(Rectangle rect){ return this.Length == rect.Length &&this.Breadth == rect.Breadth; } }
4.8 Dretve
Ovo potpoglavlje bavi se povećavanjem efikasnosti koda koji se izvršava u dretvama.
Dretve su oblik paralelizacije izvođenja programskog koda na razini procesa. Svaki se
proces može podijeliti u određen dretvi koje se izvršavaju prividno paralelno. U ovom
potpoglavlju će se obraditi slijedeći elementi koji utječu na optimizaciju programskog
koda:
• Smanjiti kreiranje dretvi.
• Koristiti ThreadPool klasu kada je potrebna dretva.
• Koristiti paralelne, radije nego sinkrone obrade.
4.8.1 Smanjiti stvaranje dretvi
Dretve koriste upravljane (engl. managed) i neupravljane(engl. unmanaged) resurse te su
zbog toga skupe tijekom inicijalizacije [3]. Ako se dretve često kreiraju to može uzrokovat
usko grlo na procesoru koji cijelo vrijeme izmjenjuje dretve. Slijedeći odlomak
programskog koda prikazuje stvaranje i održavanje nove dretve u svakoj iteraciji. Rezultat
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
56
toga je puno procesorskog vremena utrošenog na izmjene dretvi. Također,
GrabageCollector se stavlja pod pritisak jer treba počistiti sve nepotrebne resurse. Ovo je
način kako ne bi trebalo inicijalizirati dretve:
for (int i = 0; i < m; i++){ // Kreiraj i pokreni dretvu
Thread th = newThread(newParameterizedThreadStart(MojaFunkcija));
th.Start(i); }
Trajanje kreiranja dretvi na ovaj način će biti jako dugo. U slijedećem primjeru se pokazati
kako korištenje klase ThreadPool može smanjiti to vrijeme.
4.8.2 ThreadPool klasa
ThreadPool klasa se koristi kada želimo izbjeći skupu inicijalizaciju dretvi. Slijedeći
odlomak programskog koda ilustrira pokretanje dretvi pomoću te klase.
for (int i = 0; i < m; i++){ ThreadPool.QueueUserWorkItem(newWaitCallback(MojaFunkcija), i); }
Nakon što se pozove metoda QueueUserWorkItems, metoda se stavlja u red za izvršavanje,
a trenutna dretva nastavlja s radom. ThreadPool klasa koristi dretve iz aplikacijskog skupa
dretvi kako bi izvršila metodu koja joj je poslana.
Slika 25 Utjecaj korištenja ThreadPool klase pri inicijalizaciji dretvi
Ovo se događa odmah nakon što prva dretva postane dostupna. Dok smo prije morali
eksplicitno kreirati svaku dretvu, u ovom slučaju koristimo već kreirane dretve u sistemu i
4
8392
0
2000
4000
6000
8000
10000
ThreadPool
Threading
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
57
prosljeđujemo im posao. Razlika u performansama je ogromna i inicijalizacija korištenjem
ThreadPool klase je čak 2.000 puta brža.
4.8.3 Paralelni poslovi
Prije nego se implementira asinkroni programski kod, dobro je razmotriti utjecaj
paralelnog izvođenja više poslova na ukupne performanse sustava. Povećavanje
paralelizma može imati golemi utjecaj na performanse programa. Kao što smo vidjeli,
dodatne dretve konzumiraju sistemske resurse poput procesora, memorije, diska i mreže.
Pri donošenju te odluke važno je vidjeti da li vam nove dretve pomažu ili odmažu u
performansama.
Korištenje paralelnih procesa najbolje dolaze do izražaja u situacijama gdje jedan proces
nije ovisan o rezultatu drugog procesa na način da ne treba čekati na njegov završetak i
implementirati posebnu sinkronizaciju. Ako proces koristi I/O operacije, on ima koristi od
vlastite dretve jer dretva može pauzirat dok druga dretva koristi isti resurs. No, ako je
posao usko vezan uz intenzivno korištenje procesora, onda će paralelno izvršavanje imati
nepovoljan učinak na performanse.
4.9 Obrada iznimki
Strukturirano baratanje iznimkama korištenjem try/catch blokova je preporučeno pri izradi
robusnih aplikacija. Svakako bi u tom slučaju trebalo koristiti i finally blok kako bi bili
sigurni da su resursi oslobođeni i zatvoreni čak i u slučaju iznimke.
Iako je baratanje sa iznimkama preporučeno pri objektno orijentiranom razvoju, to ipak
nije tako jeftino u pogledu resursa kao što bi se na prvi pogled moglo pomisliti. Bacanje i
hvatanje iznimki je izuzetno skupa operacija [3]. Zbog toga se preporučuje da se iznimke
bacaju i hvataju samo onda kada je to stvarno potrebno, a ne da bi se njima kontrolirala
logika programa.
Evo nekih natuknica o kojima treba razmisliti kako bi se iznimke na ispravan način
koristile u programu:
• Ne koristiti iznimke da bi kontrolirali tok programa.
• „Bolje spriječiti nego liječiti.“ Koristiti validacije umjesto iznimki gdje god je to
moguće.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
58
• Višestruko bacanje iznimki je izuzetno skupo.
4.9.1 Ne koristiti iznimke da bi kontrolirali tok programa
Bacanje iznimki je skupo. Zbog toga nije dobro koristiti iznimke da bi kontrolirali tok
programa. Ako možete očekivati seriju događaja koja će se vrlo vjerojatno dogoditi
tijekom rada vaše aplikacije, možda ne treba uopće bacati iznimke.
Slijedeći odlomak koda vraća logičku vrijednost true ako uspije naći broj u kolekciji. U
slučaju da ne vrati ništa onda baca iznimku koja se propagira i hvata.
for (int i = 0; i < m; i++){ if (polje[i] == broj) return true; } Throw new Exception("Broj nije pronadjen");
Ovo ponašanje je očekivano sa stajališta programske logike. Ako imamo kolekciju brojeva
i tražimo jedan broj u njoj, onda postoji mogućnost da nećemo naći traženi broj. Slijedeći
odlomak koda vraća vrijednost false u slučaju da ne može naći broj u kolekciji.
for(int i=0;i<m;i++){ if (polje[i] == broj) return true; } return false;
Vraćanje informacije o grešci putem neke vrijednosti je kritično za performanse.
Izbjegavanjem korištenje iznimaka tamo gdje stvarno nisu potrebne je uobičajena tehnika
kojom se povećavaju performanse objektno-orijentiranih aplikacija. Naravno to ne znači da
u programu treba izbjegavati ispitivanje grešaka i nenormalnog ponašanja, već ih samo
obrađivati na onaj način koji je za danu namjenu optimalan sa stajališta performansi. Na
slici 26 je vidljivo da se korištenjem ove tehnike mogu postići gotovo dvostruko bolji
rezultati.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
59
Slika 26 Utjecaj korištenja iznimki pri otkrivanju grešaka na performanse programa
4.9.2 Koristiti validacije umjesto iznimki
Ako znate da možete izbjeći određeni tip greške pisanjem programskog koda koji
provjerava i sprječava mogućnost njezinog nastanka, onda to svakako učinite. Primjerice,
ako dijelite s brojevima koje dobivate iz nekog izvora te postoji mogućnost da neki brojevi
nisu inicijalizirani, prvo provjerite da li su inicijalizirani prije nego dijelite s njima.
Slijedeći odlomak programskog koda pokazuje koja je razlika između validacije i try/catch
bloka.
Try catch:
try{ return djeljenik / djelitelj; } catch(Exception e){ return System.Double.NaN; } Validacija:
if (dijeljitelj != 0) { return djeljenik / djelitelj; } return System.Double.NaN;
Rezultati mjerenja gdje se validacijama sprječava nastanak greške i izbacivanje iznimki
ovisi o učestalosti nastanka greške koju sprječavamo. U našem primjeru čiji rezultati su
prikazani na slici 27, na slučajno izabranom skupu podataka performanse su gotovo pet
puta bolje u slučaju kada je programski kod sprječavao nastanak greške u odnosu na kod
koji je po nastanku grešku registrirao korištenjem iznimaka.
9
21
0
5
10
15
20
25
1 2
Bez iznimke
S iznimkom
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
60
Slika 27 Utjecaj sprječavanja grešaka na povećanje performansi programa
4.9.3 Ponovno bacanje iznimaka je skupo
Trošak ponovnog bacanja iznimke je gotovo jednak kao i stvaranje nove iznimke. Slijedeći
odlomak koda prikazuje hvatanje i ponovno bacanje iznimke.
try{ //akcija koja uzrokuje bacanje iznimke... } catch(Exception e){ //rad nad iznimkom i njeno ponovno bacanje throw; } U pravilu se iznimke ponovno bacaju samo onda tu iznimku želite propagirati klasama
nadređene razine te tim putem programu odnosno korisniku dati više informacija o samoj
grešci.
46
258
0
50
100
150
200
250
300
1 2
S validacijom
Bez validacije
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
61
5 ZAKLJUČAK
Performanse čine samo djelić kvalitete cijele aplikacije te obično nisu ni toliko važne. Uz
to, optimizirani kod čini samo djelić performansi te obično nije najvažniji. Dobra
arhitektura aplikacije, detaljan dizajn ili dobra struktura podataka će imati veći utjecaj na
cjelokupne performanse sustava. Sve prethodno navedeno spada u elemente kojima se bave
različite strategije optimizacije. To je nešto što bi se trebalo obaviti prije nego što se
bacimo na optimiziranje programskog koda. Također bi trebalo razmisliti i o poboljšanju
strojne opreme pošto njezine cijene stalno padaju te se nekada nabavkom nove opreme
može postići znatno poboljšanje performansi sustava uz najmanji mogući trošak.
Iz opisanih tehnika optimizacije je vidljivo koja su područja najizloženija neefikasnom
kodu. Najveći dio rada bavi se opisom tih mjesta te sam na njih utrošio i najviše vremena
pišući i testirajući kod. Iako na prvi pogled izgleda da se pravila odnosno preporuke
optimizacije mogu vrlo jednostavno i jasno preslikati u konkretan programski kod, to u
praksi baš i nije uvijek tako. Optimizacija koda je škakljiv posao koji zahtjeva puno
priprema. Kod optimizacije je vrlo važno prvo identificirati neefikasan kod, zatim ga
optimizirati te naposljetku izmjeriti ostvarene promjene. Moglo bi se reći da je testiranje i
mjerenje najvažniji dio optimizacije jer bez njih optimizacija baš i nema puno smisla.
Iako je izrada ovog dokumenta u nekim trenutcima bila naporna, uživao sam otkrivati nove
načine optimizacije koji su unaprijedili moje programerske sposobnosti. Moram također
priznati da sam neke stavke iz ovoga rada već primijenio u svojem svakodnevnom poslu te
se nadam da će ovo istraživanje pomoći i drugima.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
62
6 LITERATURA
[1] Sam Allen, „Dot Net Perls“, http://www.dotnetperls.com (20.10.2011.)
[2] Kalid Azad, „Understanding the Pareto Principle (The 80/20 Rule) “,
http://betterexplained.com/articles/understanding-the-pareto-principle-the-8020-rule/
(20.10.2011.)
[3] Rico Mariani, Brandon Bohling, Connie U. Smith, Scott Barber, „Improving .NET
Application Performance and Scalability“, Microsoft Press, 2004.
[4] Steve McConnell, „Code Complete“, Microsoft Press, 2004.
[5] Voltaire, Voltaire's Philosophical Dictionary, Carlton House, New York, 1900,
http://www.archive.org/stream/voltairesphiloso18569gut/18569.txt (20.10.2011.)
[6] William A. Wulf, „A Case Against the GOTO“, Proceedings of the ACM annual
conference ACM '72—Volume 2, ACM, New York, USA, 1972.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
63
7 SAŽETAK
Ovaj rad daje odgovore na neka uobičajena pitanja u optimizaciji. Glavne točke rada su
strategije i tehnike optimizacije. Opisane su strategije optimizacije, generalno je objašnjena
optimizacija i njezine temeljne značajke s naglaskom na performanse i njihovo različito
tumačenje. Pokazuju se koraci koje treba poduzeti prije optimizacije koda da bi se izbjegao
negativan utjecaj optimiziranja na ispravnost i čitljivost koda. Također se opisuju mjerenja
kao jedinog sredstva prepoznavanja poboljšanja performansi. Pojašnjeni su i neki
uobičajeni izvori uskih grla te kako ih izbjeći. Tehnike optimizacije na najnižoj razini su
praktično ilustrirane primjerima programskog koda u C# programskom jeziku koji
prikazuju ostvarene rezultate optimizacije. Tehnike optimizacije će obraditi najučestalije
slučajeve na koje programer može naići poput optimizacije nizova, petlji, tipova podataka i
metoda.
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
KLJUČNE RIJEČI:
Optimizacija, strategije optimizacije, tehnike optimizacije, proces optimizacije, C#
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
64
8 ABSTRACT
This thesis answers some common optimization questions. Main focus points of this paper
are optimization strategies and techniques. Fundamental characteristics of optimization
strategies are explained in general with focus on performances and their different
interpretations. Important steps that developer has to take in this process, in order to avoid
negative impact of optimization on correctness and readability of code are shown.
Measurement, as the only instrument in recognizing performance improvement is
described. Common sources of bottlenecks and how to avoid them are also explained.
Optimization techniques on low level that practically and visually show dominance of
optimized code over unoptimized are illustrated with examples in C# programming
language. Optimization techniques handle the most common fields where programmer can
bump into, such as optimization of strings, loops, data types and methods.
SOURCE CODE OPTIMIZATION STRATEGIES AND TECHNIQUES
KEYWORDS:
Optimization, optimization strategies, optimization technics, optimization process, C#
STRATEGIJE I TEHNIKE OPTIMIZACIJE PROGRAMSKOG KODA
65
9 PRILOZI
9.1 Popis slika
Slika 1 Ciklus odnosa optimizacije i snage računala ............................................................. 3
Slika 2 Odnos duljine programskog koda i brzine izvođenja .............................................. 12
Slika 3 Odnos brzine memorije i čvrstog diska ................................................................... 16
Slika 4 Ciklus optimizacije programskog koda ................................................................... 20
Slika 5 Zaustavljanje ispitivanja kada je odgovor poznat ................................................... 24
Slika 6 Odnos brzina različitih metoda pri spajanju više nizova ......................................... 30
Slika 7 Ispravna upotreba metode Append() kod rada s pojedinačnim znakovima ............. 31
Slika 8 Podrška od strane VisualStudia pri izradi ekstenzijskih(engl. extension) metoda .. 33
Slika 9 Razlika u performansama nativne ToString() metode i naše implementacije ......... 33
Slika 10 Pokazatelj skupoće korištenja ToString() metode ................................................. 35
Slika 11 Razlika u brzini for i foreach petlji ....................................................................... 36
Slika 12 Prikaz specifičnosti x86 arhitekture na performanse kretanja kroz petlju ............ 37
Slika 13 Spajanje petlji kao tehnika optimizacije ................................................................ 38
Slika 14 Odmotavanje petlji kao tehnika optimizacije ........................................................ 39
Slika 15 Unswitching kao tehnika optimizacije. ................................................................. 40
Slika 16 Izbacivanje posla iz petlje. .................................................................................... 41
Slika 17 Izbjegavanje pakiranja(engl. boxing). .................................................................. 43
Slika 18 Spljoštivanje polja kao tehnika optimizacije. ........................................................ 44
Slika 19 Prikaz skupoće korištenja izraza na primjeru negacije ........................................ 47
Slika 20 Povećanje performansi predkalkuliranjem vrijednosti i korištenjem konstanti .... 48
Slika 21 Performanse pri korištenju inline metoda ............................................................. 51
Slika 22 Dominacija statičnih metoda pri mjerenju brzine poziva ...................................... 52
Slika 23 Utjecaj smanjivanja broja parametara na brzinu poziva metode ........................... 52
Slika 24 Utjecaj redoslijeda parametra na brzinu izvođenja metode................................... 54
Slika 25 Utjecaj korištenja ThreadPool klase pri inicijalizaciji dretvi ................................ 56
Slika 26 Utjecaj korištenja iznimki pri otkrivanju grešaka na performanse programa ....... 59
Slika 27 Utjecaj sprječavanja grešaka na povećanje performansi programa ....................... 60
top related