Programoptimalizálás

A program optimalizálása több szinten is történhet. Általában a magasabb szintek nagyobb hatással bírnak, és nehezebben változtathatók meg a projekt későbbi szakaszában, jelentős változtatásokat vagy teljes újraírást igényelnek, ha változtatni kell rajtuk. Így az optimalizálás jellemzően a magasabb szintről az alacsonyabbra való finomítással történhet, ahol a kezdeti nyereség nagyobb és kevesebb munkával elérhető, a későbbi nyereség pedig kisebb és több munkát igényel. Bizonyos esetekben azonban az általános teljesítmény a program nagyon alacsony szintű részeinek teljesítményétől függ, és a késői szakaszban végrehajtott apró változtatások vagy az alacsony szintű részletek korai figyelembevétele óriási hatással lehet. Jellemzően a hatékonyságot a projekt során végig figyelembe veszik – bár ez jelentősen változik -, de a nagyobb optimalizálást gyakran olyan finomításnak tekintik, amelyet későn kell elvégezni, ha egyáltalán elvégeznek. A hosszabb futamidejű projektekben jellemzően vannak olyan optimalizálási ciklusok, amikor az egyik terület javítása egy másik terület korlátait tárja fel, és ezeket jellemzően akkor korlátozzák, amikor a teljesítmény elfogadhatóvá válik, vagy a nyereség túl kicsi vagy költséges lesz.

Mivel a teljesítmény része a program specifikációjának – egy szokatlanul lassú program nem alkalmas a célra: egy videojáték 60 Hz-es (másodpercenkénti képkocka) sebessége elfogadható, de a másodpercenkénti 6 képkocka elfogadhatatlanul szaggatott -, a teljesítményt már a kezdetektől fogva figyelembe kell venni, hogy a rendszer megfelelő teljesítményt tudjon nyújtani, és a korai prototípusoknak nagyjából elfogadható teljesítményt kell nyújtaniuk ahhoz, hogy bízni lehessen abban, hogy a végleges rendszer (optimalizálással) elfogadható teljesítményt fog elérni. Ezt néha elmulasztják abban a hitben, hogy az optimalizálás később mindig elvégezhető, aminek eredményeképpen a prototípus rendszerek túlságosan lassúak – gyakran egy nagyságrenddel vagy annál is nagyobb mértékben -, és a rendszerek végül kudarcot vallanak, mert architekturálisan nem tudják elérni a teljesítménycélokat, mint például az Intel 432 (1981); vagy évekig kell dolgozni az elfogadható teljesítmény elérésén, mint például a Java (1995), amely csak a HotSpot (1999) segítségével érte el az elfogadható teljesítményt. A bizonytalanság és a kockázat jelentős forrása lehet, hogy a teljesítmény milyen mértékben változik a prototípus és a sorozatgyártású rendszer között, és mennyire optimalizálható.

Tervezési szintSzerkesztés

A legmagasabb szinten a terv úgy optimalizálható, hogy a célok, a korlátok és a várható használat/terhelés figyelembevételével a lehető legjobban kihasználja a rendelkezésre álló erőforrásokat. Egy rendszer architekturális kialakítása túlnyomórészt befolyásolja a rendszer teljesítményét. Például egy olyan rendszert, amely hálózati késleltetéshez kötött (ahol a hálózati késleltetés a fő korlátja az általános teljesítménynek), úgy optimalizálnánk, hogy minimalizáljuk a hálózati utazásokat, ideális esetben egyetlen kérést (vagy kérés nélkül, mint egy push protokollban), nem pedig többszörös körutazást végezve. A tervezés kiválasztása a céloktól függ: egy fordítóprogram tervezésekor, ha a gyors fordítás a fő prioritás, egy egyutas fordítóprogram gyorsabb, mint egy többutas fordítóprogram (azonos munkát feltételezve), de ha a kimeneti kód sebessége a cél, egy lassabb, többutas fordítóprogram jobban teljesíti a célt, még akkor is, ha önmagában több időt vesz igénybe. A platform és a programozási nyelv kiválasztása ezen a szinten történik, és ezek megváltoztatása gyakran teljes átírást igényel, bár egy moduláris rendszer lehetővé teheti csak néhány komponens átírását – például egy Python program teljesítménykritikus részeit át lehet írni C-ben. Egy elosztott rendszerben az architektúra (kliens-szerver, peer-to-peer stb.) kiválasztása a tervezés szintjén történik, és nehéz lehet megváltoztatni, különösen, ha nem lehet minden komponenst szinkronban cserélni (pl., régi kliensek).

Algoritmusok és adatszerkezetekSzerkesztés

Az átfogó terv alapján a hatékony algoritmusok és adatszerkezetek jó kiválasztása, valamint ezen algoritmusok és adatszerkezetek hatékony megvalósítása következik. A tervezés után az algoritmusok és adatstruktúrák kiválasztása jobban befolyásolja a hatékonyságot, mint a program bármely más aspektusa. Általában az adatszerkezeteket nehezebb megváltoztatni, mint az algoritmusokat, mivel egy adatszerkezeti feltételezés és annak teljesítményfeltételei az egész programban használatosak, bár ez minimalizálható az absztrakt adattípusok használatával a függvénydefiníciókban, és a konkrét adatszerkezeti definíciók néhány helyre való korlátozásával.

Az algoritmusok esetében ez elsősorban abból áll, hogy az algoritmusok állandó O(1), logaritmikus O(log n), lineáris O(n), vagy bizonyos esetekben log-lineáris O(n log n) bemenetűek (mind térben, mind időben). Az O(n2) kvadratikus komplexitású algoritmusok nem skálázódnak, és még a lineáris algoritmusok is problémákat okoznak, ha többször hívják őket, ezért jellemzően konstans vagy logaritmikus algoritmusokkal helyettesítik őket, ha lehetséges.

Aszimptotikus növekedési sorrenden túl a konstans tényezők is számítanak: egy aszimptotikusan lassabb algoritmus gyorsabb vagy kisebb (mert egyszerűbb) lehet, mint egy aszimptotikusan gyorsabb algoritmus, ha mindketten kis bemenettel szembesülnek, ami a valóságban előforduló eset lehet. Gyakran egy hibrid algoritmus nyújtja a legjobb teljesítményt, mivel ez a kompromisszum a mérettel változik.

A teljesítmény javításának általános technikája a munka elkerülése. Jó példa erre a gyors útvonal használata a gyakori esetekben, ami a teljesítményt a felesleges munka elkerülésével javítja. Például egyszerű szövegkiosztási algoritmus használata latin szöveghez, és csak összetett írásmódok, például a devanagari esetében váltás összetett kiosztási algoritmusra. Egy másik fontos technika a gyorsítótárazás, különösen a memoizálás, amellyel elkerülhetők a felesleges számítások. A gyorsítótárazás fontossága miatt a rendszerben gyakran több szintű gyorsítótárazás van, ami a memóriahasználatból és az elavult gyorsítótárakból eredő korrektségi problémákhoz vezethet.

ForráskódszintSzerkesztés

Az általános algoritmusokon és azok absztrakt gépen történő megvalósításán túl a konkrét forráskódszintű döntések is jelentős különbséget jelenthetnek. Például a korai C fordítóprogramokon a while(1) lassabb volt, mint a for(;;) egy feltétel nélküli ciklus esetében, mert a while(1) kiértékelte az 1-et, majd volt egy feltételes ugrás, amely tesztelte, hogy igaz-e, míg a for (;;) egy feltétel nélküli ugrás volt . Néhány optimalizálást (mint például ezt) manapság már az optimalizáló fordítóprogramok is elvégezhetnek. Ez a forrásnyelvtől, a célgépnyelvtől és a fordítóprogramtól függ, és egyaránt nehéz lehet megérteni vagy megjósolni, valamint idővel változik; ez egy olyan kulcsfontosságú hely, ahol a fordítók és a gépi kód megértése javíthatja a teljesítményt. A ciklusinvariáns kódmozgatás és a visszatérési értékek optimalizálása olyan optimalizációk példái, amelyek csökkentik a segédváltozók szükségességét, és a körkörös optimalizációk elkerülése révén akár gyorsabb teljesítményt is eredményezhetnek.

Build levelEdit

A forráskód és a fordítási szint között a direktívák és a build flagek a forráskódban, illetve a fordítóban lévő teljesítményopciók hangolására használhatók, például a preprocesszor-definíciók használatával letilthatók a nem szükséges szoftverfunkciók, optimalizálás bizonyos processzormodellekre vagy hardverképességekre, vagy például az elágazások előrejelzése. A forrásalapú szoftverterjesztő rendszerek, mint például a BSD Ports és a Gentoo Portage kihasználhatják az optimalizálás ezen formáját.

Compile levelEdit

Az optimalizáló fordító használata általában biztosítja, hogy a futtatható program legalább annyira optimalizált legyen, amennyire a fordító képes előre jelezni.

Assembly levelEdit

A legalacsonyabb szinten az adott hardverplatformra tervezett assembly nyelvvel írt kód a leghatékonyabb és legkompaktabb kódot eredményezheti, ha a programozó kihasználja a gépi utasítások teljes repertoárját. A beágyazott rendszereken használt számos operációs rendszert hagyományosan asszambler kódban írnak emiatt. A programokat (a nagyon kis programok kivételével) ritkán írják az elejétől a végéig assembly-ben az ezzel járó idő és költség miatt. A legtöbbet egy magas szintű nyelvből fordítják le assemblyre, és onnan kézzel optimalizálják. Ha a hatékonyság és a méret kevésbé fontos, a nagy részeket meg lehet írni magas szintű nyelven.

A modernebb optimalizáló fordítók és a legújabb CPU-k nagyobb komplexitása miatt nehezebb hatékonyabb kódot írni, mint amit a fordító generál, és kevés projektnek van szüksége erre a “végső” optimalizálási lépésre.

A manapság írt kódok nagy részét úgy tervezik, hogy a lehető legtöbb gépen fussanak. Ennek következtében a programozók és a fordítók nem mindig használják ki az újabb CPU-k által biztosított hatékonyabb utasításokat vagy a régebbi modellek sajátosságait. Ráadásul egy adott processzorra hangolt assembly kód, amely nem használ ilyen utasításokat, egy másik processzoron még mindig szuboptimális lehet, ami a kód más hangolását várja el.

A programozók ma jellemzően assembly nyelven való írás helyett inkább disassemblerrel elemzik a fordítóprogram kimenetét, és úgy módosítják a magas szintű forráskódot, hogy az hatékonyabban fordítható legyen, vagy megértik, miért nem hatékony.

FutásidőSzerkesztés

A just-in-time fordítóprogramok a futásidejű adatok alapján testreszabott gépi kódot állíthatnak elő, a fordítási többletköltségek árán. Ez a technika a legkorábbi reguláris kifejezésmotorokig nyúlik vissza, és a Java HotSpot és a V8 for JavaScript révén vált széles körben elterjedté. Bizonyos esetekben az adaptív optimalizálás képes lehet a statikus fordítók képességeit meghaladó futásidejű optimalizálásra azáltal, hogy a paramétereket dinamikusan az aktuális bemeneti adatoknak vagy más tényezőknek megfelelően módosítja.

A profilvezérelt optimalizálás egy futásidejű profilokon alapuló, futásidő előtti (AOT) fordítási optimalizálási technika, amely az adaptív optimalizálás dinamikus technikájának statikus “átlagos eset” analógjához hasonlít.

Az önmagát módosító kód a futásidejű körülményekre reagálva módosíthatja önmagát a kód optimalizálása érdekében; ez inkább az assembly nyelvű programokban volt elterjedt.

Egyes CPU-tervek képesek bizonyos optimalizálásokat futásidőben elvégezni. Néhány példa erre a soron kívüli végrehajtás, a spekulatív végrehajtás, az utasításvezetékek és az elágazás-előrejelzők. A fordítók segíthetnek a programnak kihasználni ezeket a CPU-funkciókat, például utasításütemezéssel.

Platformfüggő és platformfüggetlen optimalizálásokSzerkesztés

A kódoptimalizálás is nagyjából kategorizálható platformfüggő és platformfüggetlen technikákra. Míg az utóbbiak a legtöbb vagy minden platformon hatékonyak, addig a platformfüggő technikák egy adott platform speciális tulajdonságait használják ki, vagy az adott platformtól vagy akár az adott processzortól függő paraméterekre támaszkodnak. Ezért szükség lehet ugyanannak a kódnak a különböző processzorokhoz való különböző változatainak megírására vagy előállítására. Például a fordítási szintű optimalizálás esetében a platformfüggetlen technikák olyan általános technikák (például ciklusok felgöngyölítése, függvényhívások csökkentése, memóriahatékony rutinok, feltételek csökkentése stb.), amelyek a legtöbb CPU-architektúrára hasonló módon hatnak. A platformfüggetlen optimalizálás nagyszerű példáját a belső for-hurokkal mutatták be, ahol megfigyelték, hogy egy belső for-hurokkal ellátott ciklus több számítást végez egységnyi idő alatt, mint egy belső for-hurok nélküli vagy egy belső while-hurokkal ellátott ciklus. Ezek általában a program befejezéséhez szükséges teljes utasítási útvonal hosszának csökkentését és/vagy a teljes memóriahasználat csökkentését szolgálják a folyamat során. Másrészt a platformfüggő technikák magukban foglalják az utasításütemezést, az utasításszintű párhuzamosítást, az adatszintű párhuzamosítást, a gyorsítótár-optimalizálási technikákat (azaz a különböző platformokon eltérő paramétereket), és az optimális utasításütemezés még ugyanazon architektúra különböző processzorain is eltérő lehet.