You are on page 1of 11

PREDAVANJE 7: CODE TUNING

Code tuning (podešavanje koda) je praksa modifikovanja korektnog koda na način da se poboljšaju
njegove performanse. „Tuning“ se odnosi na male promjene klase, metode, nekoliko linija koda.
Code tuning tehnike su u nekim slučajevima u kontraverzi sa refaktoring tehnikama jer se može desiti
da degradiraju internu strukturu koda da bi se postigle bolje performanse. Često je čitljivost koda
mnogo važnija od poboljšanja performansi. U mnogim slučajevima se poboljšaju performanse koda
primjenom code tuning tehnika ali ponekad ta poboljšanja mogu biti beznačajna. Također, poboljšanja
mogu varirati ovisno od kompajlera. Zbog toga je uvijek neophodno mjeriti performanse prije i poslije
primjene code tuning tehnika. Materijal je pripremljen na osnovu knjige – Code Complete, autora
Steve McConnela (poglavlje 26) i koristi se u edukativne svrhe.

Slijedi upoznavanje sa osnovnim code-tuning tehnikama koje se primjenjuje na: 1. Logičke iskaze 2.
Petlje 3. Podatke 4. Izraze 5. Rutine. Nakon toga se razmatra nekoliko specifičnih podešavanja koda u
.NET-u. Za sve code tuning tehnike postoje i chech liste.

1. Tuning logičkih iskaza

Princip ne testiraj ako znaš odgovor


Npr. za iskaz:

if (5 < x) and (x<10) then ….

Kada se procijeni da 5 nije veće od x nije potrebno da se ispituje drugi dio iskaza. Neki jezici
daju formu evaulacije poznatu kao „short-circuit“ evaulacija koja znači da kompajler
automatski zaustavi testiranje odmah čim je poznat rezultat. Dio je C++ i Java standarda.

Ako jezik ne podržava short-circuit evaulaciju tada se izbjegava korištenje logičkih operatora
(and, or,...) i dodaje se logički iskaz.

if (5<x) then
if (x<10) then

Princip ne testiraj ako znaš odgovor je dobar i za mnoge druge situacije.

//Primjer ne-zaustavljanja kada je poznat odgovor


negativeInputFound=false;
for (i=0; i<count; i++)
{
if (input [i] < 0)
{
negativeInputFound=true ;
}
}
Bolji pristup za primjer iznad je zaustaviti prolaz kroz petlju čim se nađe negativna
vrijednost. To se može rješiti:
-dodavanjem break iskaza nakon negativeInputFound=true;
-promijenom for petlje sa while petljom koja provjerava negativeInputFound nakon svake
iteracije.
Test koji je imao 100 vrijednosti i negativna vrijednost je nađena na pola je utvrdio uštedu
od 29% vremena za Java code-tuned koji je sadržavao break.

Uređivanje testova po frekvenciji


Aranžiranje testova po procijenjenoj frekvenciji izvršavanja u switch i if-then-else
strukturama može ubrzati izvršavanje koda. Rezultati nisu uočljivi za C# i Javu jer se uslovi
uz case postavljaju odvojeno a ne kao rang. Npr. u VisualBasicu se može postaviti case “A”
to “Z”. Za jezike koji ne podržavaju rang vrijednosti u case-u preporučuje se korištenje if-
then-else iskaza.

Korištenje lookups tabela za komplikovane izraze


U nekim jezicima kod koji koristiti lookup tabelu je brži nego komplikovani lanac logike.
Poenta komplikovanog lanca logike je kategorizacija i poslije poduzimanje akcije ovisno od
kategorije (npr. 3 grupe A,B,C).
//Kod sa komplikovanim lancem logike
if ((a&& !c) || (a&&b&&c)) {
category=1;
}
else if ((b && !a) || (a&&c&&!b)) {
category=2;
}
else if (c&& !a &&!b)) {
category=3;
}
else {
category=0;
}

Kod se može zamijeniti sa lookup tabelom. Lookup tabela je teška za čitanje, pa je treba i
dodatno dokumentovati. Procijenjeno poboljšanje za C++ je 33%.
// definisanje tabele kategorija
static int categoryTable[2][2][2]={
// !b!c !bc b!c bc
0, 3, 2, 2, // !a
1, 2, 1, 1 // a
};

category=categoryTable[a][b][c];

Korištenje lazy evaulacije


Lazy evaulacija je bazirana na principu da se izvršenje neke akcije, procjena izraza, poziv
metode i slično obavlja blizu trenutka kada se rezulat navedenog koristi. Dakle, izbjegava se
da se radi u programskom kodu bilo što drugo osim što je potrebno i onoliko koliko je
potrebno. Npr. ako postoji tabela od 5000 slogova a koristi se samo mali broj slogova onda
nema smisla učitati cijelu tabelu na početku programa.

2.Tuning petlji

Pošto se petlje izvršavaju više puta vruće tačke u programu su često unutar petlje. Tehnike
koje slijede čine petlju bržom.
Unswitching
Česte su petlje u kojima se ispitivanje odluke vrši unutar svake iteracije petlje, pri čemu ta
odluka ne utiče na tok izvršavanja.

//Kod sa if iskazom unutar petlje, pri čemu if iskaz ne utiče na indeks petlje
for (i=0; i<count;i++)
{
if (sumType==SUMTYPE_NET){
netSum=netSum+amount[i];
}
else
{
grossSum=grossSum+amount[i];
}
}

Unswitching (prebacivanje) propagira uklanjanje if iskaza koji nisu povezani sa indeksom


petlje izvan petlje. Slijedi izmjenjeni kod u kojem je if izvan petlje.

//UNSWITCHED kod – if iskaz izvan petlje


if (sumType==SUMTYPE_NET)
{
for (i=0; i<count;i++)
{
netSum=netSum+amount[i];
}
}
else
{
for (i=0; i<count;i++)
{
grossSum=grossSum+amount[i];
}
}

Ovaj fragment koda krši mnoga pravila dobrog programiranja. Čitljivost i održavanje koda su
često važniji od brzine izvršavanja ili veličine. Ovdje se petlje moraju održavati paraleno.

Jamming
Jamming (spajanje, fuzija) je kombinovanje 2 petlje koje imaju iste kontrolne uslove i koje
rade na istom setu elemenata.
//Kod sa dvije petlje koje rade na istom setu elemenata
for (i=0; i<n; i++)
sum[i] = 0.0;
for (i=0; i<n; i++)
rate[i] = 0.06;

Nakon spajanja petlji dobija se kod.

//Jamming – spajanje dvije petlje koje rade nad istim setom elemenata
for (i=0; i<n; i++)
{ sum [i] = 0.0;
rate[i] = 0.06;
}
Unrolling
Unrollong (odmotavanje) propagira da se što je moguće više posla uradi u okviru jedne
iteracije petlje.
//Primjer petlje koja može biti unrooled
i=0;
while (i <count)
{
a[i]=i;
i=i+1;

Tehnika odmotavanja mijenja originalnu liniju a[i]=i sa dvije linije, brojač se povećava za
2 (umjesto za 1). Ekstra linija koda nakon terminiranje while petlje je potrebna kada je count
neparan.
// Unrolled kod
i=0;
while (i <count-1)
{
a[i]=i;
a[i+1]=i+1;
i=i+2;

}
if (i== count-1)
{
a[count-1] = count-1 ;
}

Testovi pokazuju da su poboljšanja značajna i da ovisno od jezika iznose od 16-43% .

Minimiziranje rada unutar petlje


Jedan od načina poboljšanja performansi petlje se može postići minimiziranjem rada unutar
petlje.
//Primjer komplikovanog pointera unutar petlje
for (i = 0; i < rateCount; i++) {
netRate[i] = baseRate[i] * rates->discounts->factors->net;
}

U ovom slučaju može se pointer izraziti kao dobro-imenovana varijabla i na taj način
poboljšati čitljivost i performanse.

// Kod sa minimiziranim radom unutar petlje-pointer izvan petlje


quantityDiscount = rates->discounts->factors->net;
for (i = 0; i < rateCount; i++) {
netRate[i] = baseRate[i] * quantityDiscount;
}

Eksperimenti pokazuju da je u Javi za rateCount 100 poboljšanje čak 43%.


Sentinel vrijednost
Za petlje čija namjena je pretraga niza može se postići ubrzanje postavljanjem sentinel
(stražarske) vrijednosti tj. vrijednosti koja garantira terminiranje pretrage.

// Petlja pretrage sa ugrađenim if iskazom koji testira da li je tražena vrijednost


nađena
found=FALSE;
i=0;
while ((!found)&& (i<count))
{
if (item[i]==testValue){
found=TRUE;
}
else {
i++;
}
}
if found() {
….

U ovoj petlji svaka iteracija petlje testira da li je !found i da li je i<count. Namjena


!found testa je da se odredi kada je željeni element nađen. Namjena i<count testa je
indikacija kraja niza.

Ukoliko se prije početka petlja postavi na kraju niza sentinel vrijednost koja odgovara
traženoj vrijednosti pretrage, uslov petlje je samo jedan test item[i]!=testValue.

//Kod sa postavljenom sentinel vrijednosti


initialValue=item[count];
item[count]==testValue; // potrebno je obezbjediti prostor
// za sentinel vrijednost na kraju niza
i=0;
while (item[i]!=testValue){
i++;
}

if (i<count) {
….

Npr. za niz od 100 cjelobrojnih elemenata ubrzanje za Java kod je 44% a za C# 23%.

Smještanje više zaposlene petlje izvan


Kada postoje ugnježdene (nested) petlje treba razmisliti o načinu ugnježdavanja.

// Primjer ugnježdenih petlji koje se mogu poboljšati.


for (column=0; column<100; column++)
{
for (row=0;row<5;row++)
{
sum=sum+table[row][column];
}
}

Broj izvršavanja za iteraciju vanjske petlje je 100, za unutrašnju petlju 100*5=500, što je
ukupno 600 iteracija. Promjenom petlji vanjska petlje bi se izvršavala 5 puta a unutrašnja
5*100=500, što je ukupno 505 iteracija.
Strenght Reduction (Jaka redukcija)
Jaka redukcija (strenght redukcija) znači zamijeniti skupe operatore kao što je npr.
množenje sa jeftinijim kao što je sabiranje u izrazima koji su ovisni od indeksa petlje.

// Kod kod kojeg je brojač petlje u izrazu množenja


for (i=0;i<n;i++)
a[i]=i*conversion;

Nakon tuninga dobija se kod:


//Promijenjeni kod – množenje zamijenjeno sa sabiranjem
a[0]=0;
for (i=0; i<n;i++)
{
a[i]=a[i-1]+conversion;
}

3. Tuning transformacije podataka

Promjene u tipovima podataka mogu reducirati veličinu programa i poboljšati brzinu


izvršavanja.

-Koristiti integer tip podataka umjesto floating point (npr. indeks petlje), slično float
umjesto double,...

Koristiti što je moguće manje dimenzionalne nizove


Ako se moguće strukturirati podatke u niz sa manje dimenzija onda je u cilju poboljšanja
performansi to potrebno uraditi.
//Primjer standardne dvo-dimenzionalne inicijalizacije
for (row=0; row<numRows; row++) {
for (column=0; column<numColumns; column++) {
matrix[row][column] = 0;
}
}

Poboljšanje performansi u nekim programskim jezicima se može dobiti sa


jednodimenzionalnim nizom.
//Jednodimenzionalna reprezentacija niza
for (entry=0; entry<numRows*numColumns; entry++)
matrix[entry] = 0;

Minimiziranje referenciranja nizova


Korisno je minimizirati i pristup nizovima. Petlja koja u svakoj iteraciji koristi isti element
niza je dobar kandidat za primjenu ove tehnike.

//Kod sa nepotrebnim referenciranjem niza izvan petje


for (discountType=0; discountType<typeCount;discountType++)
{
for (discountLevel=0; discountLevel <levelCount; discountLevel++){
rate[discountLevel]=rate[discountLevel]*discount[discountType];
}
}
Referenca na discount[discountType] se ne mijenja sa discountLevel. Posljedično može
se pomjeriti iz unutrašnje petlje i na taj način pristup elementu niza je tokom svake iteracije
vanjske petlje a ne unutrašnje petlje.

//Kod sa pomjeranjem reference niza izvan petje


for (discountType=0; discountType<typeCount;discountType++)
{
thisDiscount=discount[discountType];

for (discountLevel=0; discountLevel <levelCount; discountLevel++){


rate[discountLevel]=rate[discountLevel]*thisDiscount;
}
}

Kao i obično rezultati varijaju od kompajlera do kompajlera.

Korištenje suplementarnih indeksa


Korištenje suplementarnog indeksa znači dodavanje povezanog podatka koji omogućava
efektivniji pristup određenom tipu podatka. Npr. za C je ponekad korisno dodati na kraju
niza podatak o dužini niza da se ne bi stalno ispitivalo da li je element jednak '/0'. Za velike
tipove podataka treba razmotriti i korištenje nezavisnih paralelnih indeksnih struktura
(sortiranje i pretraga su brži po indeks referenci).

Korištenje hešinga
Heširanje znači spašavanje više traženih vrijednosti na takav način da se mogu lakše
koristiti. Naprimjer, ako program slučajnim uzorkom čita sa diska, metoda može koristiti heš
da spasi slogove koji se najčešće čitaju. Poslije kada rutina primi zahtjev za slogom,
provjerava se da li je taj slog u hešu i ako jeste rutina vrati slog iz memorije umjesto sa
diska.
Mogu se heširati i rezultati od zahtijevnog računanja. Npr. potrebno je izračunati dužinu
hipotenuze pravouganog trougla, dajući dužine druge dvije strane.

//Rutina koja je pogodna za hešing


double Hypotenuse
{ double sideA,
double sideB,
}
{return Math.sqrt((sideA*sideA)+(sideB*sideB)) };

Ako je poznato da zahtjevi za nekim vrijednosti imaju tendenciju da se ponavljaju vrijednosti


se mogu heširati na ovaj način:
// Heširanje da izbjegne skupo računanje
private double cachedHypotenuse=0;
private double cachedSideA=0;
private double cachedSideB=0;

public double Hypotenuse


{ double sideA;
double sideB;
}
{
// provjera da li trougao već u kešu
if ((sideA==cachedSideA)&&(sideB=cachedSideB)){
return cachedHypotenuse ;
}
// računanje nove hipotenuze i keširanje
cachedHypotenuse= return Math.sqrt((sideA*sideA)+(sideB*sideB));
cachedSideA=sideA;
cachedSideB=sideB;
return cachedHypotenuse;
}

Druga verzija je mnogo komplikovanija nego prva ali zbog brzine (naročito u C++) je treba
razmotriti. Mnoge hešing šeme heširaju više od jednog elementa.
Kao i neke druge optimizacijske tehnike, heširanje dodaje kompleksnost i ima tendenciju
koda podložnog greškama.

4. Tuning izraza

Za izraze pogodne code tuning tehnike su:


Jaka redukcija (Strenght Reduction)
Koristiti identitete za zamjenu skupih operacije sa jeftinijim
Npr.
Not a and not b
može se zamijeniti sa:
Not(a or b)
i na takav način se može spasiti jedna not operacija.

Npr.
Umjesto sqrt(x) < sqrt(y) testirati x < y

-Koristiti efikasnu polinom evaluaciju


A*x*x*x + B*x*x + C*x + D = (((A*x)+B)*x)+C)*x+D

-Zamijeniti eksponent sa množenjem


-Zamijeniti cjelobrojno množenje sa 2 i dijeljenje sa 2 sa shift operacijama
-Koristiti cjelobrojnu vrijednost umjesto floting point
-Zamijeniti ako nije zahtijevana dupla preciznost sa jednostrukom preciznošću
-Zamijeniti trigonometrijske rutine sa trigonometrijskim identitetima
Inicijalizacija za vrijeme kompilacije
Ako se koristi imenovana konstanta ili magični broj kao jedini argument u pozivu neke
metode dobro je razmotriti izbjegavanje poziva metode odgovarajućim preračunavanjem.

//Log-base-two rutina
bazirana na sistemskoj rutini
unsigned int Log2(unsigned int x) {
return(unsigned int) (log(x)/log(2)) ;
}

Vrijednost od log(2) se nikada ne mijenja pa se može zamijeniti sa preračunatom vrijednosti


0.69314718 i izbjeći pozivanje metode.

//Korištenje konstante umjesto poziva metode


const double LOG2 =0.69314718;
unsigned int Log2(unsigned int x) {
return(unsigned int) (log(x)/LOG2) ;
}

Za dodatno značajno unapređenje treba razmotriti da li je potrebno da log2() vraća integer


vrijednost a koristi floating-point.

Upotreba shift operatora


Ubrzanja se mogu dobiti i korištenjem shift operatora.
//Alternativna Log-base-two rutina bazirana na right-shift operatoru
unsigned int Log2(unsigned int x)
{
unsigned int i=0;
while ((x=(x>>1))!=0){
i++ ;
}
return i;
}
Rutina posebno za ne C programere je veoma komplikovana za čitanje i treba je izbjegavati.

Koristiti korektne tipove


-izbjegavati nepotrebne konverzije tipova
-koristiti floating-point konstante za floats, integer konstante za ints,...

Preračunati rezultate
Prilikom dizajniranja na niskom nivou uvijek je odluka da li računati rezultate u toku rada ili
ih izračunati jednom i koristiti ih po potrebi. Ako se rezultati koriste više puta često je
jeftinije da se preračunaju jednom što se može postići na više načina. Već je pominjano da se
može dio računanja izvršiti izvan petlje umjesto unutar petlje, koristiti lookup tabela,
smjestiti rezultate u fajl ili neku programsku strukturu.
Npr. u space-war video igrici program inicijalno računa graviti koeficijente za različite
distance od sunca. Prepoznato je relativno malo različitih distanci od sunca koje se
preračunavaju i smještaju u 10-dimenzionalni niz, što je brže nego da se preračunavaju u toku
izvršavanja programa.
Dodatno optimizacija programa preračunavanjem može se postići:
-preračunavanjem rezultata i dodijeljivanje istih u konstante za vrijeme kompilacije,
-preračunavanjem rezultata i dodjeljivanje istih u varijable koje se koriste za vrijeme
izvršavanja programa,
-preračunavanjem rezultata i smještanje istih u fajlove koji se pune za vrijeme izvršavanja
programa,
-preračunavanjem rezultata jednom kada se startuje program i onda referenciranje istih svaki
put kada je to potrebno,
-izračunavanjem što je moguće više prije nego što petlja počne i minimiziranjem posla koji se
obavlja unutar petlje,
-izračunavanjem rezultata prvi put kada zatrebaju i heširanjem istih tako da se mogu zatražiti
kada zatrebaju.

Eliminisanje dijelova izraza


Ako postoje dijelovi izraza koji se ponavljaju više puta umjesto da se svaki put
preračunavaju treba ih dodijeliti varijablama i referencirati se na te varijable.
//Kod sa izrazom koji se ponavlja više puta
payment= loanAmount/(
(1.0-Math.pow((1.0+(interestRate/12.0)),-months)/(interesrRate/12.0));
U ovom primjeru može se dodijeliti interestRate/12.0 varijabli koja će se referencirati 2
puta umjesto da se izraz interestRate/12.0 računa dva puta.
Ako se izabere dobro ime varijable ova optimizacija uz poboljšanje performansi može
poboljšati čitljivost koda.
//Kod sa preračunatim izrazom koji se ponavlja
monthlyInterest=interestRate/12.0;
payment= loanAmount/((1.0-Math.pow(1.0+ monthlyInterset),-months))/
monthlyInterest));
//Može se desiti da je Math.pow rutina skupa.

5.Tuning rutina
Dekompozicija programa je važna za code tuning. Objektno orijentisani dizajn doprinosi
dobroj organizacija koda nad kojim se može primijeniti i refaktoring i potrebne code tuning
tehnike.

Napisati inline rutinu


Ukoliko je moguće rutinu napisati kao inline dolazi do uštede vremena. Neki jezici daju i ključnu riječ
za takve rutine (C++ inline).

Pisanje rutine u jeziku niže nivoa


Pisanje nekih rutina u jeziku niže nivoa poboljšava i veličinu koda i brzinu. Npr. ako se piše
u C++ može se koristi asembler za neku rutinu, za Phyton to može biti C.
6.Code tuning specifično za .Net okruženja
Slijedi nekoliko code tuning prikaza specifičnih za .Net okruženje.

Code tuning za string


• Umjesto:
string s1 = "";
s1 = "test";
s1+="ing";
s1 += " string object";
• Pisati:
string s1="";
s1="test" +"ing" +" string object";

Spajanje string literala se dešava za vrijeme kompilacije dok se za string varijable dešava za
vrijeme run time i koristiti se heap.
Koristiti StringBuilder –a za spajanje string varijabli:
StringBuilder sb1 = new StringBuilder();
string s1 = "test";
for (int i = 0; i < 100; i++)
sb1.Append(s1);
StringBuilder reducira overhead prilikom alokacije.

Koristiti Use Compare metodu za case-insensitive string komparacije:


Umjesto:
string string1 = "Hyderabad";
string string2 = "hyderabad";
if (string1.ToLower() == string2.ToLower())
{
}
Pisati:
string string1 = "Hyderabad";
string string2 = "hyderabad";
if (String.Compare(string1,string2,true) == 0)
{
}

Mnogi alati dostupni za dijagnostike i mjerenje performansi. U Visual Studiu postoji


Diagnostic Tools (slika 1) sa kojim se može vidjeti CPU korištenje, korištenje memorije,
vrijeme potrebno za izvršavanje sekcija koda....

Slika 1: Diagnostic Tool u Visual Studiu

You might also like