Assembler (Jazyk symbolických adres)

(7. část)

Další instrukce logických operací

Další použití logických operací

Změna počtu bitů

Poměrně často je potřeba ukládat čísla do menšího počtu bitů než výchozí počet a také (možná ještě častěji) naopak do většího počtu bitů, např. šestnáctibitové číslo na osmibitové nebo osmibitové na 32-bitové.
Převod z většího počtu bitů na menší je jednoduchý, vezmeme pouze tento menší počet spodních bitů z většího počtu bitů, např. pomocí maskování. Ale pozor, při převodu většího čísla, než které lze uložit do menšího počtu bitů, dostaneme samozřejmě špatný výsledek.
Opačný převod, tj. z menšího počtu bitů na větší už tak jednoduchý není, záleží totiž na tom, zda máme čísla bez znaménka nebo se znaménkem. Pro bezznaménková čísla je to snadné, vynulujeme horní bity místa (např. registru) o větším počtu bitů pomocí maskování. Druhá a mnohem jednodušší možnost je použít instrukci MOVZX:

U čísel se znaménkem je tento převod složitější, proto Assembler obsahuje následující instrukce (bez operandů):

Otázka: Proč u převodu většího počtu bitů na menší nezáleží na tom, jestli se jedná o číslo se znaménkem nebo bez znaménka? A proč u opačného převodu na tom naopak záleží? Jak se převádějí čísla se znaménkem do většího počtu bitů?
Nápověda: způsob uložení celých čísel v doplňkovém dvojkovém kódu.

Zbytek po celočíselném dělení mocninami 2

Výpočet zbytku po dělení mocninami 2 pomocí instrukce DIV není zrovna rychlý (tato instrukce je jednou z nejdéle trvajících). Výsledek dostaneme mnohem rychleji a přitom velice jednoduše pomocí jednoduché úvahy. Tento zbytek je totiž roven hodnotě ve spodních bitech dělence, jejichž počet je roven oné mocnině 2, což je právě počet spodních nulových bitů dělitele (mocniny 2) a zároveň počet všech jedničkových bitů dělitele - 1. Nyní již je jasné, že použijeme mnohem rychlejší maskování místo pomalého dělení.
Například zbytek po dělení číslem 32 = 2^5 získáme nejrychleji pomocí instrukce AND dělenec, 1Fh.

Instrukce posuvů a rotací

Tyto instrukce jsou velmi dobrým pomocníkem každému, kdo je umí používat. Jedná se o bitový posuv uvnitř paměťového místa (slabiky, slova, nebo dvojslova). Při instrukcích posuvů jsou bity, které jsou z místa vysunuty ven, ztraceny (poslední vysunutý bit je přenesen do vlajky CF). Rotace jsou takové posuvy, u kterých se vysunuté bity opět do místa postupně vsunou z druhé strany, tj. bity jsou v paměťovém místě rotovány. Pozice bitů jsou v paměťovém místu číslovány od 0 vzestupně od nejméně významných k významnějším (zprava doleva, paměťové místo se kreslí s nějméně významnými bity vpravo). V následujících instrukcích je cíl, ve kterém budou bity posouvány, specifikován použitým registrem, nebo označením paměťového místa, a počet posunutí je zadán buď přímo 8-bitovým číslem, nebo je v registru CL (musí se uvést).

Posuvy:

Kromě těchto instrukcí existují ještě instrukce posuvu ze zdroje (registru nebo paměti) do cíle SHLD a SHRD, ale na ty už se případní zájemce podívají sami.

Rotace:

Použití posuvů a rotací

Zjištění hodnoty jednotlivých bitů
Jestliže potřebujeme zjistit, jakou hodnotu nese některý z bitů místa, stačí jej posunout nebo rotovat doprava. Hodnotu, kterou bit nese, potom obdržíme ve vlajce CF. Tento způsob je samozřejmě destruktivní. Např. pro zjištění hodnoty 3. bitu (počítáno zprava od 0) provedeme SHR reg, 4.
Tvorba masky
Masku s nastaveným určitým bitem do log. 1 můžeme vytvořit použitím instrukce posuvu nebo rotace doleva na číslo 1. Např. pro vytvoření osmibitové masky s nastaveným 3. bitem (00001000) provedeme např. MOV AL, 1; SHL AL, 3.

Př. Napište v Assembleru funkci (funkci, jejíž tělo tvoří jediný blok Assembleru), která bude mít dva parametry - pozici p a počet n. Funkce bude vracet 32-bitovou masku, ve které bude nastaveno n bitů od pozice p (včetně, počítáno od 0 zprava doleva).

Prohození polovin bitové reprezentace hodnoty
Užitečným použitím rotací je prohození polovin bitové reprezentace hodnoty. Typicky u 32-bitových registrů pro všeobecné použití. Tyto registry se dělí na 16-bitové poloviny, avšak jménem lze přistupovat pouze ke spodní polovině (např. u EAX lze pracovat se spodní polovinou AX). Pokud potřebujeme horní polovinu, můžeme je prohodit pomocí rotace (jedno jestli vpravo nebo vlevo) o 16 pozic, např. ROL EAX, 16.
Celočíselné násobení a dělení mocninou 2

Je to nejdůležitější použití posuvů. Vychází z faktu, že efekt bitového posuvu čísla doleva o jednu pozici je stejný, jako bychom číslo vynásobili dvěma. Naopak bitový posuv čísla doprava o jednu pozici je totéž jako dělení čísla dvěma. Pozor! Toto násobení, resp. dělení je sice velmi rychlé (nejrychlejší možné), ale použitelné jen pro násobení, resp. dělení mocninou 2. Ke zjištění zbytku po celočíselném dělení mocninou 2 použijeme maskování (jak bylo popsáno výše).

Př. Napište v Assembleru funkci, která bude mít čtyři parametry - 32-bitové číslo x, mocnitel n a dva ukazatele na 32-bitová čísla. Funkce uloží na adresy určené ukazately celočíselný podíl a zbytek dělení čísla x n-tou mocninou 2.

Násobení konstantou

Jedná se o velice rychlé násobení čísla libovolnou konstantou (pokud je tato konstanta dostatečně malá, je rychlejší než instrukce MUL). Myšlenka je taková, že konstantu vyjádříme jako součet mocnin 2 (co nejvyšších) a číslo potom násobíme těmito mocninami 2 a tyto mezivýsledky sečteme. Naznačený postup se samozřejmě dá zobecnit na násobení předem neznámým číslem, tj. ne konstantou!
Postup v Assembleru: Číslo posuneme doleva o počet pozic rovnající se pozici log. 1 v binárním vyjádření konstanty (počítáno od 0 zprava doleva). Posuneme ho tolikrát, kolik těch log. 1 je, ale vždy posouváme původní číslo. Mezivýsledky (posuvy čísla) sečteme a máme výsledek.

Příklad vynásobení čísla konstantou 18 = 00010010:

  MOV AX, cislo
  MOV BX, AX
  SHL AX, 1
  SHL BX, 4
  ADD AX, BX
  MOV cislo, AX
    

Myslíte, že takové složité násobení je naprosto zbytečné, když přece máme instrukci MUL? Ale co když ji nemáme! Nemyslíte, že například takto by mohl samotný procesor interně vykonávat instrukci MUL? Jedná se totiž o celkem efektivní algoritmus násobení pomocí sčítání a bitového posuvu.

Otázka: Uvedený postup má jednu nevýhodu, totiž že je obecně potřeba pomocné místo (např. registr) pro mezivýsledek (číslo po vynásobení mocninou 2). Vymyslete ještě efektivnější postup (ne nutně konkrétně v Assembleru) a formulujte jeho myšlenku, který nepotřebuje žádné pomocné místo navíc, tzn. pracuje jen s místem s výchozím číslem a s místem pro výsledek.

„Bitové“ instrukce

Pomocí dosud uvedených instrukcí logických operací nelze (přímo) operovat s jednotlivými bity operandů, pouze s celými operandy (slabikami, slovy nebo dvojslovy). Tento nedostatek se úspěšně řešil maskováním nebo posuvy, ale to zřejmě nebylo dostatečně rychlé (bylo potřeba několik instrukcí) a tak procesory 386 a novější mají instrukční sadu rozšířenou o instrukce pracující s jednotlivými bity operandů. Jsou to instrukce zjištění hodnoty, nastavení a invertování hodnoty bitu. Tyto nové instrukce číslují bity od 0 zprava doleva. Možné kombinace operandů jsou registr - hodnota (pouze 8-bitová), paměť - hodnota (8-bitová), registr - registr a paměť - registr (16-bitové).

Kromě těchto základních instrukcí obsahují procesory 386 a novější ještě instrukce vyhledávání prvního/posledního bitu nastaveného do log. 1, BSF a BSR, což se může hodit např. u násobení pomocí posuvů a sčítání.

Matematický koprocesor

Aritmetické a porovnávací instrukce procesoru pracují pouze nad celými čísly a navíc realizují jen základní operace jako porovnání, sčítání a odčítání, násobení a dělení (plus zbytek). Co když je ale potřeba počítat nad reálnými čísly a složitější operace jako např. odmocninu? Samozřejmě i tento problém lze řešit pomocí procesoru. Výpočty s reálnými čísly se téměř vždy dějí nad určitým intervalem a při volbě vhodné přesnosti (dostatečného počtu reálných čísel z tohoto intervalu) můžeme tento interval zobrazit na interval celých čísel a počítat s nimi, navíc relativně velice rychle. No a složité operace lze také téměř všechny realizovat (aproximovat) pomocí jednoduchých operací, ale v tomto případě jsou bohužel vždy rychlejší hardwarové implementace operací.

Pokud ale celá čísla nestačí (nebo prostě chceme reálná) a složité operace musejí být rychlé (nebo pro ně není algoritmus nebo je prostě chceme hardwarově), nezbývá než využít matematický koprocesor. Je to speciální procesor vedle hlavního procesoru, který pracuje právě nad reálnými čísly a obsahuje instrukce i pro složité operace. U historických procesorů starších 386 je označován číslicí 7 místo 6 (např. 387), u novějších procesorů je běžně přítomen a bere se tedy jako standardní součást procesoru. Jako je pro hlavní procesor zkratka CPU (Central Processing Unit), koprocesor je označován zkatkou FPU (Floating Point Unit).
Matematický koprocesor má svoje vlastní registry a instrukce. Registry uchovávají reálná čísla a označují se ST(i), kde i je číslo od 0 do 7 (ST(0) se označuje zkráceně ST). Je jich tedy relativně málo a jsou stejné, výsadnější postavení má pouze první z nich. Instrukce se jmenují stejně nebo podobně jako u hlavního procesoru, jen všechny začínají písmenem F. Nezbytné jsou instrukce pro přenos dat (FLD, FST). Instrukcemi jsou samozřejmě realizovány všechny základní operace jako u hlavního procesoru - porovnání (FCOM), sčítání a odčítání (FADD, FSUB), násobení a dělení (FMUL, FDIV). Navíc jsou ale např. instrukce pro 2. odmocninu (FSQRT) nebo pro transcendentální funkce jako sinus a cosinus (FSIN, FCOS)!

I když tedy můžeme s reálnými čísly počítat pomocí matematického koprocesoru, je vždy lepší realizovat výpočet nad celými čísly (pokud to jde), protože je to pořád relativně rychlejší. Podrobnější informace o instrukcích matematického koprocesoru jsou k nalezení např. v TECHHELPu.



Jan Outrata
outrataj@phoenix.inf.upol.cz