Programiranje

Hitra Java: optimizirajte!

Po besedah ​​pionirskega računalniškega znanstvenika Donalda Knutha je "prezgodnja optimizacija korenina vsega zla." Vsak članek o optimizaciji se mora začeti s poudarjanjem, da je razlogov običajno več ne optimizirati kot optimizirati.

  • Če vaša koda že deluje, je optimizacija zanesljiv način za uvedbo novih in morda subtilnih napak

  • Optimizacija ponavadi otežuje razumevanje in vzdrževanje kode

  • Nekatere tukaj predstavljene tehnike povečujejo hitrost z zmanjšanjem razširljivosti kode

  • Optimizacija kode za eno platformo lahko dejansko poslabša stanje na drugi platformi

  • Veliko časa lahko porabimo za optimizacijo, z malo povečanja zmogljivosti in lahko povzroči zamegljeno kodo

  • Če ste pretirano obsedeni z optimizacijo kode, vas bodo ljudje poklicali za piflarja za hrbtom

Pred optimizacijo natančno premislite, ali jo sploh morate optimizirati. Optimizacija v Javi je lahko nedosegljiv cilj, saj se izvedbena okolja razlikujejo. Uporaba boljšega algoritma bo verjetno prinesla večje povečanje zmogljivosti kot katera koli količina optimizacij na nizki ravni in je verjetneje, da bo prinesla izboljšave v vseh pogojih izvajanja. Praviloma je treba pred optimizacijo na nizki ravni razmisliti o optimizaciji na visoki ravni.

Zakaj torej optimizirati?

Če je to tako slaba ideja, zakaj sploh optimizirati? No, v idealnem svetu ne bi. Toda v resnici je včasih največja težava programa ta, da zahteva preprosto preveč virov in ti viri (pomnilnik, cikli procesorja, pasovna širina omrežja ali kombinacija) so lahko omejeni. Fragmenti kode, ki se v programu pojavijo večkrat, so verjetno občutljivi na velikost, medtem ko je koda z veliko ponovitvami izvedbe lahko občutljiva na hitrost.

Naj bo Java hitra!

Kot interpretiran jezik s kompaktno bajt kodo se v Javi najpogosteje pojavlja kot težava hitrost ali pomanjkanje le-te. Najprej bomo preučili, kako Java hitreje teči, namesto da bi se prilegala manjšemu prostoru - čeprav bomo opozorili, kje in kako ti pristopi vplivajo na pomnilnik ali pasovno širino omrežja. Poudarek bo na osrednjem jeziku in ne na Java API-jih.

Mimogrede, ena stvar smo ne bo tukaj bomo razpravljali o uporabi izvornih metod, napisanih v jeziku C ali montaži. Čeprav lahko uporaba izvornih metod izjemno poveča zmogljivost, to počne za ceno neodvisnosti platforme Java. Za izbrane platforme je mogoče napisati tako različico Java metode kot izvorne različice; to vodi do večje zmogljivosti na nekaterih platformah, ne da bi se pri tem odrekli zmožnosti teka na vseh platformah. Ampak to je vse, kar bom rekel na temo zamenjave Jave s kodo C. (Za več informacij o tej temi glejte Nasvet za Java, "Pisanje izvornih metod".) V tem članku se osredotočamo na to, kako Java hitro narediti hitro.

90/10, 80/20, koča, koča, pohod!

Praviloma se 90 odstotkov časa izvajanja programa porabi za izvajanje 10 odstotkov kode. (Nekateri uporabljajo pravilo 80 odstotkov / 20 odstotkov, vendar moje izkušnje s pisanjem in optimizacijo komercialnih iger v več jezikih v zadnjih 15 letih kažejo, da je formula 90 odstotkov / 10 odstotkov značilna za programe, ki so lačni po uspešnosti, izvesti z veliko pogostostjo.) Optimizacija ostalih 90 odstotkov programa (kjer je bilo porabljenih 10 odstotkov časa izvajanja) nima opaznega vpliva na uspešnost. Če bi lahko dosegli, da se 90 odstotkov kode izvede dvakrat hitreje, bi bil program samo 5 odstotkov hitrejši. Prva naloga pri optimizaciji kode je torej prepoznati 10 odstotkov (pogosto manj kot to) programa, ki porabi večino časa izvajanja. To ni vedno tam, kjer pričakujete.

Splošne optimizacijske tehnike

Obstaja več pogostih tehnik optimizacije, ki se uporabljajo ne glede na jezik, ki se uporablja. Nekatere od teh tehnik, kot je globalna dodelitev registrov, so izpopolnjene strategije za dodeljevanje strojnih virov (na primer registri CPU) in se ne nanašajo na bajtne kode Java. Osredotočili se bomo na tehnike, ki v osnovi vključujejo kodo za prestrukturiranje in nadomeščanje enakovrednih operacij znotraj metode.

Zmanjšanje moči

Zmanjšanje moči se zgodi, ko operacijo zamenja enakovredna operacija, ki se izvede hitreje. Najpogostejši primer zmanjšanja moči je uporaba operaterja premika za množenje in deljenje celih števil z močjo 2. Na primer, x >> 2 se lahko uporablja namesto x / 4, in x << 1 nadomešča x * 2.

Odprava skupnega podraza

Izločitev skupnega podraza odstrani odvečne izračune. Namesto pisanja

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

skupni podizraz se izračuna enkrat in se uporabi za oba izračuna:

dvojna globina = d * (lim / max); dvojni x = globina * sx; dvojni y = globina * sy;

Gibanje kode

Gibanje kode premakne kodo, ki izvede operacijo ali izračuna izraz, katerega rezultat se ne spremeni ali pa je nespremenljivo. Koda se premakne tako, da se izvede le, kadar se rezultat lahko spremeni, namesto da se izvede vsakič, ko je rezultat potreben. To je najpogostejše pri zankah, lahko pa vključuje tudi kodo, ki se ponovi ob vsakem klicu metode. Sledi primer nespremenljivega gibanja kode v zanki:

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

postane

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

Odvijanje zank

Odvijanje zank zmanjša neobremenjene nadzorne kode zanke, tako da vsakič izvede več kot eno operacijo skozi zanko in posledično izvede manj ponovitev. Obdelava iz prejšnjega primera, če vemo, da je dolžina x [] je vedno večkratnik dveh, zanko lahko prepišemo kot:

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

V praksi odvijanje zank, kot je ta, pri kateri se vrednost indeksa zanke uporablja znotraj zanke in jo je treba ločeno povečati, ne povzroči občutnega povečanja hitrosti v interpretirani Javi, ker bajtkod nima navodil za učinkovito kombiniranje "+1"v indeks polja.

Vsi nasveti za optimizacijo v tem članku vključujejo eno ali več zgoraj naštetih splošnih tehnik.

Uvajanje prevajalnika v delo

Sodobni prevajalniki C in Fortran proizvajajo zelo optimizirano kodo. Prevajalniki C ++ na splošno proizvajajo manj učinkovito kodo, vendar so še vedno na dobri poti do izdelave optimalne kode. Vsi ti prevajalniki so skozi vpliv močne tržne konkurence prešli skozi številne generacije in postali natančno izpiljena orodja za iztiskanje vsakega zadnjega padca zmogljivosti iz običajne kode. Skoraj zagotovo uporabljajo vse zgoraj predstavljene splošne tehnike optimizacije. Vendar je še vedno veliko trikov, s katerimi prevajalniki ustvarjajo učinkovito kodo.

javac, JIT in prevajalniki izvornih kod

Raven optimizacije, ki javac izvaja pri prevajanju kode na tej točki je minimalen. Privzeto počne naslednje:

  • Nenehno zlaganje - prevajalnik razreši vse konstantne izraze tako i = (10 * 10) prevaja v i = 100.

  • Zlaganje vej (večino časa) - nepotrebno Pojdi do bytecode se izognemo.

  • Omejeno odstranjevanje mrtve kode - za izjave, kot je, ni izdelana koda če je (napačno) i = 1.

Raven optimizacije, ki jo nudi javac, bi se morala verjetno dramatično izboljšati, saj jezik dozori in prodajalci prevajalnikov začnejo resno tekmovati na podlagi generiranja kode. Java pravkar dobiva drugo generacijo prevajalnikov.

Potem so tu še pravočasni prevajalniki (JIT), ki pretvorijo bajtode Java v izvorno kodo med izvajanjem. Na voljo jih je že več, in čeprav lahko dramatično povečajo hitrost izvajanja vašega programa, je stopnja optimizacije, ki jo lahko izvedejo, omejena, ker optimizacija poteka v času izvajanja. Prevajalnik JIT se bolj ukvarja s hitro generiranjem kode kot z najhitrejšo kodo.

Nativni prevajalniki kode, ki Javo prevajajo neposredno v izvorno kodo, bi morali ponujati največjo zmogljivost, vendar za ceno neodvisnosti platforme. Na srečo bodo številne trike, predstavljene tukaj, dosegli prihodnji prevajalniki, toda za zdaj je treba nekaj dela, da bi prevajalnik kar najbolje izkoristil.

javac ponuja eno možnost delovanja, ki jo lahko omogočite: sklic na -O možnost, da povzroči, da prevajalnik vstavi določene klice metode:

javac -O MyClass

Vključitev klica metode vstavi kodo metode neposredno v kodo, ki kliče metodo. To odpravi režijske stroške klica metode. Za majhno metodo lahko ta režijski stroški predstavljajo pomemben odstotek časa izvedbe. Upoštevajte, da so samo metode, ki so deklarirane kot obe zasebno, statično, ali dokončno se lahko šteje za vstavljanje, ker prevajalnik statično razreši samo te metode. Prav tako sinhronizirano metode ne bodo vstavljene. Prevajalnik bo vstavil le majhne metode, ki so običajno sestavljene iz samo ene ali dveh vrstic kode.

Na žalost imajo različice 1.0 prevajalnika javac napako, ki bo ustvarila kodo, ki ne more prenesti preveritelja bajtkode, ko -O možnost. To je bilo popravljeno v JDK 1.1. (Preverjevalnik bajtkode preveri kodo, preden jo lahko zažene, da se prepriča, da ne krši nobenih pravil Java.) Vdela bo metode, ki se sklicujejo na člane razreda, ki niso dostopni klicnemu razredu. Če so na primer naslednji razredi zbrani z uporabo -O možnost

razred A {zasebni statični int x = 10; javna statična praznina getX () {return x; }} razred B {int y = A.getX (); } 

klic A.getX () v razredu B bo v razredu B zapisan, kot da bi bil B zapisan kot:

razred B {int y = A.x; } 

Vendar bo to povzročilo generiranje bajt kod za dostop do zasebne spremenljivke A.x, ki bo ustvarjena v kodi B. Ta koda se bo izvršila v redu, ker pa krši Java-jeve omejitve dostopa, jo bo preveritelj označil z IllegalAccessError prvič, ko se koda izvede.

Ta napaka ne povzroča -O možnost neuporabna, vendar morate biti previdni pri uporabi. Če je poklican v enem razredu, lahko določene klice metod vstavi v razred brez tveganja. Če ni možnih omejitev dostopa, je mogoče razvrstiti več razredov. Nekatera koda (na primer aplikacije) ni podvržena preveritelju bajtkode. Napake lahko prezrete, če veste, da se bo vaša koda izvedla samo, ne da bi bila podvržena preveritelju. Za dodatne informacije glejte moja pogosta vprašanja o javac-O.

Profilers

Na srečo je JDK opremljen z vgrajenim profilerjem, ki pomaga določiti, kje v programu preživi čas. Spremljal bo čas, porabljen za vsako rutino, in zapisoval podatke v datoteko java.prof. Za zagon profilatorja uporabite -prof možnost pri klicanju tolmača Java:

java -prof myClass

Ali za uporabo z programčkom:

java -prof sun.applet.AppletViewer myApplet.html

Obstaja nekaj opozoril za uporabo profilatorja. Izhoda profilerja ni posebej enostavno razvozlati. Prav tako v JDK 1.0.2 imena metode skrajša na 30 znakov, zato nekaterih metod morda ne bo mogoče razlikovati. Na žalost pri Macu ni mogoče priklicati profilerja, zato uporabniki Maca nimajo sreče. Poleg vsega tega dokument s strani Java na Javi (glejte Viri) ne vsebuje več dokumentacije za -prof možnost). Če pa vaša platforma podpira -prof Za interpretacijo rezultatov lahko uporabite HyperProf Vladimirja Bulatova ali ProfileViewer Grega Whitea (glejte Viri).

Kodo lahko tudi "profilirate" tako, da v kodo vstavite eksplicitni čas:

dolg začetek = System.currentTimeMillis (); // naredimo operacijo, ki bo tukaj časovno omejena = System.currentTimeMillis () - start;

System.currentTimeMillis () vrne čas v 1/1000 sekunde. Vendar imajo nekateri sistemi, na primer računalnik z operacijskim sistemom Windows, sistemski časovnik z manj (veliko manj) ločljivostjo kot 1/1000 sekunde. Tudi 1/1000 sekunde ni dovolj dolgo, da natančno določite čas številnih operacij. V teh primerih ali v sistemih z merilniki časa z nizko ločljivostjo bo morda treba določiti čas, koliko časa je treba ponoviti postopek n krat, nato pa skupni čas razdelite na n da dobite dejanski čas. Tudi če je na voljo profiliranje, je ta tehnika lahko koristna za določanje časa določene naloge ali operacije.

Tu je nekaj zaključnih opomb o profiliranju:

  • Vedno časovite kodo pred spremembami in po njih, da preverite, ali so vsaj na preizkusni platformi vaše spremembe izboljšale program

  • Poskusite narediti vsak časovni test pod enakimi pogoji

  • Če je mogoče, pripravite test, ki se ne zanaša na noben uporabniški vnos, saj lahko spremembe uporabnikovega odziva povzročijo nihanje rezultatov

Primerjalni programček

Aplikacijski preizkus meri tisoč (ali celo milijone) krat čas, potreben za izvedbo neke operacije, odšteva čas, porabljen za izvajanje drugih operacij, razen testa (na primer režijske stroške zanke), in nato s pomočjo teh informacij izračuna, kako dolgo posamezna operacija vzel. Vsak test izvaja približno eno sekundo. Da bi računalnik med preskusom odpravil naključne zamude pri drugih operacijah, trikrat izvede vsak test in uporabi najboljši rezultat. Prav tako poskuša odpraviti zbiranje smeti kot dejavnik pri testih. Zaradi tega je več pomnilnika, kot je na voljo merilo, bolj natančni so rezultati.

$config[zx-auto] not found$config[zx-overlay] not found