Az utasításkészletek útvesztője
Egy adott processzor esetében a teljesítmény a "működési frekvencia x IPC" (Instructions Per Clock) képlettel írható le legegyszerűbben, ez az órajel és az órajelenként végrehajtott utasítások számának szorzata. Az órajel minden egyes processzortípus esetén ismert, és az egyes processzorcsaládok szerkezeti rajzát vizsgálva az is könnyen meghatározható, hogy az maximálisan hány utasítást tud kezelni órajelenként (azaz milyen "széles"). A valóságban persze nagyon ritkán lehet elérni a maximális kihasználtságot; ez ugyanúgy függ magától a programtól, mint az egyes részegységek (pl. L1 cache) méretétől. Általánosságban azonban elmondható, hogy egy kevésbé széles felépítés magasabb órajel mellett fel tudja venni a versenyt egy nála szélesebbel. Illetve még valamivel: újabb, okosabb utasításkészletekkel.
Az utasításkészletek fejlődése jól megfigyelhető tendenciákat követ: az újabb készletek tagjai egyre több munkát képesek elvégezni, így alkalmazásukkal alapvetően a végrehajtandó utasítások számát lehet csökkenteni. Mondhatjuk úgy is, hogy egy-egy új utasítás több korábbi feladatait váltja ki egy személyben, tehát a fenti "frekvencia x IPC" képlethez vegyük hozzá azt is, hogy egy új utasításkészletet alkalmazó program kevesebb utasításból is áll, mint korábbi verziója, tehát kevesebb utasítást kell lefuttatni az adott probléma megoldásához.
Az x86/x64 utasításkészletek a következőképpen bővültek napjainkig:
- Az eredeti x86 és x87 utasítások Single Instruction Single Data, azaz SISD felépítésűek és többnyire a:=a op b formájúak (pl. a:=a+b). Ez azt jelenti, hogy ezen utasítások nagyrészt egy bemeneti adatpáron (ritkábban egy adathármason vagy csak egyetlen adaton) dolgoznak, és az egyik bemenő értéket lecserélik az eredménnyel. Amennyiben tovább kell számolnunk mindkét bemenő értékkel, azokat a művelet előtt át kell másolni egy másik helyre.
- Az MMX megjelenése indította útjára az x86-os világban a Single Instruction Multiple Data, vagyis SIMD utasításkészletek karrierjét, amelyek formája [a1,a2,...]:=[a1,a2,...] op [b1,b2,...] (például [a1,a2,...]:=[a1+b1,a2+b2,...]). Több egymást követő adaton – azaz vektorokon – végrehajtják ugyanazt a műveletet, és az egyik bemenő adatsort írják felül az eredménnyel. A vektorok mérete a fejlődés során egyre nőtt, először a 64 bites MMX és 3DNow! került piacra, őket követték a 128 bites SSE-verziók, jelenleg pedig a 256 bites AVX-nél tartunk, így egy utasítással akár 8 db lebegőpontos számon is végezhetünk műveletet, bizonyos esetekben 7 külön utasítást megspórolva.
- Az AVX készlet bevezetésével még egy újítás történt: az eredmény már nem kötelezően írja felül a bemenő adatokat, célként megadható más terület is. Általánosan [c1,c2,...]:=[a1,a2,...] op [b1,b2,...] formában írhatóak fel, így ha a bemenő értékekre szükség van a későbbiekben is további számításokhoz, megspórolható egy adatmásolás.
- Még egy tendencia megfigyelhető, mégpedig maguk a végrehajtott műveletek is egyre bonyolultabbá válnak. Egyik triviális példa a speciális AES-utasítások megjelenése, amelyek önmagukban kiváltják a számításhoz szükséges akár 10-20 korábbi egyszerűbb utasítást, nagyban felgyorsítva az erre alapuló algoritmusokat. A tervezők ilyen esetekben elsősorban azt veszik figyelembe, hogy mely tipikus műveletsorok kerülnek gyakran egy-egy programban végrehajtásra. A Bulldozerben bemutatkozó FMA4-készlet az egyik leggyakoribb esetre ad megoldást a vektorokon dolgozó SIMD [d1,d2,...]:=[a1*b1+c1,a2*b2+c2,...] műveletek implementálásával, amelyeket ugyanannyi idő alatt képes végrehajtani a processzor, mint egy egyszerű szorzást vagy összeadást önmagában. Ezzel egyszerre az x86/x64 egy régi adósságát is törlesztették, mivel ezt a műveletet más architektúrák már régóta ismerik.
Természetesen mindennek komoly ára van: minél nagyobbak a vektorok és minél bonyolultabbak a rajtuk végrehajtott műveletek, annál több tranzisztort és energiát igényelnek a végrehajtóegységek. A tervezők viszont igyekeznek mindkettővel spórolni:
- általános megoldás, hogy az utasításkészleteket alkalmazó első processzorgenerációkban a legszélesebb vektormérethez nem építenek külön végrehajtókat, hanem feleakkora egységekkel számoltatják őket, így közel felére csökkentve a tranzisztorigényt. Erre korábbi példa a 128 bites SSE és SSE2 esetében a 64 bites végrehajtókkal ellátott Pentium 3, Pentium 4, K7 és K8.
- egy-egy végrehajtó egységet igyekeznek a lehető legjobban kihasználni, azaz több programszál utasításait futtatni rajta, így az egységek fogyasztása még mindig kisebb lesz, mintha kétszer annyit építenének be belőlük az ezekből kisajtolt többletteljesítmény mellett is. A multi-threading pontosan ezt a célt szolgálja, és a Bulldozer esetében mindkettő alkalmazását láthatjuk.
A Bulldozer esetében a támogatott utasításkészletek száma eddig nem látott mértékben bővült: az említett AVX, AES és FMA4 mellett már ismeri az SSE4.1 és SSE4.2 készletet is, így az Intel processzoraival összehasonlítva is naprakésszé vált. Ezenkívül a korábbi SSE5 terveiből átültetett XOP nevezetű is bekerült a listába.
Az XOP utasításai között találjuk az egész számok vektorainak d:=a*b+c számításait, a szomszédos vektorelemek összeadását/kivonását, vektorok elemeinek shiftelését és forgatását, két vektor összehasonlítását, a vektorok elemeinek (a C nyelv ? : operátorának megfelelő) feltételes másolását, valamint a lebegőpontos számok törtrészeinek kimásolását.
128 bites vektorokkal dolgozó utasítások:
- Vízszintes integer összeadás/kivonás: vektorbeli szomszédos 8/16/32 bites egész számok előjeles vagy előjeltelen összeadása, illetve előjeles kivonása, 16/32/64 bites eredménybe kiterjesztve.
- Integer szorzás+összeadás: két bemeneti 128 bites vektor elemeinek összeszorzása, az eredmény összeadása a harmadik bemeneti vektor elemeivel.
- Léptetés/forgatás vektorelemenként megadott bitszámmal: bemenete lépésszámok vektora, lehetővé téve a másik bemeneti vektor elemeinek eltérő méretékű léptetését vagy forgatását; biztosít egy olyan utasítást is, amivel a vektor összes eleme egyetlen megadott bitszámmal forgatható.
- Integer összehasonlítás: 8/16/32/64 bites értékek előjeles vagy előjeltelen összehasonlítása különböző megadott szempontok szerint (egyenlő, kisebb stb.), az eredménybe a feltételnek megfelelő elemek pozíciójába 1, a többi helyre 0 biteket írva.
- Byte permutáció: két bemeneti, 16 bites elemeket tartalmazó vektor bájtjainak másolása az eredménybe, másolás előtt a harmadik bemenet (selector) által meghatározott egyszerű transzformációkat (NOT, előjelkiterjesztés, ...) elvégezve az egyes értékeken.
128 vagy 256 bites vektorokkal dolgozó utasítások:
- Bitszintű feltételes másolás: két bemeneti vektor bitjeinek összefésülése az eredménybe, aszerint választva a két vektor bitjei körül, hogy a harmadik bemenet azonos pozíciójú bitje 0 vagy 1.
- Törtrészkiemelés: a törtrészek átmásolása egyetlen lebegőpontos értékből vagy 128/256 bites vektor elemeiből, egyszeres és dupla pontosságra egyaránt.
A cikk még nem ért véget, kérlek, lapozz!