5.4 Feltételes végrehajtás: CSET/CSEL és feltételkódok

5.4.1 Motiváció és hatókör

Egy minimumkeresésben vagy állapotérték előállításakor gyakori feladat: két érték összehasonlítása után vagy egy logikai eredményre, vagy a két érték egyikére van szükségünk. Ehhez nem feltétlenül kell megváltoztatni a program végrehajtási útvonalát. Az elágazásmentes kiválasztás (branchless selection) feltételtől függő adatot állít elő, miközben az utasítássor lineáris marad.

Az előfeltétel Az NZCV állapotjelzők és eredetük ismerete. Most azt vizsgáljuk, hogyan lesz az állapotjelzőkből C-szerű összehasonlítás, majd regiszterben tárolt eredmény.

Architektúra: az alfejezet valamennyi utasítása és regisztere A64 ISA, AArch64, ARMv8-A környezethez tartozik. Az assemblydirektívák GNU assemblerhez, AArch64/ELF célhoz tartoznak. A labor little-endian profilt használ; Cortex-M/Thumb-2 utasítás nem szerepel benne.

5.4.2 Feltételkód: logikai állítás az NZCV-ről

A feltételkód (condition code) az aktuális NZCV állapotjelzőkből képzett logikai feltételt jelöl. Nem tárol összehasonlítási operandusokat, és nem emlékszik azok C-típusára.

Az A64 CMP W1, W2 a 32 bites kivonás állapotjelzőit állítja elő. Ugyanezekből a jelzőkből választhatunk előjeles vagy előjel nélküli összehasonlítást. A választást a feltételkód végzi, nem a CMP.

Az alábbi táblázat összehasonlítási jelentése akkor érvényes, ha a jelzőket közvetlenül a megadott szélességű CMP a, b állította elő, és azóta nem íródtak felül.

A64-feltételkód NZCV-feltétel Jelentés CMP a, b után
EQ Z = 1 a == b
NE Z = 0 a != b
CS, más néven HS C = 1 Előjel nélkül a >= b
CC, más néven LO C = 0 Előjel nélkül a < b
MI N = 1 A kivonási eredmény legfelső bitje 1
PL N = 0 A kivonási eredmény legfelső bitje 0
VS V = 1 Előjeles túlcsordulás történt
VC V = 0 Nem történt előjeles túlcsordulás
HI C = 1 és Z = 0 Előjel nélkül a > b
LS C = 0 vagy Z = 1 Előjel nélkül a <= b
GE N = V Előjelesen a >= b
LT N ≠ V Előjelesen a < b
GT Z = 0 és N = V Előjelesen a > b
LE Z = 1 vagy N ≠ V Előjelesen a <= b
AL, NV Mindig igaz Az A64 feltételkiértékelésének különleges esetei

A CS és HS, illetve a CC és LO ugyanannak a kódnak két neve. Összehasonlításnál a HS és LO elnevezés gyakran olvashatóbb.

Az NV történeti neve ellenére az A64 itt használt feltételkiértékelésében nem „soha” jelentésű. Az A64 CSEL esetén az AL és NV egyaránt az első forrás kiválasztását eredményezi; az A64 CSET viszont egyiket sem fogadja el. Szokásos összehasonlításokhoz nincs rájuk szükség.

5.4.2.1 Levezetés: miért nem elég az N jelző?

Előjel nélküli kivonásnál C = 1 a kölcsönzés hiányát jelenti. Ez közvetlenül megadja az a >= b feltételt. A szigorú nagyobb relációhoz az egyenlőséget is ki kell zárni, ezért HI feltétele C = 1 és Z = 0.

Előjeles összehasonlításnál a kivonás túlcsordulhat. Vegyük a következő, 32 bites, levezetett esetet:

Ezért N = 0, Z = 0, C = 1, V = 1. Az N önmagában pozitív eredményt sugallna, mégis a < b. A túlcsordulást figyelembe vevő feltétel N ≠ V, vagyis LT.

Ugyanezek a bitminták előjel nélkül 2147483648 és 1 értékűek, ezért LO hamis. Ez nem ellentmondás: más értelmezési tartományban hasonlítunk.

5.4.3 CSET: feltételből 0 vagy 1

Az A64 CSET egy feltétel igazságértékét írja regiszterbe:

// A64 ISA, AArch64: 32 bites összehasonlítás és logikai eredmény.
cmp     w1, w2
cset    w3, lt

A W3 értéke 1, ha a feltétel igaz, különben 0. Ez megfelel egy előjeles C-összehasonlítás eredményének, ha az operandusok 32 bites előjeles értékeket jelentenek.

A CSET:

A CSET utasításálnév (instruction alias). Például az A64 CSET W3, LT mögött CSINC W3, WZR, WZR, GE áll: az invertált feltétel igaz ágán nulla, hamis ágán nulla plusz egy keletkezik. Emiatt az aliasokat elrejtő disassembly eltérhet a forrásszövegtől.

5.4.4 CSEL: két regiszterérték közötti választás

Az A64 CSEL jelentése:

// A64 ISA, AArch64: ha LT igaz, W5 = W1; különben W5 = W2.
csel    w5, w1, w2, lt

Az első forrás az igaz, a második a hamis ághoz tartozik. Mindhárom adatregiszter azonos szélességű: vagy W, vagy X nézetű. A kiválasztás magukat a bitmintákat másolja; nincs külön előjeles és előjel nélküli CSEL.

A CSEL nem módosítja az NZCV-t. Egyetlen összehasonlítás után ezért több CSET és CSEL is használhatja ugyanazt az állapotot.

Kódolási korlát: az A64 CSEL adatforrásai regiszterek, nem közvetlen konstansok (immediate). Nullához WZR vagy XZR használható; más konstanshoz előbb regiszterértéket kell előállítani.

A C feltételes operátorának (conditional operator) egyszerű leképezése:

C-kifejezés, megfelelő operandustípusokkal A64-kiválasztás CMP W1, W2 után
Előjeles a < b ? a : b CSEL W5, W1, W2, LT
Előjel nélküli a < b ? a : b CSEL W5, W1, W2, LO
Előjeles a >= b ? a : b CSEL W5, W1, W2, GE
Előjel nélküli a >= b ? a : b CSEL W5, W1, W2, HS

A C tényleges összehasonlítását a szokásos aritmetikai konverziók határozzák meg. Vegyes előjelű operandusoknál ne pusztán az egyik változó deklarációjából válassz feltételkódot.

5.4.4.1 Mit jelent itt az elágazásmentesség?

A CSEL nem választ következő végrehajtási címet, ezért ehhez a kiválasztáshoz nincs feltételes elágazás, amelyet az elágazásbecslőnek (branch predictor) meg kellene jósolnia.

Ettől azonban a megoldás nem automatikusan gyorsabb. Mindkét forrásérték előállítása munkát igényelhet, és az eredmény függ a jelzőktől, valamint a forrásregiszterektől. Az elágazásbecslés és az adatfüggőségek teljesítményhatását később tárgyaljuk.

Fontos szemantikai korlát: a C ?: csak a kiválasztott operandusát értékeli ki. A p != NULL ? *p : 0 ezért nem írható át általánosan feltétel nélküli betöltésre és utólagos CSEL-re. A hibás memóriaelérés addigra már megtörténne. Mellékhatásos függvényhívásoknál ugyanez az óvatosság szükséges.

Az elágazásmentesség önmagában állandó idejű, mellékcsatornákkal szemben védett végrehajtást sem garantál.

5.4.5 Kidolgozott példa: ugyanazok a bitek, kétféle sorrend

Munkakönyvtár: ~/arm-labor/a64/a017/ — az alfejezet minden laborfájlja ide kerül, és minden parancs innen futtatandó (a könyvtárrendet a Labor-alapkörnyezet alfejezet írja le).

mkdir -p ~/arm-labor/a64/a017
cd ~/arm-labor/a64/a017

A labor előjeles és előjel nélküli minimumot, két logikai eredményt, valamint egy egyszerű if/else választását állítja elő.

Az alábbi teljes C-referencia legyen a lab-csel-ref.c fájl:

#include <stdint.h>

int main(void)
{
    int32_t a = INT32_MIN;
    int32_t b = 1;
    uint32_t ua = (uint32_t)a;
    uint32_t ub = (uint32_t)b;

    int signed_less = a < b;
    int unsigned_less = ua < ub;
    int32_t signed_min = a < b ? a : b;
    uint32_t unsigned_min = ua < ub ? ua : ub;

    int selected;
    if (a < b)
        selected = 100;
    else
        selected = 7;

    return !(signed_less == 1 &&
             unsigned_less == 0 &&
             signed_min == INT32_MIN &&
             unsigned_min == 1 &&
             selected == 100);
}

Az előjelesből előjel nélkülibe alakítás itt jól definiált C-művelet. A referencia szemantikai ellenőrzésre szolgál; nem állítjuk, hogy a fordító pontosan az alábbi kézi utasítássort állítja elő.

A teljes assemblyprogram legyen a lab-csel.S fájl:

// A64 ISA, ARMv8-A, little-endian Linux.
// Direktívák: GNU as, AArch64/ELF cél.
.arch armv8-a
.text
.balign 4
.global _start
.type _start, %function

_start:
    movz    w1, #0x8000, lsl #16
    movz    w2, #1

    cmp     w1, w2
    cset    w3, lt
    cset    w4, lo
    csel    w5, w1, w2, lt
    csel    w6, w1, w2, lo

    movz    w9, #100
    movz    w10, #7
    csel    w7, w9, w10, lt

lab_selected:
    // Önellőrzés: W0 végül 0 siker, 1 eltérés esetén.
    eor     w0, w3, w2
    orr     w0, w0, w4

    cmp     w5, w1
    cset    w11, ne
    orr     w0, w0, w11

    cmp     w6, w2
    cset    w11, ne
    orr     w0, w0, w11

    cmp     w7, w9
    cset    w11, ne
    orr     w0, w0, w11

    // AArch64 Linux exit: X8 = 93, X0 = kilépési állapot.
    movz    x8, #93
    svc     #0

.size _start, . - _start
.section .note.GNU-stack, "", %progbits

Az első utasítás a MOVZ 16. bitre tolt mezőjével pontosan 0x80000000 értéket állít elő (a korlátokat lásd az A64 immediate-korlátok összefoglalójában). A MOVZ nem módosítja az NZCV-t, ezért a két konstans betöltése nem rontja el a későbbi kiválasztást.

A Linux-híváshoz itt SVC #0 szerepel; az utasítás és a Linux rendszerhívási szerződés nem ugyanaz a specifikációs réteg.

5.4.5.1 Összeállítás, linkelés és futtatás

Cél: AArch64 Linux, QEMU user-mode; host: Ubuntu 24.04 x86_64. A korábban telepített cross-eszközláncot használjuk.

# Target: qemu-user / AArch64, little-endian.
aarch64-linux-gnu-as -EL -march=armv8-a -g \
    -o lab-csel.o lab-csel.S
aarch64-linux-gnu-ld -EL -e _start \
    -o lab-csel lab-csel.o

qemu-aarch64 ./lab-csel
printf 'assembly kilépés=%d\n' "$?"

# Ugyanazon cél C-referenciája; statikus linkelés.
aarch64-linux-gnu-gcc -std=c17 -O0 -Wall -Wextra \
    -march=armv8-a -mlittle-endian -static \
    lab-csel-ref.c -o lab-csel-ref

qemu-aarch64 ./lab-csel-ref
printf 'C kilépés=%d\n' "$?"

Várt, nem futtatással igazolt eredmény: mindkét kilépési állapot 0. Maguk a programok nem írnak a szabványos kimenetre.

A lab_selected címkénél az ISA-szemantikából levezetett állapot:

A64-regiszter vagy jelző Várt érték Értelmezés
W3 1 Előjeles kisebb: igaz
W4 0 Előjel nélküli kisebb: hamis
W5 0x80000000 Előjeles minimum: −2147483648
W6 1 Előjel nélküli minimum
W7 100 Az if igaz ágának értéke
NZCV N=0, Z=0, C=1, V=1 Az első összehasonlítás jelzői

A W5 írása miatt X5 = 0x0000000080000000. Ez nem a negatív szám 64 bites előjelkiterjesztett alakja.

5.4.5.2 Megfigyelés GDB-vel

Az első terminálban:

# Target: qemu-user / AArch64; induláskor debuggerre vár.
qemu-aarch64 -g 1234 ./lab-csel

A második terminálban:

# Target: a fenti QEMU-folyamat AArch64 regiszterállapota.
gdb-multiarch -q ./lab-csel \
    -ex 'target remote localhost:1234' \
    -ex 'break lab_selected' \
    -ex 'continue' \
    -ex 'info registers x3 x4 x5 x6 x7 cpsr' \
    -ex 'p/x ($cpsr & 0xf0000000)'

A maszkolt jelzőérték várhatóan 0x30000000. Ezután a GDB continue parancsa végigfuttatja az önellenőrzést; a várt kilépési állapot továbbra is 0.

Az utasításkódolás ellenőrzése:

# Célállomány: A64 ELF; ez elemzés, nem futtatás.
aarch64-linux-gnu-objdump -d -M no-aliases ./lab-csel

Az aliasok nélkül a CSET helyén CSINC, a CMP helyén nulla célregiszteres SUBS jelenhet meg. Ez nem fordítási hiba.

5.4.6 Tipikus hibák és ellenpróbák

Hibakereséskor először az összehasonlítás szélességét és operandusait nézd meg, utána az NZCV-t, végül a feltételkódot. Egyszerre csak egy feltételezett hibát módosíts.

5.4.7 Feladatok

  1. (magyarázó, alapszint — cél: megértés-ellenőrzés) 32 bites A64 CMP után határozd meg az NZCV-t, valamint az LT és LO igazságértékét:

  2. (implementációs, középszint — cél: rutinépítés) Egészítsd ki a labort az előjeles maximum W12-be és az előjel nélküli maximum W13-ba választásával. A két utasítás az első összehasonlítás jelzőit használja. Ellenőrizd GDB-vel a lab_selected címkénél, majd vizsgáld meg az egyenlő operandusok esetét is. Az eredeti önellenőrzés a rögzített laboradatokra készült; más bemenetnél ehhez is igazítani kell.

  3. (hibakeresési, középszint — cél: rutinépítés) Szúrd be az A64 adds w14, w2, w2 utasítást közvetlenül az első CMP után. Fordítsd újra, figyeld meg a kilépési állapotot és az NZCV-t, majd egyetlen utasítás módosításával javítsd a hibát.

5.4.8 Megoldások

1. megoldás — Segítség. Számítsd ki mindhárom esetben a 32 bites kivonást bitminta szinten (nem előjeles aritmetikával), és nézd meg, mikor kölcsönöz (C), mikor negatív a legfelső bit (N), és mikor csordul túl előjelesen (V) — az LT feltétele N≠V, az LO feltétele C=0.

1. megoldás — Teljes megoldás. A 32 bites kivonásokból levezetett eredmények:

Operandusok Kivonási bitminta NZCV LT LO
0x00000000, 0x00000000 0x00000000 0, 1, 1, 0 Hamis Hamis
0xffffffff, 0x00000001 0xfffffffe 1, 0, 1, 0 Igaz Hamis
0x7fffffff, 0xffffffff 0x80000000 1, 0, 0, 1 Hamis Igaz

A harmadik sorban előjelesen 2147483647 és −1 szerepel. A különbség túlcsordul, ezért N = 1 ellenére az első operandus nagyobb: N = V.

2. megoldás — Segítség. A maximum kiválasztása pontosan a minimum feltételének a FORDÍTOTTJA — ha a minimumhoz LT/LO kellett, a maximumhoz melyik feltételkód adja a “nagyobb vagy egyenlő” döntést?

2. megoldás — Teljes megoldás. A következő kiegészítés például közvetlenül a lab_selected címke elé helyezhető:

// A64 ISA, AArch64; az eredeti CMP jelzőit használja.
csel    w12, w1, w2, ge
csel    w13, w1, w2, hs

A laboradatokkal W12 = 1, W13 = 0x80000000. Egyenlőségnél mindkét feltétel igaz, ezért az első forrást választják; a két forrás számszerűen azonos.

3. megoldás — Segítség. Az ADDS is módosítja az NZCV-t — mi történik az eredeti CMP jelzőivel, mire a CSET/CSEL sorozat ténylegesen kiértékelné őket?

3. megoldás — Teljes megoldás. A beszúrt összeadás eredménye 2, jelzői N=0, Z=0, C=0, V=0. Emiatt LT hamis, LO igaz lesz: W3 = 0, W4 = 1, a minimumok felcserélődnek, W7 = 7. A várt hibás kilépési állapot 1.

GDB-ben a lab_selected ponton a maszkolt NZCV várhatóan nulla. Az objdump megmutatja a jelzőket felülíró ADDS utasítást. Ha az összeadás jelzőire nincs szükség, egyetlen javítás elegendő: ADDS helyett A64 ADD. Az eredmény továbbra is 2, de az eredeti összehasonlítás jelzői megmaradnak.

5.4.9 Rövid összefoglaló

Az A64 feltételes kiválasztása az NZCV állapotát alakítja adattá, nem új végrehajtási útvonallá. Ugyanaz a CMP előjeles és előjel nélküli döntést is megalapozhat, de a megfelelő feltételkódot nekünk kell kiválasztanunk. A CSET 0 vagy 1 értéket ad, a CSEL két regiszterérték egyikét másolja; egyik sem módosítja a jelzőket. Figyelni kell az összehasonlítás szélességére, a közbeékelt jelzőfrissítésre és a C operanduskiértékelési szabályaira. Az elágazás elhagyása nem automatikus gyorsítás: a forrásértékek előállításának költsége és az adatfüggőségek továbbra is számítanak. A laborban a regiszterek és az NZCV együttes ellenőrzése teszi láthatóvá a hibát.

5.4.10 Források és ellenőrzési hiányok

A linkek és a dokumentumverziók közös állapota az Olvasói útmutató forrásállapot-táblájában van; itt csak az alfejezet saját megállapításai szerepelnek.

Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is ténylegesen fennmaradnak: a kódot itt nem fordítottuk és nem futtattuk. A kimenetek és regiszterállapotok levezetett, várt értékek. Az Ubuntu 24.04-ben telepített konkrét eszközverziókkal a buildet, a GDB regisztermegjelenítését és az aliasok megjelenítését a hallgatói laborban kell ellenőrizni. Teljesítménymérést nem végeztünk; a QEMU-futtatás nem igazol hardverhű ciklusszámot.