Zsebkonzol

Zsebkonzol

Tanulófeladat

Bevezetés

Elágazás nélküli kód

A feltételes utasítások fogalma mutatja be a jcc-t, a feltételen alapuló elágazásra szolgáló utasítások családját. A jcc csak akkor adja át a vezérlést a program egy másik pontjára, ha a feltétele teljesül.

A modern CPU-k azonban gyorsak, és sok utasítást képesek párhuzamosan végrehajtani. Túl költséges lenne leállítani minden munkát, miközben a CPU egy feltétel futásidejű ellenőrzésére vár.

Ezért a CPU belül nem vár. Elágazás-előrejelzőt használ, hogy megtippelje egy feltétel kimenetelét, és spekulatívan elkezdi végrehajtani a megjósolt útvonalat.

Ha a jóslat helyes, a végrehajtás úgy folytatódik, mintha nem is lett volna elágazás. Ha viszont a jóslat hibás, a CPU-nak el kell dobnia a megjósolt útvonalon végzett munkát, és újra kell kezdenie a helyes útvonalon.

Ezt hibás elágazás-előrejelzésnek nevezzük, és költséges: késleltetéssel jár.

Ha egy elágazást nehéz megjósolni, ezek a hibás előrejelzések összeadódnak. Ilyenkor néha jobb teljesen elkerülni az elágazást, hogy ugyanazok az utasítások fussanak le, a feltételtől függetlenül. Az olyan kódot, amely jcc nélkül választ az értékek között, elágazás nélkülinek nevezzük.

Többféleképpen is felépíthető a kód úgy, hogy ritkábban ágazzon el, vagy hogy a megmaradó elágazásai kiszámíthatóbbak legyenek. Az x86-64 különösen két utasításcsaládot kínál, amelyeket gyakran használnak elágazás nélküli kód írásához.

Feltételes mozgatások

A cmovcc utasításcsalád csak akkor másolja a forrásoperandust a célba, ha egy adott feltétel teljesül. Ellenkező esetben a cél változatlan marad.

A cc utótag ugyanazt a névadást követi, mint a jcc-nél, ugyanazzal a jelentéssel:

utasítás mozgatás feltétele
cmove A == B, miután cmp A, B
cmovne A != B, miután cmp A, B
cmovl A < B (előjeles), miután cmp A, B
cmovb A < B (előjel nélküli), miután cmp A, B
cmovg A > B (előjeles), miután cmp A, B
cmova A > B (előjel nélküli), miután cmp A, B

A szabályok hasonlóak, mint a jcc-nél:

  1. Az l és a g az előjeles összehasonlításokhoz használatos, a b és az a pedig az előjel nélküliekhez.
  2. Ha az utótaghoz e-t fűzünk, az az egyenlőséget is magában foglalja.
  3. Az n hozzáfűzése tagadja a feltételt.
  4. Vannak olyan változatok is, amelyek egy beállított jelzőbitre hivatkoznak. Például a cmovz és a cmovc.

Vegyük egy rdi-ben lévő előjeles egész szám abszolút értékét, amelyet rax-ben adunk vissza. jcc-vel megírva a függvény két útvonal közül választ:

abs_branch:
    mov rax, rdi
    cmp rax, 0
    jge .done
    neg rax
.done:
    ret

cmovcc-vel megírva mindkét lehetséges érték feltétel nélkül kiszámításra kerül, és a feltételes mozgatás választ közülük:

abs_branchless:
    mov rax, rdi
    neg rax            ; rax = -rdi
    cmp rdi, 0
    cmovge rax, rdi    ; the value in `rdi` is moved to `rax` if rdi >= 0
                       ; otherwise, it stays the same, i.e., -rdi
    ; now rax = abs(rdi)
    ret

Az elágazás nélküli változat mindig ugyanazokat az utasításokat hajtja végre. Nincs olyan jcc, amit az előrejelzőnek meg kellene tippelnie.

A cmovcc célja csak regiszter lehet. A forrás lehet regiszter vagy memóriacím, de azonnali érték nem.

Note

A fenti kódban a neg utasítás az eredménytől függően több jelzőbitet is beállít, köztük az előjelbitet (SF). Ez azt jelenti, hogy a cmp rdi, 0 teljesen elhagyható:

abs_branchless:
    mov rax, rdi
    neg rax            ; rax = -rdi. `neg` sets SF = 1 if the result is negative
    cmovs rax, rdi     ; if SF == 1 (i.e., rax < 0), replace rax with rdi
                       ; otherwise rax stays = -rdi, which is non-negative
    ; now rax = abs(rdi)
    ret

Pozitív rdi esetén a neg negatív eredményt ad, és SF == 1, így a cmovs visszaállítja rdi-t. Nulla vagy negatív rdi esetén a neg nem negatív eredményt ad, és SF == 0, így rax megtartja a negált értéket (ami a helyes abszolút érték).

Bár assemblyben a cmp a fő módja az értékek összehasonlításának, sok utasítás a saját eredménye alapján is beállít jelzőbiteket. Az utasítás által érintett jelzőbitek teljes listája általában az adott utasítás referenciájában található. A neg esetében konkrétan ezek: SF, ZF, CF, OF és PF.

Feltételes beállítás

A setcc utasításcsalád 1-re állítja a céloperandust, ha egy adott feltétel teljesül, egyébként pedig 0-ra. A cél mindig 8 bites operandus.

A cc utótag ugyanazt a névadást követi, mint a jcc-nél és a cmovcc-nél. Például a setz 1-re állítja a célt, ha ZF == 1, egyébként pedig 0-ra.

Mivel a cél 8 bites, gyakori, hogy a setcc-t egy movzx követi, amikor szélesebb értékre van szükség:

cmp rdi, rsi
setg al             ; al = 1 if rdi > rsi (signed), 0 otherwise
movzx eax, al       ; eax (and rax) = 1 or 0, with the upper bits cleared

Ez egy gyakori idiom arra, hogy egy összehasonlítás eredményét 0 vagy 1 egész számmá alakítsuk.

Utasítások

Firmware-t írsz egy retro kézi konzol pontozási rendszeréhez. A gép CPU-ja szerény, és az eredménytábla kijelzője rögzített ütemben frissül. Hogy a kép folyamatos maradjon, a pontozó rutinoknak kiszámítható számú ciklusban kell lefutniuk, függetlenül attól, mit csinál éppen a játékos.

Négy részfeladatod van.

Note

A részfeladatok kódja legyen elágazás nélküli. Használj cmovcc, setcc utasításokat és aritmetikát feltételes ugrások helyett.

1. Adj hozzá bónuszt a kijelző túlcsordulása nélkül

A kézi konzol LCD-jének pontozási területén hat tizedesjegy fér el. 999999 feletti számot nem tud megjeleníteni. Amikor egy bónusz a futó összeget e határ fölé vinné, a megjelenített összeg inkább a maximumon marad, ahelyett hogy körbefordulna.

Definiálj egy add_bonus függvényt, amely egy aktuális összeg és egy hozzáadandó bónusz alapján az új, 999999-nél levágott összeget adja vissza. A paraméterek sorrendben:

  1. total: az aktuális pontszámösszeg (mindig 0 és 999999 között)
  2. bonus: a hozzáadandó bónusz (mindig nemnegatív)

A visszatérési érték total + bonus, ha ez az összeg kisebb vagy egyenlő, mint 999999, egyébként pedig 999999:

add_bonus(500, 100);
// => 600
add_bonus(999990, 50);
// => 999999
add_bonus(999999, 0);
// => 999999

Mindkét paraméter és a visszatérési érték is 64 bites előjeles egész szám.

Note

Feltételezheted, hogy a total + bonus nem csordítja túl a 64 bites előjeles egészt.

2. Hasonlíts össze két pontszámot

A kézi konzol a mentési fájljában egy kis ranglistát vezet a játékos legjobb friss pontszámairól. Minden játék után az új pontszám bekerül a ranglista megfelelő helyére. A rendező rutin megkérdezi, hogy két pontszám közül melyik van előrébb, és a választ egy kis egész számként várja.

Definiálj egy compare_scores függvényt, amely két pontszám alapján a következőt adja vissza:

  • +1, ha az első pontszám nagyobb
  • -1, ha az első pontszám kisebb
  • 0, ha a két pontszám egyenlő

Példa:

compare_scores(500, 300);
// => 1
compare_scores(300, 500);
// => -1
compare_scores(500, 500);
// => 0

Mindkét paraméter és a visszatérési érték is 64 bites előjeles egész szám.

3. Érvényesíts egy nyers pontszámot

A kézi konzol a linkkábelén keresztül csatlakozhat egy másik egységhez, hogy többjátékos mérkőzés után megosszák a pontszámokat. A kábel elektromosan zajos, és a beérkező bájtok időnként megsérülnek, az érvényes minimum alatti vagy az érvényes maximum feletti értékeket eredményezve. Az érvényesítő minden beérkező pontszámot az érvényes tartományba vág, mielőtt a rendszer többi része látná.

Definiálj egy validate_score függvényt, amely egy nyers pontszám és az érvényes határok alapján a [min, max] tartományba vágott pontszámot adja vissza. A paraméterek sorrendben:

  1. score: a linkkábelen érkező nyers pontszám
  2. min: a legkisebb megengedett pontszám
  3. max: a legnagyobb megengedett pontszám

A visszatérési érték min, ha score < min, max, ha score > max, egyébként pedig score. Feltételezheted, hogy min <= max.

Példa:

validate_score(450, 0, 500);
// => 450
validate_score(-50, 0, 1530);
// => 0
validate_score(1234567, 0, 2999);
// => 2999

Minden paraméter és a visszatérési érték is 64 bites előjeles egész szám.

4. Kövesd nyomon a két legmagasabb pontszámot

Minden játékszesszió végén a kézi konzol átvizsgálja a mentési fájljában lévő legutóbbi játékok naplóját, hogy megtalálja a valaha rögzített két legmagasabb pontszámot. A napló a memóriában egy egyszerű tömb, amely 64 bites előjeles egészeket tartalmaz. Az átvizsgálás egyszer végigmegy a tömbön, és két futó maximumot tart: az eddig látott legmagasabbat és a második legmagasabbat.

Vedd figyelembe, hogy mindkét szám legalább 0 kell hogy legyen. A negatív számokat figyelmen kívül hagyja.

A függvény első változata egymásra épülő feltételes ugrásokkal íródott. Minden jelölt esetében a kód megkérdezi, hova illik (az első fölé, az első és a második közé, vagy sehova), és ennek megfelelően eltolja a futó maximumokat, elvetve minden negatív eredményt.

Ez működik. Mivel azonban a bemeneti értékek véletlenszerűek, minden feltételes ugrás sok hibás elágazás-előrejelzést eredményez. Ezért döntöttél úgy, hogy a fő ciklust elágazás nélkülire írod át.

Az alábbiakban a függvény látható, amelyben az elágazásokat tartalmazó rész ki van kommentelve:

; rdi = output buffer for two elements, rsi = input array address, rdx = number of elements in array
top_two:
    xor r8d, r8d                   ; first  = 0
    xor r9d, r9d                   ; second = 0
    xor ecx, ecx                   ; index = 0
    test rdx, rdx
    jz .done

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;                       BRANCHY CODE TO REFACTOR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;.loop:
;    mov rax, qword [rsi + 8*rcx]   ; candidate
;    inc rcx
;    cmp rax, r8
;    jle .check_second              ; candidate <= first, try second
;    mov r9, r8                     ; second = first
;    mov r8, rax                    ; first  = candidate
;    cmp rcx, rdx
;    jb .loop                       ; go to next iteration
;    jmp .done                      ; otherwise, we are done
;.check_second:
;    cmp rax, r9
;    jle .loop                      ; candidate <= second, go to next iteration
;    mov r9, rax                    ; second = candidate
;    cmp rcx, rdx
;    jb .loop                       ; go to next iteration
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

.done:
    mov qword [rdi], r8            ; save first
    mov qword [rdi + 8], r9        ; save second
    ret

Cseréld le a kikommentelt részt egy elágazás nélküli implementációra. Az új kódban az egyetlen megengedett ugrás az, amely visszaugrik a ciklus elejére, hogy megnézze a tömb következő elemét.

A függvénynek nincs visszatérési értéke, és ugyanazokat a paramétereket kapja, mint az elágazásokat tartalmazó változat:

  1. out: kimeneti puffer, ahova a függvény a két legmagasabb nemnegatív pontszámot írja csökkenő sorrendben, 64 bites előjeles egészekként
  2. array: 64 bites előjeles egészek bemeneti tömbje
  3. length: a tömb elemeinek száma, 64 bites előjel nélküli egészként
Note

Ha a tömb ismétlődő értékeket tartalmaz, a duplikátumok mindkét kimeneti helyen megjelenhetnek, ha ők a két legmagasabb pontszám.

Szerkesztés GitHubon A hivatkozás új ablakban vagy lapon nyílik meg
x86-64 Assembly Exercism

Készen állsz elkezdeni a(z) Zsebkonzol feladatot?

Iratkozz fel az Exercism-re, hogy megtanuld és elsajátítsd a(z) x86-64 Assembly nyelvet 22 fogalom130 feladat segítségével, valódi emberi mentorálással, mindez ingyen.