Az optimalizálás az ASSEMBLY nyelv egyik nagyon fontos része, hiszen hiába működik egy program, az sem árt, ha gyors vagy kicsi, esetleg mindkettő egyszerre. Én a programozási idő optimalizálását is ebbe a témakörbe vettem, mivel az sem árt, ha mondjuk a kitűzött feladatot minél gyorsabban meg tudjuk oldani, azaz minél kevesebbet gépeljünk feleslegesen, ahogy azt egy jó programozó csinálja.

GYORS, HATÉKONY PROGRAMOZÁS
Használjunk előre elkészített alap.asm-okat, amikbe csak bele kell irogatni a codeot, nem kell mindig újra írni a fejlécet.

Gyártsunk MACRO libary-kat, amiket csak be kell majd "include"-lni.

Gondoljuk át többször is a programrészleteket, mert egy jó ötlettel gyakran többre megyünk, mintha a ciklusidőket számolgatnánk.

Irogassunk gyakran megjegyzéseket, hogy később is emlékezzünk.

Találjunk ki egy stílust, és úgy írjuk a programokat: nálam pl. minden kisbetü, csak a PROC-ok kezdőbetüi nagyok, az utasítás eltolás 12, a hozzá tartozó paramétereké 25.

Ha valamilyen magasszintű nyelven is tudunk programozni, akkor egy bonyolultabb rutint egyszerűbb abban megírni, és csak miután működik, akkor írjuk át asm-ra.

Törekedjünk a moduláris programozásra, azaz minden nagyobb rutint külön objectbe fordítsuk, és csak ezután linkeljük őket össze, így megkíméljük magunkat a "ja, hogy ezt a változót már definiáltam, csak éppen wordként, és nem double wordként, ráadásul egy másik proc éppen így használja" kellemetlen helyzetektől...

SEBESSÉG OPTIMALIZÁLÁS
A szorzás (mul) nem tartozik a kegyetlenül gyors utasítások közé, az osztás (div) főleg nem :) Ezért próbáljuk ezeket minél jobban hanyagolni.

Kettő hatványával való szorzás esetén használjuk az SHL, kettő hatványával való osztás esetén az SHR utasítást.
Példa: y word 80-nal való szorzása:

     mov    ax,y      mov    bx,ax      shl    ax,4      shl    bx,6      add    ax,bx      mov    y,ax
Tehát ha ilyen páros számmal való szorzást szeretnénk elérni, akkor próbáljuk összerakni a számot 2 hatványaiból. Vegyük mondjuk a 72-t.
2^6=64, és ehhez még 8 kellene, ami 2^3. Tehát 72-vel való szorzás=n^6+n^3
A bitműveletek igencsak hasznosak.
"LOOP" helyett használj: dec cx                           jnz cimke -t, sokkal gyorsabb         mov      ah,al         shr      cx,1         jnc      put            ; ha nincs maradék byte, mivel                                 ; páros pixelt akartunk kiírni         stosb                   ; páratlant akartunk    put: rep      stosw          ; egy kicsit (kb 1.9*) gyorsabb
esetleg
                                ;  clocks                                 ; --------         mov      bl,col         ;     1         mov      bh,bl          ;     1         mov      ax,bx          ;     1         rol      eax,16         ;     3         mov      ax,bx          ;     1         shr      cx,1         jnc      @u         stosb    @u:  shr      cx,1         jnc      @v         stosw    @v:  rep      stosd
Ennek a felső részét is lehetne optimalizálni:
        mov      al,col         ; 1         mov      ah,al          ; 1         shl      eax,16         ; 3         bswap    ax             ; 1
Tehát ez egy clock-kal gyorsabb, de ne felejtsük el, hogy csak 486-on, és attól felfelé fog menni. A Nasm nem ismeri a "bswap 16 bites regiszter" utasítást, ezért ennek a hexadecimális kódját kell beírni.

Ha nem 286-osra írunk programot, akkor nyugodtan használhatjuk a lea utasítást is bizonyos esetekben.

        lea      eax,[eax+eax]          ; eax=eax*2         lea      eax,[eax*2+eax]        ; eax=eax*3         lea      eax,[eax*4]            ; eax=eax*4         lea      eax,[eax*4+eax]        ; eax=eax*5         lea      eax,[eax*8]            ; eax=eax*8         lea      eax,[eax*8+eax]        ; eax=eax*9
Például egy 12-vel való szorzást így is megvalósíthatunk:
     [eax=szám]         lea      eax,[eax*2+eax]         shl      eax,2
Néha nem ártana az NG-ket is olvasgatni.
          mov      eax,[valami]         add      eax,3         mov      [valami],eax
helyett az
        add      [valami],3
is megy :)
MÉRET OPTIMALIZÁLÁS
Most jön jól a "loop" utasítás, mert ez elég kevés byte

számoljunk ki mindent, amit csak lehet... pl. egy sinus táblához ott van a coprocessor, meg a tört kezelésre is ráadásul a koprocesszorral a tört számokat is viszonylag gyorsan lehet használni...

Ha 32bites szegmensben vagyunk, akkor a lehető legtöbb 32bites utasítást használjuk, mert így kevesebb helyet foglal a program, ugyanis ilyenkor a normális cuccokhoz nyomja hozzá a 66h-t és/vagy a 67h-t.

  • A bswap és a hasonló utasítások itt is nyerők...
  • OPTIMALIZÁLÁS PENTIUM PROCESSZORRA
    A 80486 az első Intel processzor, amelyben pipeline technikát használtak, ami azt jelenti, hogy 1 utasítást több részletben hajt végre, de egyszerre több utasításon is dolgozik a processzor. Ezt úgy megoldották meg, hogy a processzort részegységekre bontották, és egy-egy ilyen részegység az utasításnak csak egy speciális részfeladatát képes elvégezni. Itt egy rajz a részegységekről:
                 MEMORY INPUT ("memory load" unit)                         ł                                Ú------------ż                         Ŕ-----Â->Ú------------ż<-------->ł            ł                              ÚĹż ł FETCH      ł          ł            ł        Ú---------------------ŮłŔ>Ŕ------------Á--ż       ł            ł        ł (instruction pointer)ł                  ł       ł            ł        ł                      ł  Ú------------ż<-Ů       ł            ł        ł                      ł  ł DECODE     ł<-------->ł            ł        ł                      ł  Ŕ------------Á--ż       ł            ł        ł                      ł                  ł       ł            ł        ł                      ł  Ú------------ż<-Ů       ł CONTROL    ł        ł                     ÚŮ  ł ADDRESS C. ł<-------->ł            ł        ł                Ú----Ĺ-->Ŕ------------Á--ż       ł UNIT       ł        ł                ł    Ŕż                  ł       ł            ł Ú-----Á----------------Á-ż   Ŕ->Ú------------ż<-Ů       ł            ł ł REGISTER FILE          Ă----->ł EXECUTE    ł<-------->ł            ł Ŕ------------------------Ů<-----Á------------Á--ż       ł            ł                                                  ł       ł            ł                                  Ú------------ż<-Ů       ł            ł                                  ł WRITEBACK  ł<-------->ł            ł                                  Ŕ------------Á--ż       ł            ł                                                  ł       Ŕ------------Ů                                MEMORY OUTPUT <---Ů                               ("memory store" unit)
    A rajzból látható, hogy a control unit-nak kell kommunikálnia az összes részegységgel, hogy azok együtt tudjanak dolgozni. Egy olyan processzorban, amelyikben egyáltalán nincs pipeline, a leggyorsabb utasítás végrehajtásához is legalább 5 gépi ciklus kell. (MEMORY INPUT-> MEMORY OUTPUT)

    Ha a processzorban van pipeline, akkor egy utasítás átlagos végrehajtási ideje is 1 gépi ciklus lesz. Így ha egy 8 utasításból álló kód végrehajtása egy pipeline nélküli gépen optimális esetben 40 gépi ciklusba kerül (8*5), addig egy pipeline-nal rendelkező processzoron ez csak 5+7 = 12 gépi ciklusba. Az elsőnek 5 ugrásba kerül, az utána lévők egy lépéssel mögötte mennek :)

    Egy pipelinnal rendelkező processzor control unit - jának meg kell vizsgálnia, hogy:
    A) Az 1. részegységben (FETCH)

    Ha a 2..4 részegységben ugró utasítás van (mivel a jump az ip-t állítja) akkor az 1. részegységnek várnia kell, amíg az ugrás végre nem hajtódik, ami után "újraindul" , és behívja az új utasítást.

    Mivel a jump opcode-nak át kell mennie a decode részegységen, ezért ha a DECODE egység jump opcode-ot detektál, akkor leállítja a FETCH részegységet legalább 2 gépi ciklusra, amíg az ugrás végre nem hajtódik (Amíg az EXECUTE-ba nem ér). Így természetesen ciklusidőt vesztünk.
    B) A 3. részegységben (ADDRESS CALCULATION)

    Ha a 3. részegységben egy olyan regiszterre van szükség a címszámításhoz, ami a 4. részegységben használat alatt van, akkor kapunk 1 ciklusidő várakozást.

    Ez a pipeline technika viszont egy új optimalizálási problémát vet fel, méghozzá az utasításokat megfelelő sorrendben kell használni a processzor maximális teljesítményéhez. Mert ha nem megfelelő sorrendet alkalmazunk, akkor pipeline elakadás történik, amit elveszett ciklusidőknek érzékelünk. Íme egy példa a nem megfelelő és a javított utasítás sorrendre:

            MOV EDI,puffer          MOV EDI,puffer         MOV EAX,minta           MOV EAX,minta         MOV ECX,darab           ADD EDI,4    Loop:                        MOV ECX,darab         ADD EDI,4          Loop:         MOV [EDI],EAX           MOV [EDI],EAX         DEC ECX                 ADD EDI,4         JNZ Loop                DEC ECX                                 JNZ Loop
    A baloldali esetben a címgenerálásnak várni kell az előző utasítás eredményére, tehát a pipeline egy pillanatra elakad .
                     ----------------------------------------                       ADDRESS GENERATION STALLS (AGI)                  ----------------------------------------
    AGI akkor keletkezik, amikor egy regiszter, amit bázis vagy indexként használunk, cél volt az előző utasításban.Példa:
         add edx, 4      mov esi, [edx]  ; az előző utasításban cél volt...
    Pentiumon:
               pipeline lépés             pipe1  pipe2      címszámítás/fetch                 C      D    |      végrehajtás                       B      A    |                                                    V
    Ha C-nek szüksége van A eredményére, akkor 1 ciklusidőt kell várnia, míg A kiér a pipeline 2-ből.Ha C-nek szüksége van D eredményére, akkor 2 ciklusidőt kell várnia, míg A kiér a pipeline 2-ből.Egy példa:
         add esi, 4      pop ebx      dec ebx      mov edx, [esi]
    586-oson a mov parancsnak várnia kell az add-ra, mivel az AGI megállítja 1 clock-ra. A kód 3 időegység alatt fut le 586-oson.Agi keletkezik ekkor is:
         mov esp,ebp      pop ebp        |        V      mov esp,ebp      [agi]          ; pop az esp -t használja mutatóként      pop ebp
    A Pentium processzor legfontosabb újdonsága, hogy képes szuperskalár futtatásra, azaz egyszerre több utasítást képes végrehajtani. Ez annak köszönhető, hogy két pipeline található benne, melyek ha az utasítások sorrendje megfelelő, teljesen párhuzamosan tudnak müködni. Ezért a Pentiumon a legfontosabb optimalizálási feladat, hogy mindig kerüljön utasítás a második pipeline-ba is, és egyik pipeline-ban se történjék elakadás. Ezt a megfelelő utasítás sorrenddel és a rutinok párhuzamosításával érhetjük el. Lássunk egy példát az utóbbira:
            MOV ESI,forrás          MOV ESI,forrás         MOV EDI,cél             MOV EDI,cél         MOV ECX,darab           MOV ECX,darab/2     Loop:                  Loop:         MOV EAX,[ESI]           MOV EAX,[ESI]         ADD ESI,4               MOV EBX,[ESI+4]         MOV [EDI],EAX           ADD ESI,4         ADD EDI,4               MOV [EDI],EAX         DEC ECX                 MOV [EDI+4],EBX         JNZ Loop                ADD EDI,4                                 DEC ECX                                 JNZ Loop
    A dolgot bonyolítja, hogy a két pipeline nem teljesen szimmetrikus, s vannak utasítások, amik csak az elsőben, s vannak utasítások, amik csak a másodikban képesek lefutni.

    A pentium pipeline-ja 2 különböző pipeline-ra van szedve, amiket U és V pipeline-nak neveznek. Ennek segítségével 2 utasítást lehet párhuzamosan futtatni, így lehetőséget kapunk a 2 utasítás 1 ciklusidő alatti végrehajtására. Az U pipeline olyan, mint a 486-osokban, a V pipeline azonban csak egyszerű utasításokat tud kezelni.
    A kritériumok a "2 utasítás 1 ciklusidő alatt" eléréséhez:

    • Mindkét műveletnek egyszerűnek kell lennie
    • Nem lehet írás/olvasási függőség az utasítások között
    • Egyiknek sem lehet immediate-je vagy displacement-je

    Prefixes műveletek csak az U-pipeline-ban lehetnek (kivétel jz,jc,...), eltolás, forgatás (SHL,ROL,...) is csak itt lehet.
    Az egyszerű utasítások (ALU = ADD, stb.)

         1. Mov reg, reg/mem/immed      2. mov mem, reg/imm      3. ALU reg, reg/mem/immed      4. ALU mem, reg/immed      5. inc reg/mem      6. dec reg/mem      7. push reg/mem      8. pop reg      9. nop      10. lea reg/mem      11. Jmp/ Call / Jcc near                  ----------------------------------------                                CODE rendezés                  ----------------------------------------
    A műveleteknek cache határon kellene kezdődnie, ergo 32 byte 586-on 16 byte 486-oson, tehát használjuk az align parancsot...
                     ----------------------------------------                                ADAT rendezés                  ----------------------------------------
    3 plusz ciklus bűntetést kaphatunk, ha a kód nem megfelelő címen kezdődik.
          DWORD  -> 4-gyel osztható címen kezdődjön       WORD   -> 2-vel osztható címen kezdődjön       8 BYTE -> 8-cal osztható címen kezdődjön                  ----------------------------------------                            Regiszteres gyorsítás                  ----------------------------------------
    Minél többet használjuk az EAX-et, mivel a legtöbb művelet ekkor 1 byte-tal rövidebb. DS regisztert is minél többször használjuk hasonló okokból. ESP-vel hivatkozzunk a verem alsóbb szintjeire, mert gyorsabb, mint a push/pop páros.
                     ----------------------------------------                          Extra gyorsítási ötletek                  ----------------------------------------
    Kerüljük a bonyolult utasításokat (LEAVE,ENTER,LOOP, MOVx, STOSx...) egyszerű utasításokkal sokkal gyorsabb lesz a program.

    Használjunk kevés előjeles utasítást. A movzx helyett pl.

          xor eax,eax       mov al,[szam]
    A LEA utasítással nagyon sokat lehet gyorsítani, ráadásul a legroszabb esetben 2 vagy 3 AGI-t kapunk, és még így is sokkal gyorsabb.
    • Előjeles osztásCDQ helyett használjuk a
          mov edx,eax       sar edx,31
    utasításokat
    • Az ENTER helyett valami ilyesmit próbáljunk:
          push ebp       mov  ebp, esp       sub  esp, BYTE_COUNT
    • Ugrás optimalizálás

    Kétfajta jump van, az egyikkel -127 és 128 között ugrálhatunk, valamit a 32 bites verzió, amivel bárhova. A rövidebb egy kicsit gyorsabb, ezért használjuk a jmp short-ot jmp helyett, ahol csak lehet...

  • Unrolling jumps A belső ciklus optimalizálására a legjobb, pl:
  •             mov   ecx,10       @ide: stosb             loop  @ide
    helyett:
          stosb       stosb       stosb       stosb       ...       stosb
    EGYÉB TRÜKKÖK
    • ah nullázása: cbw, bár lassabb, mint a xor ah,ah
    • A bswap utasítás felhasználására az alábbi példa: Egy memória címre ki szeretnénk írni azt, hogy 'zso' és még egy számot. Ekkor:
         mov     eax,'zso '      mov     al,[talalt]      bswap   eax      mov     [edi],eax
    ennek lehetne egy 386-oson futó változata:
         mov     eax,'zso '      mov     al,[talalt]      xchg    ah,al      rol     eax,16      xchg    ah,al      mov     [edi],eax
    Ha eax=0, akkor eax:=0, kulonben eax:=0ffffffffh
         cmp             eax,1               ; Z flag allitasa      sbb             eax,eax             ; eax =0-Z      not             eax                 ; eax =ffffffff-eax
    String hossz számolás in: edi=offset; out: ecx=hossz
         mov             ecx,0ffffffffh      repnz           scasb      not             ecx
    eax = 0ffffffffh (32bites alkalmazás esetén, méretre optimalizáláskor)
         xor             eax,eax      not             eax
    rep movsb gyorsítva
         push            ecx      shr             ecx,02      rep             movsd      pop             ecx      and             ecx,03      rep             movsb
    Ezeket a kis programrészleteket GEM-eknek hívják. Akik több ilyet szeretnének látni, azok olvassanak IMPHOBIA-t, meg coder-l levéllistát. :)