Učinite Java brzom: optimizirajte!

Prema pionirskom informatičaru Donaldu Knuthu, "prerana optimizacija korijen je svega zla". Svaki članak o optimizaciji mora započeti ističući da obično postoji više razloga za ne optimizaciju nego za optimizaciju.

  • Ako vaš kod već radi, optimizacija je siguran način za uvođenje novih, a možda i suptilnih grešaka

  • Optimizacija teži da kôd bude teže razumjeti i održavati

  • Neke ovdje predstavljene tehnike povećavaju brzinu smanjenjem proširivosti koda

  • Optimizacija koda za jednu platformu može ga zapravo pogoršati na drugoj platformi

  • Mnogo se vremena može potrošiti na optimizaciju, s malim dobitkom u performansama, a može rezultirati zamagljenim kodom

  • Ako ste pretjerano opsjednuti optimizacijom koda, ljudi će vas nazvati štreberom iza leđa

Prije optimizacije, pažljivo razmislite trebate li uopće optimizirati. Optimizacija u Javi može biti nedostižna meta jer se okruženja izvršavanja razlikuju. Korištenje boljeg algoritma vjerojatno će donijeti veće povećanje performansi od bilo koje količine optimizacija na niskoj razini, a vjerojatnije je da će postići poboljšanje u svim uvjetima izvršenja. U pravilu, optimizacije na visokoj razini treba razmotriti prije nego što se naprave optimizacije na niskoj razini.

Pa zašto optimizirati?

Ako je to tako loša ideja, zašto uopće optimizirati? Pa, u idealnom svijetu ne biste. Ali stvarnost je da je ponekad najveći problem programa taj što zahtijeva jednostavno previše resursa, a ti resursi (memorija, CPU ciklusi, propusnost mreže ili kombinacija) mogu biti ograničeni. Fragmenti koda koji se javljaju više puta tijekom programa vjerojatno će biti osjetljivi na veličinu, dok kod s mnogo iteracija izvršenja može biti osjetljiv na brzinu.

Neka Java bude brza!

Kao interpretirani jezik s kompaktnim bytecodeom, brzina ili njegov nedostatak ono je što se najčešće pojavljuje kao problem na Javi. Primarno ćemo razmotriti kako Java učiniti bržim, umjesto da je uklopimo u manji prostor - iako ćemo ukazati na to gdje i kako ti pristupi utječu na memoriju ili mrežnu propusnost. Fokus će biti na osnovnom jeziku, a ne na Java API-ima.

Usput, jedna stvar o kojoj ovdje nećemo raspravljati je upotreba izvornih metoda napisanih na C-u ili u zborniku. Iako upotreba izvornih metoda može krajnje poboljšati performanse, to čini po cijenu neovisnosti Java platforme. Moguće je napisati i Java verziju metode i izvorne verzije za odabrane platforme; to dovodi do povećanih performansi na nekim platformama bez odricanja od mogućnosti pokretanja na svim platformama. Ali ovo je sve što ću reći na temu zamjene Jave s C kodom. (Pogledajte Java savjet, "Napišite izvorne metode" za više informacija o ovoj temi.) Naš je fokus u ovom članku na tome kako ubrzati Java.

90/10, 80/20, koliba, koliba, planinarenje!

U pravilu se 90 posto vremena izvršavanja programa potroši na izvršavanje 10 posto koda. . izvoditi s velikom frekvencijom.) Optimiziranje ostalih 90 posto programa (gdje je potrošeno 10 posto vremena izvršavanja) nema zamjetni učinak na izvedbu. Da uspijete postići da se 90 posto koda izvrši dvostruko brže, program bi bio samo 5 posto brži. Dakle, prvi zadatak optimizacije koda je identificirati 10 posto (često manje od ovog) programa koji troši većinu vremena izvršenja. Ovo nijeuvijek tamo gdje očekujete.

Opće tehnike optimizacije

Postoji nekoliko uobičajenih tehnika optimizacije koje se primjenjuju bez obzira na jezik koji se koristi. Neke od ovih tehnika, poput globalne dodjele registara, sofisticirane su strategije za dodjelu strojnih resursa (na primjer, CPU registri) i ne primjenjuju se na Java bajt kodove. Usredotočit ćemo se na tehnike koje u osnovi uključuju kod za restrukturiranje i zamjenu ekvivalentnih operacija unutar metode.

Smanjenje snage

Smanjenje snage događa se kada se operacija zamijeni ekvivalentnom operacijom koja se izvršava brže. Najčešći primjer smanjenja čvrstoće je upotreba operatora smjene za množenje i dijeljenje cijelih brojeva snagom od 2. Na primjer, x >> 2može se koristiti umjesto x / 4i x << 1zamjenjuje x * 2.

Uklanjanje uobičajenog podraza

Uklanjanje uobičajenog podraza uklanja suvišne izračune. Umjesto da pišem

double x = d * (lim / max) * sx; double y = d * (lim / max) * sy;

uobičajeni podizraz izračunava se jednom i koristi za oba izračuna:

double depth = d * (lim / max); double x = depth * sx; double y = depth * sy;

Kretanje koda

Kretanje koda premješta kôd koji izvodi operaciju ili izračunava izraz čiji se rezultat ne mijenja ili je invarijantan . Kôd se premješta tako da se izvršava samo kad se rezultat može promijeniti, umjesto da se izvršava svaki put kada je rezultat potreban. To je najčešće kod petlji, ali može uključivati ​​i kôd ponovljen pri svakom pozivanju metode. Slijedi primjer invarijantnog kretanja koda u petlji:

for (int i = 0; i < x.length; i++) x [i] * = Math.PI * Math.cos (y); 

postaje

dvostruka pikozija = Math.PI * Math.cos (y); for (int i = 0; i < x.length; i++)x [i] * = pikozija;

Odmotavanje petlji

Odmotavanjem petlji smanjuju se troškovi kontrolnog koda petlje izvodeći više puta jednu operaciju svaki put kroz petlju, a time i izvršavajući manje iteracija. Radeći iz prethodnog primjera, ako znamo da je duljina x[]uvijek višestruka od dva, petlju bismo mogli prepisati kao:

dvostruka pikozija = Math.PI * Math.cos (y); for (int i = 0; i < x.length; i += 2) {x [i] * = pikozija; x [i + 1] * = pikozija; }

U praksi odmotavanje petlji poput ove - u kojoj se vrijednost indeksa petlje koristi unutar petlje i mora se zasebno povećavati - ne daje osjetno povećanje brzine u interpretiranoj Javi, jer bajtkodovi nemaju upute za učinkovito kombiniranje " +1"u indeks niza.

Svi savjeti za optimizaciju u ovom članku utjelovljuju jednu ili više gore navedenih općih tehnika.

Pokretanje kompajlera na posao

Moderni C i Fortran kompajleri proizvode visoko optimizirani kod. C ++ kompajleri obično proizvode manje učinkovit kod, ali su još uvijek na dobrom putu do stvaranja optimalnog koda. Svi su ti kompajleri prošli kroz mnoge generacije pod utjecajem jake tržišne konkurencije i postali su fino izbrušeni alati za istiskivanje svake posljednje kapi izvedbe iz uobičajenog koda. Gotovo sigurno koriste sve gore predstavljene opće tehnike optimizacije. No, ostalo je još dosta trikova kako natjerati kompajlere da generiraju učinkovit kod.

javac, JIT-ovi i izvorni kompajleri koda

Razina optimizacije koja se javacu ovom trenutku izvodi prilikom sastavljanja koda je minimalna. Zadano radi sljedeće:

  • Stalno presavijanje - kompajler rješava sve konstantne izraze takve da se i = (10 *10)kompajlira u i = 100.

  • Preklapanje grana (većinu vremena) - gotoizbjegavaju se nepotrebni bajt kodovi.

  • Ograničena eliminacija mrtvog koda - ne izrađuje se kôd za izjave poput if(false) i = 1.

Razina optimizacije koju nudi javac trebala bi se poboljšati, vjerojatno dramatično, kako jezik sazrijeva, a dobavljači kompajlera počinju se ozbiljno nadmetati na temelju generiranja koda. Java upravo dobiva kompajlere druge generacije.

Zatim postoje JIT-ovi kompajleri koji pretvaraju Java bajt kodove u izvorni kôd u vrijeme izvođenja. Nekoliko ih je već dostupno, i iako mogu dramatično povećati brzinu izvršavanja vašeg programa, razina optimizacije koju mogu izvesti ograničena je jer se optimizacija događa u vrijeme izvođenja. Prevodnik JIT više se brine za generiranje koda nego za generiranje najbržeg koda.

Izvorni kompajleri koda koji Java kompajliraju izravno u izvorni kôd trebali bi pružiti najveće performanse, ali po cijenu neovisnosti platforme. Srećom, mnoge ovdje predstavljene trikove postići će budući kompajleri, ali za sada je potrebno malo truda kako bi kompajler izvukao maksimum.

javacnudi jednu opciju izvedbe koju možete omogućiti: pozivajući se na -Oopciju da uzrokuje da kompajler ugradi određene pozive metode:

javac -O MyClass

Umetanje poziva metode ubacuje kôd metode izravno u kôd koji upućuje poziv metode. Ovo eliminira opće troškove poziva metode. Za malu metodu ovaj režijski iznos može predstavljati značajan postotak vremena izvršenja. Imajte na umu da se za ugradnju mogu uzeti u obzir samo metode koje su deklarirane kao ili private, staticili se finalmogu ukloniti jer ih prevodilac statički rješava samo te metode. Također, synchronizedmetode neće biti uvrštene. Kompajler će umetnuti samo male metode koje se obično sastoje od samo jednog ili dva retka koda.

Nažalost, verzije 1.0 prevoditelja javac imaju bug koji će generirati kôd koji ne može proslijediti verifikator bajt koda kada -Ose koristi opcija. To je popravljeno u JDK 1.1. (Provjerivač bajtkoda provjerava kôd prije nego što mu je dopušteno pokretanje kako bi se osiguralo da ne krši nijedno Java pravilo.) Ugradit će metode koje upućuju na članove klase nedostupne pozivajućoj klasi. Na primjer, ako se sljedeće klase kompajliraju pomoću -Oopcije

klasa A {privatni statički int x = 10; javna statička void getX () {return x; }} klasa B {int y = A.getX (); }

poziv A.getX () u klasi B bit će postavljen u klasi B kao da je B napisan kao:

klasa B {int y = Axe; }

Međutim, to će generirati bytecode-ove za pristup privatnoj Ax varijabli koja će se generirati u B-ovom kodu. Ovaj će se kôd izvrsno izvršiti, ali budući da krši Javina ograničenja pristupa, verifikator će ga označiti IllegalAccessErrorprvim izvršavanjem koda.

Ova programska pogreška ne čini -Omogućnost beskorisnom, ali morate biti oprezni kako je upotrebljavate. Ako se pozove na jednoj klasi, može ugraditi određene pozive metoda unutar klase bez rizika. Nekoliko klasa može se staviti zajedno dok ne postoje potencijalna ograničenja pristupa. A neki kod (poput aplikacija) nije podvrgnut provjeri bajtkoda. Grešku možete zanemariti ako znate da će se vaš kôd izvršiti samo bez podvrgavanja provjeri. Za dodatne informacije pogledajte moj FAQ o javac-O.

Profilari

Srećom, JDK dolazi s ugrađenim profilom koji pomaže identificirati gdje se vrijeme provodi u programu. Pratit će vrijeme provedeno u svakoj rutini i upisivati ​​podatke u datoteku java.prof. Da biste pokrenuli program za profiliranje, upotrijebite -profopciju prilikom pozivanja Java interpretera:

java -prof myClass

Ili za upotrebu s apletom:

java -prof sun.applet.AppletViewer myApplet.html

Postoji nekoliko upozorenja za upotrebu profila. Izlaz za profiliranje nije osobito lako dešifrirati. Također, u JDK 1.0.2 skraćuje nazive metoda na 30 znakova, pa neke metode možda neće biti moguće razlikovati. Nažalost, s Macom nema načina za pozivanje profila, tako da korisnici Maca nemaju sreće. Povrh svega, Sunina stranica dokumenta Java (vidi Resursi) više ne uključuje dokumentaciju za -profopciju). Međutim, ako vaša platforma podržava ovu -profopciju, za tumačenje rezultata mogu se koristiti ili HyperProf Vladimira Bulatova ili ProfileViewer Grega Whitea (vidi Resursi).

Također je moguće "profilirati" kôd umetanjem eksplicitnog vremena u kôd:

long start = System.currentTimeMillis(); // do operation to be timed here long time = System.currentTimeMillis() - start;

System.currentTimeMillis()vraća vrijeme u 1/1000 sekundi sekunde. Međutim, neki sustavi, poput Windows računala, imaju sistemski mjerač vremena s manje (puno manje) razlučivosti od 1/1000 dio sekunde. Ni 1/1000 sekunde nije dovoljno dugo da precizno odredi vrijeme mnogih operacija. U tim slučajevima ili na sustavima s tajmerima s niskom razlučivošću, možda će biti potrebno vremenski odrediti koliko je vremena potrebno ponoviti operaciju n puta, a zatim podijeliti ukupno vrijeme s n da biste dobili stvarno vrijeme. Čak i kad je dostupno profiliranje, ova tehnika može biti korisna za određivanje vremena određenog zadatka ili operacije.

Evo nekoliko završnih napomena o profiliranju:

  • Uvijek kodirajte kôd prije i nakon izmjena kako biste provjerili jesu li vaše promjene barem na testnoj platformi poboljšale program

  • Pokušajte napraviti svaki test vremena pod jednakim uvjetima

  • Ako je moguće, osmislite test koji se ne oslanja na bilo koji korisnički unos, jer varijacije u korisnikovom odgovoru mogu dovesti do kolebanja rezultata

Alat za mjerilo

Alat Benchmark mjeri vrijeme potrebno za operaciju tisućama puta (ili čak milijunima) puta, oduzima vrijeme provedeno u operacijama koje nisu test (kao što je režijska petlja), a zatim koristi te podatke da izračuna koliko dugo svaka operacija uzeo. Pokreće svaki test otprilike jednu sekundu. U pokušaju da eliminira slučajna kašnjenja iz drugih operacija koje računalo može izvesti tijekom testa, svako ispitivanje izvodi tri puta i koristi najbolji rezultat. Također pokušava eliminirati sakupljanje smeća kao čimbenik u testovima. Zbog toga su, što je više memorije dostupno referentnoj vrijednosti, to su precizniji rezultati.