Programavimas

Padarykite „Java“ greitai: optimizuokite!

Pasak novatoriško kompiuterių mokslininko Donaldo Knutho, „Ankstyvas optimizavimas yra viso blogio šaknis“. Bet kurį straipsnį apie optimizavimą reikia pradėti nuo to, kad nurodoma, jog priežasčių yra daugiau ne optimizuoti nei optimizuoti.

  • Jei jūsų kodas jau veikia, jo optimizavimas yra tikras būdas įvesti naujas ir galbūt subtilias klaidas

  • Optimizavimas daro kodą sunkiau suprantamą ir prižiūrimą

  • Kai kurie čia pateikti metodai padidina greitį, sumažindami kodo išplėtimą

  • Optimizavus kodą vienai platformai, jis gali iš tikrųjų pablogėti kitoje platformoje

  • Daug laiko galima praleisti optimizuojant, o našumas nedaug padidėja, todėl gali būti sugadintas kodas

  • Jei esate pernelyg apsėstas kodo optimizavimo, žmonės vadins jus už jūsų nugaros

Prieš optimizuodami, turėtumėte gerai apsvarstyti, ar apskritai reikia optimizuoti. „Java“ optimizavimas gali būti sunkiai pasiekiamas tikslas, nes vykdymo aplinkos skiriasi. Geresnio algoritmo naudojimas greičiausiai padidins našumą nei bet koks žemo lygio optimizavimo būdas ir greičiausiai pagerins visas vykdymo sąlygas. Paprastai prieš atliekant žemo lygio optimizavimą reikėtų atsižvelgti į aukšto lygio optimizavimą.

Tad kam optimizuoti?

Jei tai tokia bloga idėja, kam apskritai optimizuoti? Na, idealiame pasaulyje to nedarytum. Tačiau realybė yra ta, kad kartais didžiausia programos problema yra ta, kad jai reikia tiesiog per daug išteklių, ir šie ištekliai (atmintis, procesoriaus ciklai, tinklo pralaidumas ar jų derinys) gali būti riboti. Kodo fragmentai, kurie visoje programoje pasitaiko kelis kartus, greičiausiai bus jautrūs dydžiui, o kodas, turintis daug vykdymo iteracijų, gali būti jautrus greičiui.

Greitai sukurkite „Java“!

Kaip interpretuojama kalba su kompaktišku baitų kodu, „Java“ dažniausiai iškyla kaip greitis ar jo trūkumas. Pirmiausia panagrinėsime, kaip priversti „Java“ veikti greičiau, o ne pritaikyti ją mažesnėje erdvėje - nors nurodysime, kur ir kaip šie metodai veikia atmintį ar tinklo pralaidumą. Pagrindinis dėmesys bus skiriamas pagrindinei kalbai, o ne „Java“ API.

Beje, vienas dalykas mes nebus aptarti čia yra vietinių metodų, parašytų C arba surinkimas, naudojimas. Naudojant vietinius metodus, galutinis našumas gali padidėti, tačiau tai daroma „Java“ nepriklausomybės nuo platformos kaina. Pasirinktoms platformoms galima parašyti ir „Java“ metodo versiją, ir savąsias versijas; tai lemia didesnį kai kurių platformų našumą neatsisakant galimybės veikti visose platformose. Bet tai viskas, ką pasakysiu apie „Java“ pakeitimą C kodu. (Norėdami gauti daugiau informacijos šia tema, skaitykite „Java“ patarimą „Rašyti vietinius metodus“.) Šiame straipsnyje daugiausia dėmesio skiriama tam, kaip greitai sukurti „Java“.

90/10, 80/20, trobelė, trobelė, žygis!

Paprastai 90 procentų programos sugedimo laiko praleidžiama vykdant 10 procentų kodo. (Kai kurie žmonės naudoja 80 proc. / 20 proc. Taisyklę, tačiau mano patirtis rašant ir optimizuojant komercinius žaidimus keliomis kalbomis per pastaruosius 15 metų parodė, kad 90 proc. / 10 proc. Formulė būdinga našumo ištroškusioms programoms, nes nedaug užduočių linkusios atlikti atlikti labai dažnai.) Kitų 90 procentų programos (kur praleista 10 procentų vykdymo laiko) optimizavimas neturi pastebimo poveikio našumui. Jei sugebėtumėte, kad 90 procentų kodo būtų vykdoma dvigubai greičiau, programa būtų tik 5 procentais greitesnė. Taigi pirmoji kodo optimizavimo užduotis yra nustatyti 10 procentų (dažnai tai yra mažiau nei tai) programos, kuri sunaudoja didžiąją dalį vykdymo laiko. Tai ne visada ten, kur tikiesi.

Bendros optimizavimo technikos

Yra keletas įprastų optimizavimo metodų, kurie taikomi neatsižvelgiant į vartojamą kalbą. Kai kurie iš šių būdų, pvz., Visuotinis registrų paskirstymas, yra sudėtingos strategijos, skirtos paskirstyti mašinų išteklius (pavyzdžiui, procesoriaus registrus) ir netaikomos „Java“ baitkodams. Mes sutelksime dėmesį į metodus, iš esmės susijusius su kodo pertvarkymu ir lygiaverčių operacijų pakeitimu metodu.

Stiprumo mažinimas

Stiprumas sumažėja, kai operacija pakeičiama lygiaverte operacija, kuri atliekama greičiau. Dažniausias stiprumo sumažinimo pavyzdys yra „shift“ operatoriaus naudojimas sveikiems skaičiams padauginti ir padalyti iš 2 galios. Pavyzdžiui, x >> 2 gali būti naudojamas vietoje x / 4ir x << 1 pakeičia x * 2.

Bendras posakio pašalinimas

Bendras posakio pašalinimas pašalina nereikalingus skaičiavimus. Užuot rašiusi

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

bendras posakis apskaičiuojamas vieną kartą ir naudojamas abiem skaičiavimams:

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

Kodo judesys

Kodo judėjimas perkelia kodą, kuris atlieka operaciją arba apskaičiuoja išraišką, kurios rezultatas nesikeičia arba yra nekintantis. Kodas perkeltas taip, kad jis būtų vykdomas tik tada, kai rezultatas gali pasikeisti, o ne vykdyti kiekvieną kartą, kai reikalingas rezultatas. Tai dažniausiai būdinga kilpoms, tačiau tai taip pat gali apimti kodą, kartojamą kiekvienam metodo iškvietimui. Toliau pateikiamas nekintamo kodo judėjimo cikle pavyzdys:

už (int i = 0; i <x. ilgis; i ++) x [i] * = Math.PI * Math.cos (y); 

tampa

dvigubas picosy = Math.PI * Math.cos (y);už (int i = 0; i <x. ilgis; i ++) x [i] * = pikingas; 

Išvyniojamos kilpos

Išvyniojus kilpas, sumažėja kilpos valdymo kodo pridėtinės išlaidos, atliekant daugiau nei vieną operaciją kiekvieną kartą per kilpą ir todėl atliekant mažiau iteracijų. Dirbame iš ankstesnio pavyzdžio, jei žinome, kad ilgis x [] visada yra dviejų kartotinis, mes galime perrašyti kilpą taip:

dvigubas picosy = Math.PI * Math.cos (y);už (int i = 0; i <x. ilgis; i + = 2) { x [i] * = pikingas; x [i + 1] * = pikingas; } 

Praktiškai išvyniojant tokias kilpas, kai ciklo indekso vertė naudojama kilpoje ir turi būti atskirai didinama, aiškinamasis „Java“ greitis pastebimai nepadidėja, nes bytecodes trūksta instrukcijų, kaip efektyviai sujungti „“.+1"į masyvo indeksą.

Visi šio straipsnio optimizavimo patarimai atspindi vieną ar daugiau iš pirmiau išvardytų bendrų metodų.

Kompiliatoriaus įjungimas į darbą

Šiuolaikiniai C ir „Fortran“ kompiliatoriai sukuria labai optimizuotą kodą. „C ++“ kompiliatoriai paprastai kuria mažiau efektyvų kodą, tačiau vis tiek sėkmingai pasiekia optimalų kodą. Visi šie kompiliatoriai išgyveno daugelį kartų, veikiami stiprios rinkos konkurencijos, ir tapo gerai ištobulintais įrankiais, kurie pašalina kiekvieną paskutinį našumą iš įprasto kodo. Jie beveik neabejotinai naudojasi visais aukščiau pateiktais bendrais optimizavimo būdais. Tačiau vis dar yra daugybė gudrybių, leidžiančių kompiliatoriams sukurti efektyvų kodą.

„javac“, JIT ir vietinių kodų sudarytojai

Optimizavimo lygis javac atlieka, kai kompiliuoti kodą šiuo metu yra minimalu. Numatyta atlikti šiuos veiksmus:

  • Nuolatinis lankstymas - kompiliatorius išsprendžia visas pastovias išraiškas taip, kad i = (10 * 10) surenka į i = 100.

  • Šakų lankstymas (dažniausiai) - nereikalingas eiti į vengiama baitų kodų.

  • Ribotas negyvo kodo pašalinimas - nėra sukurtas kodas tokiems teiginiams kaip jei (klaidinga) i = 1.

„Javac“ teikiamo optimizavimo lygis turėtų pagerėti, tikriausiai, dramatiškai, nes bręsta kalba ir kompiliatorių tiekėjai pradeda rimtai konkuruoti remdamiesi kodų generavimu. „Java“ ką tik gauna antrosios kartos kompiliatorius.

Tada yra „just-in-time“ (JIT) kompiliatoriai, kurie vykdymo metu paverčia „Java“ baitekodus į gimtąjį kodą. Keletas jau yra prieinami, ir nors jie gali žymiai padidinti jūsų programos vykdymo greitį, optimizavimo lygis, kurį jie gali atlikti, yra ribojamas, nes optimizavimas vyksta vykdymo metu. JIT kompiliatorius labiau rūpinasi greitu kodo generavimu nei greičiausio kodo generavimu.

Gimtųjų kodų kompiliatoriai, kompiliuojantys „Java“ tiesiai į gimtąjį kodą, turėtų pasiūlyti didžiausią našumą, tačiau platformos nepriklausomumo kaina. Laimei, daugelį čia pateiktų gudrybių pasieks būsimi kompiliatoriai, tačiau kol kas reikia šiek tiek padirbėti, kad kuo geriau išnaudotumėte kompiliatorių.

javac siūlo vieną našumo parinktį, kurią galite įgalinti: -O parinktis priversti kompiliatorių įterpti tam tikrus metodo iškvietimus:

„javac -O MyClass“

Įterpdami metodo iškvietimą, metodo kodas įterpiamas tiesiai į kodą, kuris metodo iškvietimą. Tai pašalina metodo skambučio pridėtines išlaidas. Taikant nedidelį metodą, ši pridėtinė suma gali sudaryti reikšmingą jos vykdymo laiko procentą. Atkreipkite dėmesį, kad tik metodais deklaruojama privatus, statinisarba galutinis gali būti svarstomas kaip įterpimas, nes kompiliatorius statiškai išsprendžia tik šiuos metodus. Be to, sinchronizuotas metodai nebus išdėstyti. Kompiliatorius įtrauks tik mažus metodus, paprastai sudaromus tik iš vienos ar dviejų kodo eilučių.

Deja, „javac“ kompiliatoriaus 1.0 versijoje yra klaida, kuri sugeneruos kodą, kuris negalės perduoti baitkodo tikrintuvo, kai -O naudojamas variantas. Tai buvo nustatyta JDK 1.1. (Baitų kodų tikrintuvas patikrina kodą prieš paleidžiant, kad įsitikintų, jog jis nepažeidžia jokių „Java“ taisyklių.) Jame bus aprašyti metodai, kurie nurodo klasės narius, prieinamus skambinančiai klasei. Pavyzdžiui, jei šios klasės sudaromos kartu naudojant -O variantą

A klasė {privati ​​statinė int x = 10; public static void getX () {return x; }} B klasė {int y = A.getX (); } 

B klasės skambutis A.getX () bus įtrauktas į B klasę taip, lyg B būtų parašytas taip:

B klasė {int y = A.x; } 

Tačiau tai sukurs baitų kodų prieigą prie privataus kintamojo A.x, kuris bus generuojamas B kode. Už šį kodą bus gerai, bet kadangi jis pažeidžia „Java“ prieigos apribojimus, tikrintojas jį pažymės su „IllegalAccessError“ pirmą kartą vykdant kodą.

Ši klaida nepadaro -O variantas nenaudingas, tačiau jūs turite būti atsargūs, kaip jį naudoti. Jei tai taikoma vienai klasei, tai be rizikos gali įtraukti tam tikrus metodų iškvietimus klasėje. Kelios klasės gali būti išdėstytos kartu, jei nėra jokių galimų prieigos apribojimų. Kai kuriems kodams (pvz., Programoms) netaikomas baitų kodo tikrintuvas. Galite nepaisyti klaidos, jei žinote, kad jūsų kodas bus vykdomas tik nepatikrinus tikrintojo. Norėdami gauti papildomos informacijos, žiūrėkite mano „javac-O“ DUK.

Profilininkai

Laimei, JDK ateina su įmontuotu profiliu, kuris padės nustatyti, kur laikas praleidžiamas programoje. Jis stebės laiką, praleistą kiekvienoje rutinoje, ir įrašys informaciją į bylą java.prof. Norėdami paleisti profilį, naudokite -prof parinktis iškviečiant „Java“ vertėją

java -prof myClass

Arba naudoti su programėle:

java -prof sun.applet.AppletViewer myApplet.html

Yra keletas įspėjimų dėl profilio naudojimo. Profiliuotojo išvestį nėra ypač lengva iššifruoti. Be to, JDK 1.0.2 ji sutrumpina metodų pavadinimus iki 30 simbolių, todėl gali būti neįmanoma atskirti kai kurių metodų. Deja, naudojant „Mac“ nėra jokių būdų iškviesti profilį, todėl „Mac“ vartotojams nesiseka. Be viso to, „Sun“ „Java“ dokumentų puslapyje (žr. „Ištekliai“) nebėra „ -prof variantas). Tačiau, jei jūsų platforma palaiko -prof Jei norite interpretuoti rezultatus, galima naudoti Vladimiro Bulatovo „HyperProf“ arba Grego White'o „ProfileViewer“ (žr. Ištekliai).

Taip pat galima koduoti profilį, į kodą įterpiant aiškų laiką:

ilga pradžia = System.currentTimeMillis (); // atlikite operaciją, kad čia būtų laikas ilgai = System.currentTimeMillis () - start;

System.currentTimeMillis () grąžina laiką per 1/1000-osios sekundės dalies. Tačiau kai kuriose sistemose, pavyzdžiui, „Windows“ kompiuteryje, yra sistemos laikmatis, kurio skiriamoji geba yra mažesnė (daug mažesnė) nei 1/1000 sekundės. Net 1/1000 sekundės nėra pakankamai ilgas laikas, kad būtų galima tiksliai suplanuoti daugelį operacijų. Tokiais atvejais arba sistemose su mažos skiriamosios gebos laikmačiais gali tekti nustatyti laiką, per kurį reikia pakartoti operaciją n kartų ir tada visą laiką padalykite iš n gauti faktinį laiką. Net kai profiliavimas yra prieinamas, ši technika gali būti naudinga nustatant konkrečią užduotį ar operaciją.

Štai keletas baigiamųjų pastabų dėl profiliavimo:

  • Visada nustatykite kodą prieš atlikdami pakeitimus ir po jų, kad patikrintumėte, ar bent bandymų platformoje jūsų pakeitimai pagerino programą

  • Kiekvieną laiko testą stenkitės atlikti vienodomis sąlygomis

  • Jei įmanoma, sugalvokite testą, kuris nesiremia jokia vartotojo įvestimi, nes dėl vartotojo atsakymo skirtumų rezultatai gali svyruoti

„Benchmark“ programėlė

„Benchmark“ programėlė matuoja laiką, kurį reikia atlikti operacijai tūkstančius (ar net milijonus) kartų, atima laiką, praleistą atliekant kitas operacijas, išskyrus bandymą (pvz., Kilpos pridėtines išlaidas), ir tada naudoja šią informaciją, kad apskaičiuotų, kiek laiko atliekama kiekviena operacija. paėmė. Kiekvienas bandymas atliekamas maždaug vieną sekundę. Bandydamas pašalinti atsitiktinius kitų operacijų delsimus, kuriuos kompiuteris gali atlikti bandymo metu, jis kiekvieną bandymą atlieka tris kartus ir naudoja geriausią rezultatą. Taip pat bandoma pašalinti atliekų surinkimą kaip veiksnį atliekant bandymus. Dėl to, kuo daugiau atminties turi etalonas, tuo tikslesni yra etalono rezultatai.