Osztályozólap

Osztályozólap

Tanulófeladat

Bevezetés

SIMD: maszkok és feltételek

A skaláris kód arra támaszkodik, hogy a különféle utasítások jelzőbiteket állítanak be, és ezek alapján egy adott feltétel hatására elágazik. A csomagolt értékek viszont nem egyetlen értéket, hanem egyszerre többet képviselnek. Egyetlen feltétel az egyik sávban megbukhat, miközben a másikban teljesül.

Ezért a SIMD-kód alapértelmezés szerint elágazásmentes.

A jelzőbitekre támaszkodás helyett a csomagolt összehasonlítások általában egy maszkot állítanak elő a céloperandusba. Az összehasonlítás minden sáv esetében az egész sávot egyesekkel tölti fel, ha a feltétel igaz, és nullákkal, ha hamis. Előjeles egészként értelmezve egy igaz sáv -1, egy hamis sáv pedig 0.

Ez a maszk aztán bitenkénti műveletekkel kombinálható, hogy adott sávokat kiszűrjünk.

Csomagolt összehasonlítások

A skaláris cmp abban az értelemben általános, hogy egyszerre több jelzőbit beállítására szolgál. Egy másik utasítás aztán felhasználhatja ezeket a jelzőbiteket, hogy elágazást hajtson végre vagy számításokat végezzen.

Mivel azonban egy csomagolt összehasonlítás egyszerre ellenőriz egy feltételt és számít ki egy maszkot, nem általános. Az összehasonlításnak meg kell adni a pontosan vizsgált feltételt.

Erre kétféle módon van lehetőség:

  • Az egész összehasonlításoknál a feltételt utótagként adjuk meg: eq az egyenlőséghez, gt a nagyobb, mint relációhoz. A többi változat ezek eredményének kombinálásával áll elő.
  • A lebegőpontos összehasonlításoknál a feltételt egy immediate konstansban kódolva adjuk meg. Az immediate különböző értékei más-más vizsgált feltételnek felelnek meg.

A szintaxis ugyanazt a szerkezetet követi, amit már láttunk, azzal a kivétellel, hogy az egész összehasonlításoknál egy meghatározott feltétel-utótagot használunk:

  • Egészeknél: p + cmp + feltétel + méret (b, w, d vagy q).
  • Lebegőpontos számoknál: cmp + p + méret (s vagy d). A feltételt egy immediate konstansban, extra operandusként adjuk át.
Egész összehasonlítások

Ahogy említettük, egészeknél csak egyenlőségre és nagyobb, mint relációra van összehasonlítás:

utasítás leírás
pcmpeqb, pcmpeqw, pcmpeqd, pcmpeqq sávonkénti egyenlőség
pcmpgtb, pcmpgtw, pcmpgtd, pcmpgtq sávonkénti előjeles nagyobb, mint
movdqa  xmm0, [rel scores]
pcmpgtd xmm0, [rel threshold] ; lane i = 0xFFFFFFFF (-1) if scores[i] > threshold[i], else 0

Kisebb, mint összehasonlítás létrehozásához használd a gt-t felcserélt operandusokkal: a < b == b > a.

Figyeld meg, hogy az összehasonlítás előjeles. Előjel nélküli összehasonlításhoz mindkét operandus legfelső bitjét át kell billenteni. Ez egy olyan maszkkal végzett XOR művelettel tehető meg, amelyben csak a legfelső bit van beállítva.

Note

Két hasznos idióma:

  1. Ha egy regisztert önmagával XOR-ozunk, csupa nulla lesz az eredmény.
  2. Ha egy regisztert önmagával hasonlítunk össze, csupa egyes lesz az eredmény.

Például:

pxor xmm4, xmm4    ; xmm4 = all zeros
pcmpeqd xmm7, xmm7 ; xmm7 = all ones

A csupa nulla és a csupa egyes gyakori maszkok arra, hogy rendre a „mindenhol hamis” és a „mindenhol igaz” állapotot kódolják. Arra is használhatók, hogy csomagolt 0-t vagy csomagolt -1-et ábrázoljanak, amelyek gyakori őrértékek. Például a string végét jelölő NUL egy 0.

Lebegőpontos összehasonlítások

A lebegőpontos sávok más felépítést használnak: egyetlen utasítás, a cmpps (illetve 64 bites sávoknál a cmppd), amelynél a feltétel immediate konstansként szerepel:

movaps xmm0, [rel readings]
cmpps  xmm0, [rel limits], 1 ; condition 1 is "less than": lane i = all ones if readings[i] < limits[i]

A NASM-nak vannak olyan pszeudo-műveletei is, amelyek a helyes immediate értékre képződnek le, és könnyebben megjegyezhetők. Az alábbiakban a px-ben szereplő x lehet s (32 bites lebegőpontos) vagy d (64 bites lebegőpontos):

pszeudo-művelet immediate összehasonlítás
cmpeqpx 0 a == b
cmpltpx 1 a < b
cmplepx 2 a <= b
cmpunordpx 3 a vagy b NaN
cmpneqpx 4 a != b
cmpnltpx 5 a >= b
cmpnlepx 6 a > b
cmpordpx 7 sem a, sem b nem NaN

Kiválasztás maszkkal

A maszk egy feltétel eredményét kódolja. Ezután arra használható, hogy sávonként válasszunk két értékhalmaz között e feltétel alapján. Az egyik értékből vesszük a sávot ott, ahol a maszk igaz, a másikból ott, ahol hamis:

; result = (a AND mask) OR (b AND NOT mask)
movdqa xmm2, xmm0  ; xmm0 holds the mask, keep a copy
pand   xmm2, xmm3  ; xmm2 = a AND mask: lanes of a where mask is true
pandn  xmm0, xmm4  ; xmm0 = NOT mask AND b: lanes of b where mask is false
por    xmm2, xmm0  ; combine the two halves

Figyeld meg, hogy a pandn aszimmetriája itt kifizetődik: a maszk a céloperandusban van, ott negálódik, és egyetlen utasításban választ a b-ből.

Ez a minta az elágazásmentes kiválasztás csomagolt formája. Minden sáv kiszámítódik, és egyedül a maszk dönti el, melyik érték marad meg; jcc sehol sem szerepel.

Dedikált blend utasítások

Vannak olyan utasítások, amelyek közvetlenül elvégzik ugyanezt a kiválasztást, és elemenként egy bitet olvasnak ki egy maszkregiszterből. Ezeket blend utasításoknak nevezzük:

utasítás elem maszk forrása
pblendvb bájt implicit xmm0
blendvps 32 bites sáv implicit xmm0
blendvpd 64 bites sáv implicit xmm0

Figyeld meg, hogy az első utasítás az egész szintaxist követi, míg a másik kettő a lebegőpontosét. Mivel azonban ezek az utasítások egyszerűen nyers bájtokat választanak ki, bármelyikük használható egészekkel és lebegőpontos számokkal is.

A blend minden elemnél megtartja a céloperandust, ha a hozzá tartozó maszkelem legfelső bitje törölt, és a forrásoperandust veszi, ha az be van állítva. Csak azt a legfelső bitet veszi figyelembe, amit egy összehasonlításból származó maszk teljesít is, hiszen a sávjai csupa egyesek vagy csupa nullák. A maszkregiszter mindig xmm0, ami implicit:

movaps   xmm0, [rel mask]  ; the selecting mask must be in xmm0
movaps   xmm1, [rel b]     ; destination: kept where the mask bit is clear
blendvps xmm1, [rel a]     ; source: taken where the mask bit is set

A pblendvb-vel más méreteknél is kiválaszthatók sávok egy összehasonlítási maszkból. Mivel egy igaz sávban minden bájt csupa egyes, a pblendvb az összeset kiválasztja.

Note

Ezek az utasítások mind a v betűt fűzik az elvégzett művelet (blend) után. Ez a v a változó szót jelöli, mert a kiválasztás nem statikus: egy regisztertől függ.

Vannak v nélküli változatok is, amelyek egy immediate érték alapján választanak. Ezek ugyanazt a mintát követik: kiválasztják az i sávot, ha az immediate i. bitje be van állítva.

A maszktól vissza a skaláris világba

Bár hatékony, a SIMD-kód messze nem olyan rugalmas, mint a skaláris kód. Sok helyzetben szükség van arra, hogy egy csomagolt regiszterből visszatérjünk a skaláris utasítások világába.

A movmsk utasításcsalád hidat képez a két világ között. Ezek az utasítások minden sáv legfelső bitjét emelik ki egy általános célú regiszterbe:

utasítás mit gyűjt ki az eredmény szélessége
pmovmskb 16 bájt legfelső bitje 16 bit
movmskps 4 duplaszó legfelső bitje 4 bit
movmskpd 2 négyszeres szó legfelső bitje 2 bit

Ha összehasonlítás után használjuk, minden beállított bit egy „igaz” sávot jelöl, minden törölt bit pedig egy „hamis” sávot. Ez az eredmény aztán a szokásos módon manipulálható skaláris utasításokkal. Például a popcnt megszámolja az egyezések számát, a tzcnt pedig megkeresi az elsőt.

Az általános célú regiszter 32 vagy 64 bites szélességű lehet.

Egy teljes vektor vizsgálata

A skaláris test utasításnak is van csomagolt változata: a ptest.

Hasonlít a skaláris megfelelőjéhez abban, hogy AND műveletet végez két operandus között, anélkül hogy módosítaná őket. A test-tel ellentétben a ptest ANDN műveletet is végez, amely tagadja az első operandust.

Így a ptest felfogható a pand és a pandn nem roncsoló változataként, amely az eredmény alapján állítja be a jelzőbiteket. Akárcsak ez a két utasítás, a ptest is egyetlen sávként kezeli a teljes SIMD-regisztert, ezért nem vesz fel méretelőtagot.

Ha egy AND művelet eredménye 0, akkor a ZF beáll, ha pedig az ANDN művelet eredménye 0, akkor a CF áll be. Ez azt jelenti, hogy a ptest egyszerre használható csupa egyes és csupa nulla maszk vizsgálatára:

  1. Ha a ptest-et egy regiszterre önmagával futtatjuk, az csak akkor állítja be a ZF-et, ha a regiszter csupa nulla. Ez utánozza azt a gyakori skaláris idiómát, amikor a test-et egy regiszterre önmagával futtatjuk 0 ellenőrzésére.
  2. Ha a ptest-et egy regiszterre csupa egyes maszkkal futtatjuk, az csak akkor állítja be a CF-et, ha a regiszter csupa egyes. Ráadásul a ZF-et csak akkor állítja be, ha a regiszter csupa nulla, így egyszerre mindkét maszk ellenőrizhető.
pxor    xmm0, xmm0   ; all zeros
pcmpeqb xmm1, xmm1   ; all ones
pcmpeqb xmm2, xmm2

ptest xmm0, xmm0     ; ZF set: a register against itself detects all zeros
ptest xmm0, xmm1     ; ZF set, CF clear: xmm0 is all zeros, not all ones
ptest xmm2, xmm1     ; CF is set only if xmm2 is all ones

A ptest eredménye a szokásos módon használható elágazáshoz vagy elágazásmentes utasításokban, például a setcc-ben vagy a cmovcc-ben.

Utasítások

Egy iskola pontozóállomását vezeted, és blokkonként pontozod az osztály eredményeit.

Minden blokk 4 eredményt tartalmaz, és az állomás ugyanazt a műveletet alkalmazza a blokk minden eredményére. A pontszám egy 32 bites lebegőpontos szám. Több lépés is maszkkal dolgozik: ez egy 4 sávból álló blokk, ahol minden sáv vagy csupa egyes (egy igen az adott eredményre), vagy csupa nulla (egy nem).

Öt részfeladat vár rád. Az operandusokat memóriacímeken keresztül kapod. Egyes részfeladatok az eredménycímre írják a válaszukat, mások viszont közvetlenül adják vissza.

Ebben a feladatban minden memóriacím 16 bájtra van igazítva.

Note

A feladat számításait SIMD utasításokkal kell elvégezni.

1. Jelöld meg a küszöb feletti pontszámokat

Az első lépés minden eredményt egy küszöbhöz viszonyítva pontoz. Egy eredmény akkor kerül a küszöb fölé, ha a pontszáma szigorúan nagyobb, mint a küszöb. A küszöbnél kisebb vagy azzal egyenlő pontszám viszont nem.

Valósítsd meg a flag_above_threshold függvényt, amely olyan maszkot épít, ahol minden küszöb fölötti pontszámhoz csupa egyes sáv, egyébként pedig csupa nulla sáv tartozik.

A függvény a következő argumentumokat kapja, ebben a sorrendben:

  • result: annak a puffernek a memóriacíme, ahová a maszk 4 sávját írja.
  • scores: a pontszámok memóriacíme, benne 4 darab 32 bites normál lebegőpontos számmal (soha nem NaN).
  • thresholds: az egyes sávok küszöbének memóriacíme, benne 4 darab 32 bites normál lebegőpontos számmal (soha nem NaN).
scores     = {72.0, 55.0, 90.0, 40.0}
thresholds = {60.0, 60.0, 60.0, 60.0}
result     = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}

A függvénynek nincs visszatérési értéke.

2. Jelöld meg a tökéletes pontszámokat

Egy külön jelentés emeli ki a tökéletes eredményeket, azokat, amelyek elérték a lehetséges legmagasabb pontszámot.

Valósítsd meg a flag_perfect függvényt, amely olyan maszkot épít, ahol minden, a maximumával egyenlő pontszámhoz csupa egyes sáv, egyébként pedig csupa nulla sáv tartozik.

A függvény a következő argumentumokat kapja, ebben a sorrendben:

  • result: annak a puffernek a memóriacíme, ahová a maszk 4 sávját írja.
  • scores: a pontszámok memóriacíme, benne 4 darab 32 bites normál lebegőpontos számmal (soha nem NaN).
  • maxima: az egyes sávok maximális pontszámának memóriacíme, benne 4 darab 32 bites normál lebegőpontos számmal (soha nem NaN).
scores = {100.0, 88.0, 100.0, 73.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}

A függvénynek nincs visszatérési értéke.

3. Rendelj rangot

Minden pontszám 1 és 3 közötti rangot ér el:

  • 1-es rang a 50.0-s sikerküszöbön lévő vagy annál alacsonyabb pontszámért.
  • 2-es rang az e küszöb fölötti, de a maximumot el nem érő pontszámért.
  • 3-as rang a tökéletes pontszámért, azaz amely megegyezik a maximummal.

Valósítsd meg az assign_ranks függvényt, amely kiírja az egyes pontszámok rangját.

A sikerküszöböt és a rangértékeket érdemes csomagolt konstansként definiálni a memóriában. Az előző két részfeladat függvényei újra felhasználhatók: egy pontszám legalább 2-es rangú, ha a küszöb fölött van, és 3-as rangú, ha megegyezik a maximummal.

A függvény a következő argumentumokat kapja, ebben a sorrendben:

  • result: annak a puffernek a memóriacíme, ahová a 4 rangot írja, mindegyiket 32 bites előjel nélküli egészként.
  • scores: a pontszámok memóriacíme, benne 4 darab 32 bites normál lebegőpontos számmal (soha nem NaN).
  • maxima: az egyes sávok maximális pontszámának memóriacíme, benne 4 darab 32 bites normál lebegőpontos számmal (soha nem NaN).
scores = {40.0, 75.0, 100.0, 60.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {1, 2, 3, 2}

A függvénynek nincs visszatérési értéke.

4. Számold meg a bukásokat

Az év során minden tanuló összesített rangot gyűjt. Az állomás összeszámolja, hogy a teljes csoportban hány rang marad egy sikerküszöb alatt, hogy megtervezze, hány pótórát kell indítani.

Valósítsd meg a count_failures függvényt, amely visszaadja, hogy minden blokkot figyelembe véve hány rang van szigorúan a sikerküszöb alatt. A küszöböt 4 azonos sávból álló blokként kapod, így egyszer betöltheted, és minden blokkhoz újra felhasználhatod.

A függvény a következő argumentumokat kapja, ebben a sorrendben:

  • ranks: a rangok memóriacíme, egész számú 4 sávos blokkot alkotnak, és minden rang 32 bites előjel nélküli egész szám.
  • block_count: a 4 sávos blokkok száma, mindig nagyobb, mint 0.
  • pass_threshold: a sikerküszöb memóriacíme, benne 4 azonos 32 bites egész számmal.
ranks          = {1, 2, 3, 1, 2, 2, 1, 3} // 2 blocks
block_count    = 2
pass_threshold = {2, 2, 2, 2}
// => 3

A függvény a darabszámot előjeles 32 bites egészként adja vissza.

5. Mindenki átment?

Mielőtt az adatokat irattárba helyeznék, az állomás ellenőrzi, hogy tiszta-e a csoport: akkor felel meg, ha egyetlen eredmény sem bukott meg egyetlen blokkban sem.

Valósítsd meg az all_passed függvényt, amely 1-et ad vissza, ha minden tanuló átment, egyébként pedig 0-t. Egy tanuló akkor megy át, ha a failing tömbben a hozzá tartozó sáv csupa nulla.

A függvény a következő argumentumokat kapja, ebben a sorrendben:

  • failing: a bukást jelző maszkok memóriacíme, egész számú 4 sávos blokk, ahol minden sáv csupa egyes vagy csupa nulla.
  • block_count: a 4 sávos blokkok száma, mindig nagyobb, mint 0.
failing     = {0x00000000, 0x00000000, 0x00000000, 0x00000000,
               0x00000000, 0x00000000, 0x00000000, 0x00000000} // 2 blocks
block_count = 2
// => 1

A függvény a választ előjeles 32 bites egészként adja vissza: 1 vagy 0.

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

Készen állsz elkezdeni a(z) Osztályozólap 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.