Druhá série třicátého ročníku KSP

Termín odeslání Vašich řešení této série jest určen na 22. ledna 2018. Řešení můžete odevzdávat jak elektronicky, tak klasickou poštou na známou adresu.

Dotazy ohledně zadání můžete posílat na adresu ksp@mff.cuni.cz, nebo se ptát přímo na diskusním fóru KSP.

Odměna série: Sladkou odměnu si vyslouží každý, kdo vyřeší každou úlohu s lehkou variantou alespoň na polovinu bodů.

Zadání úloh

V budově Matfyzu na Malostranském náměstí je Rotunda jednoznačně největší a nejkrásnější místností. Je to vysoká kruhová dvorana sahající až do dalšího patra, kde skleněné stěny nabízí pohled do prostor školní knihovny. Kdysi se v Rotundě nacházely prostory národní banky, ale ty časy jsou už dávno pryč. Místo toho tu jsou do kruhu seřazené počítače, nalevo unixové, vpravo windowsové, jako kdyby se co nevidět měly pustit do boje. Venku se už dávno setmělo a otevírací doba počítačové laboratoře (neboli labu, jak říká každý správný matfyzák) se chýlí ke konci. Dělal jsem tu dnes celý den službu a teď zbývá lab, beztak už prázdný, zamknout.

Beru si věci, jdu ke dveřím a chystám se zhasnout světla, když vtom uvidím zvláštní věc. U jednoho z unixových počítačů úplně nalevo se rozblikala červená ledka. Co to má znamenat? Ani jsem nevěděl, že nějaký počítač v labu by uměl takhle blikat. Chci přijít blíž, ale pak si všimnu, že se úplně stejně rozblikal jeden z počítačů napravo. A po chvilce další. A další. A zanedlouho blikají červeně všechny počítače, a k tomu všemu navíc úplně synchronně.

Ilustrace: Hroch programuje

„To je ale blbej vtip,“ mumlám si pro sebe, ale spíš chci zakrýt fakt, že jsem se začal trochu bát. Přijdu k jednomu ze strojů a pohnu myší. Displej se rozzáří a já vidím…

* * *

„…a Turingův stroj se vždycky zastaví.“

Pane jo, já jsem zase usnul na přednášce? To by nebylo poprvé, ale nepamatuji si, že by se mi zdálo o sloužení v labu. Semestr už začal, musím ještě řešit resty z toho minulého, a do toho mi ostatní orgové KSP dají za úkol řídit vyšetřování toho, kam vlastně zmizel pan Nápověda. Jsem si jistý, že podobně perné týdny jsem měl minulý rok, ale musel bych se podívat do své databáze, jestli tomu tak opravdu bylo.


Teoretická úloha30-2-1 Zaneprázdněný org (11 bodů)


Náš vypravěč, organizátor KSPčka, si již dlouhou dobu zaznamenává, jak byl pro něj který týden hektický. Databázi těchto záznamů si můžete představit jako posloupnost celých čísel Ai, kde i představuje pořadové číslo týdne od samotného začátku měření. Hodnota každého prvku posloupnosti popisuje orgovu zaneprázdněnost během týdne.

Máte databázi k dispozici a chtěli bychom po vás, abyste uměli rychle odpovídat na dotazy „Kolikrát se vyskytlo v týdnech od x do y hodnocení H?“, neboli „Kolikrát se v podposloupnosti AxAy vyskytuje číslo H?“. Dotazů může být mnoho, a proto se může hodit si na začátku předpočítat nějaká data a ta poté použít při odpovídání na dotazy. V takovém případě nás zajímají časové a paměťové složitosti jak předpočítání, tak jednoho dotazu.

Příklad vstupu: Posloupnost Ai je 1, 2, 2, 3, 2, 2, 3, 3. Předpokládáme, že indexujeme od jedničky. Na dotaz „kolikrát se od druhého do pátého týdne vyskytla zaneprázdněnost 2“ je odpověď 3, na dotaz „kolikrát se od pátého týdne do osmého týdne vyskytla zaneprázdněnost 3“ je odpověď 2.

Lehčí variantaLehčí varianta (za 6 bodů): Řešte pro jednodušší dotazy „Vyskytuje se v týdnech od x do y hodnocení H?“.

Řešení

Skupince orgo-agentů s Jirkou v čele se podařilo zatopit Nápovědův bunkr, ale samotný Nápověda se někam vypařil a došla nám jen záhadná esemeska, že se přesouvá do Tokia. Moc jsme ale nevěřili tomu, že by nám ten padouch jen tak vyzradil nějaké pravdivé informace. Navíc, se začátkem semestru se popravdě nikomu do Japonska odjíždět nechtělo. Po vysušení podzemních prostor se nám ale do ruky dostala některá zařízení, která on a jeho otroci vyvíjeli.

Na začátku jsme se ale nedostali k ničemu zajímavému. Nápověda pro jeden z experimentů vyžadoval mnoho náhodných čísel, ale asi byl hodně paranoidní a nevěřil tradičním generátorům. Proto si vyráběl vlastní, hardwarové generátory. Několik jsme jich zkoumali, ale všechny dělaly téměř to samé.


Teoretická úloha30-2-2 Hardwarový generátor (13 bodů)


Máme seznam M prvků a chceme z něj náhodně vybírat prvky. U každého prvku máme uvedené, s jakou pravděpodobností má být vybrán (pravděpodobnosti všech prvků se správně posčítají na 1).

Protože nevěříme tradičním generátorům čísel, používáme náš vlastní, hardwarový generátor. Ten však umí jedinou věc: rovnoměrně generovat nějaké číslo z uzavřeného intervalu [0,1].

Rovnoměrností myslíme, že všechna čísla mají stejnou šanci být vygenerována. (Kdybychom to chtěli definovat pořádně, nestačilo by říci, že mají stejnou pravděpodobnost, protože ta je pro každé z nekonečně mnoha čísel nulová. Mohli bychom třeba říci, že pro každý podinterval délky p platí, že se do něj strefíme s pravděpodobností přesně p.)

Určete, jak budete opakovaně náhodně vybírat prvky množiny se zadanými pravděpodobnostmi jen s pomocí našeho generátoru. Předpokládejte, že umíte přesně počítat s reálnými čísly, nevznikají zaokrouhlovací chyby a generování jednoho náhodného čísla probíhá v konstantním čase. Optimalizujte nejprve na čas strávený při generování jednoho prvku, následně pak na čas předvýpočtu před prvním generováním.

Známe řešení, které tráví na předvýpočtu O(M) a následná generování zvládne v konstantním čase. Vymyslíte nějaké stejně rychlé? Pokud ne, určitě pošlete i to pomalejší, také za něj dostanete body.

Příklad vstupu: Máte prvky A, B a C. A chcete generovat s pravděpodobností 1/6, B s pravděpodobností 1/3 a C s pravděpodobností 1/2.

Řešení

Naštěstí jsme měli k dispozici i disky se softwarem. To bylo o něco zajímavější. Hned po přednášce jsem na chodbě potkal Janku, jak zkoumá zdrojové kódy něčeho, co vypadalo jako počítačový virus. Po zátahu v Hostivaři Janka zjistila, že hackování počítačů ji vlastně baví, a začala téma studovat více do hloubky. Aby zdůraznila svoji novou zálibu, začala všude nosit černé brýle a chodit spát ještě později než většina organizátorů.

„To je dosť zaujímavý kúsok, tento vírus,“ vysvětlila mi. „nikto na internete ho nepozná, ale veľmi dobre sa šíri po sieti.“ „Jak to můžeš vědět?“ ptám se. Ukázalo se, že Janka si stáhla simulátor počítačové sítě, v podstatě pár propojených virtuálních strojů, a zkoušela virus pustit v něm. Pokud se virus spustil na správném počítači, byl skutečně schopný celou síť zamořit.


Teoretická úloha30-2-3 Šíření viru podruhé (10 bodů)


Máte síť N navzájem propojených počítačů. V této síti zkoumáte chování viru, jehož úkolem je nakazit co nejvíce počítačů. Virus se ale nešíří úplně přímočaře. Máme seznam dvojic počítačů a u každé dvojice (A,B) platí, že se virus může přímo přenést z počítače A do počítače B (obráceně to být nemusí – pokud ano, tak se v seznamu vyskytuje další dvojice (B,A)).

Předpokládáme, že na začátku útoku je virus jen v jednom počítači v síti, odtud se rozšíří na všechny počítače, které dokáže přímo nakazit, následně se z těchto počítačů opět přenese dál, jak může… a takhle pokračuje, dokud je šíření možné. Pro každý počítač P chceme najít počet strojů, které se nakazí, pokud bude P nakažen na začátku útoku. Můžete se podívat na obrázek s příkladem. Kroužky odpovídají počítačům, dvojice počítačů, kde první může nakazit druhý, jsou propojeny a číslo u počítače P odpovídá počtu napadených počítačů, pokud je virus na začátku v P.

Graf s cestou a cyklem

Lehčí variantaLehčí varianta (za 6 bodů): Řešte stejnou úlohu za předpokladu, že v seznamu přenosů není cyklus.

Řešení

Displej ukazuje šíření viru a já si mimoděk vzpomenu na ten divný sen na přednášce. To s těmi blikajícími počítači se nestalo, ale když to vidím, tak se nemůžu nezeptat: „Janka, určitě se ten program z těch virtuálních mašin nemůže dostat ven?“

„Nie,“ odpověděla. Ale nic dalšího neřekla, zavřela notebook a odešla. Jaký výraz měla ve tváři, to jsem kvůli těm černým brýlím nemohl odhadnout.

Zbytek dne jsem strávil podivně zmatený. Hlavou se mi honily podivné myšlenky. Vybavovaly se mi osoby, se kterými jsem se neznal, a místa, na kterých jsem určitě nikdy nebyl. Je vážně na čase se pořádně vyspat, řekl jsem si, navíc zítra máme kvůli vyšetřování nějakou zajímavou návštěvu.

Ilustrace: Hroch snívá
* * *

Místnost S322 je základnou každého KSPáka, takže v devět hodin ráno (ano, ráno) tam jen tak na někoho nenarazíte. Když už ano, tak dotyčný vypadá, jako by právě vylezl z postele, a k dokonalému dojmu chybí jen pyžamo. Dnes jsme tu ale hostili partičku mediků, a to kvůli jednomu ze souborů v Nápovědově počítači, který podezřele připomínal sekvenci DNA. Než jsme se k souboru prokousali, museli jsme pochopit některé netradiční komprimační metody, které Nápověda používal.


Praktická opendata úloha30-2-4 Komprimace (10 bodů)


Vaším úkolem je rozbalit zkomprimovaná data. K jejich komprimaci došlo následujícím způsobem:

Data zapsaná jako posloupnost bitů se rozdělila na posloupnost různě dlouhých bloků. Každý blok je pak ve zkomprimovaném souboru reprezentován jedním ze dvou způsobů. První způsob je jednoduchý, data bloku jsou zapsána přímo tak, jak byla v původním souboru. Druhý způsob umožňuje zkrátit zápis opakujících se kusů DNA. Místo samotných dat je zde pouze reference (odkaz) na kus původních dat, která jsou s daným blokem shodná. Reference na pozici i tedy znamená, že nultý bit bloku je shodný s i-tým bitem původních dat, první bit bloku s (i+1)-ním bitem atd., až do vyplnění celého bloku.

Toto je praktická open-data úloha. V odevzdávacím systému si necháte vygenerovat vstupy a odevzdáte příslušné výstupy. Záleží jen na vás, jak výstupy vyrobíte.

Formát vstupu: Na prvním řádku dostanete dvě mezerou oddělená čísla NM. Číslo N značí počet bitů původního souboru a M počet bloků. Následuje M řádků, každý reprezentuje jeden blok. Každý z těchto řádků obsahuje 3 údaje oddělené mezerou. První je typ zápisu dat (D = data, nebo R = reference), druhý je velikost bloku. Třetí údaj je buď posloupnost bitů (tedy posloupnost nul a jedniček), pokud blok obsahuje přímo data, nebo odkaz na začátek úseku, jehož obsah je shodný s daty bloku (adresy bitů číslujeme od nuly). Bloky na vstupu jsou přesně v pořadí, jak jdou za sebou v původním souboru.

Formát výstupu: Na výstup vypište původní dekomprimovaný soubor, tedy posloupnost nul a jedniček, pokud je určena jednoznačně. Mohla se nicméně někde stát chyba a původní data nemusí jít ze zkomprimovaných informací určit jednoznačně, posloupnost původních bitů tedy nejde zrekonstruovat. V takovém případě vypište NEJDE.

Ukázkový vstup:
7 5
R 1 4
D 2 11
R 2 5
R 1 2
D 1 0
Ukázkový výstup:
0111010
Ukázkový vstup:
4 3
R 1 3
D 2 10
R 1 0
Ukázkový výstup:
NEJDE
Ukázkový vstup:
10 2
D 2 01
R 8 0
Ukázkový výstup:
0101010101
Obárzek k prvnímu vstupu
Na obrázku vidíte zobrazený první vstup. Původní soubor měl 7 bitů. Máme pět bloků: pozici 0, pozice 1–2, pozice 3–4, pozici 5, pozici 6. Každý řádek reprezentuje zápis jednoho bloku.

Řešení

„Víte, měli jsme takový projekt. Nedotáhli jsme ho do konce, ale nevylučujeme, že by se to Nápovědovi mohlo povést,“ řekl jeden z mediků. „Týkalo se to přenosu lidského vědomí. Zjistili jsme, že určitou hypnotizační technikou je možné přehrát vědomí do mozku jiného člověka.“

„Jedná se v podstatě o silnou reakci organismu na jeden vizuální vjem,“ vysvětluje druhý. „Pak je jinými vjemy docela snadné přeprogramovat velkou část buněk v mozku.“

Zamumlám: „To zní, jako kdyby ten člověk byl posedlý.“ Spánek mi nějak nepomohl, divně se mi točí hlava, asi bych potřeboval nějakou odbornou pomoc…

* * *

… nějaké dobré doktory prý mají v Tokiu.

* * *

Jeden z organizátorů beze slova vstal a odběhl z místnosti.

„Nevíte, co se s tím Kubou děje?“ ptá se Filip. „Chová se od včerejška dost divně.“ Ostatní jenom pokrčili rameny. Ještě chvíli se s mediky bavili o tom, jak by bylo možné transplantaci vědomí využít, a jak by ji asi využil Nápověda. Pak už ale přišel čas se rozloučit. Orgové zamkli S322 a doprovázeli návštěvu k východu, když se zvenku ozvalo hlasité PRÁSK!

„Co to sakra je?“ Filip otevřel vchodové dveře a zděšeně uskočil. Prohnalo se kolem něj rameno nějakého velkého stroje. „To je ten nový vysavač odpadků! Jak se tady tahle věc vzala?“ Dostal rychle odpověď. Kolem dveří projela kabina stroje, ve které seděl Kuba. Ale podle jeho výrazu ve tváři nebylo jasné, jestli je to skutečně on.


Teoretická úloha30-2-5 Autovysavač (12 bodů)


Kuchařková úloha

Kuba, respektive pomocník pana Nápovědy v Kubově těle, si „vypůjčil“ nový stroj Pražských služeb: obří vysavač na odpadky. Právě ho přivezl ho na parkoviště na Malostranském náměstí, a protože si chce vytvořit volný prostor, chce s ním nasát nějaká z aut, která tu parkují. Protože je pomocník škodolibý, chtěl by nasátím zničit co nejdražší auta, konkrétně takovou skupinu aut, aby medián jejich cen byl co nejvyšší.

Jak definujeme medián pro množinu čísel? Provedeme to jen pro případ, že je v množině lichý počet prvků. Pokud seřadíme čísla v množině podle velikosti, je medián tím číslem, které se nachází uprostřed seznamu, Platí tedy, že 50 % ostatních čísel je menší nebo rovno mediánu a 50 % je vyšší nebo rovno mediánu.

Parkoviště reprezentujeme jako čtverečkovou síť o rozměrech M×N. V každém poli je uvedena hodnota zde stojícího auta. Vysavač dokáže vysát nějakou obdélníkovou oblast o rozměrech P×Q polí. Zjistěte, která oblast této velikosti má nejvyšší medián cen – máte jistotu, že P·Q je vždy liché.

Mějme například parkoviště velikosti 4 ×4 s následujícími hodnotami cen aut, přičemž dokážeme vysát oblast 3 ×3:

200 30 10 40
20  10 50 40
60  60 10 40
10  10 10 20

Ačkoliv je nejdražší auto v levém horním rohu, medián této oblasti velikosti 3×3 je jen 30. Lepší je pravá horní oblast 3×3, která má medián 40.

Lehčí variantaLehčí varianta (za 6 bodů): Řešte za předpokladu, že N = 1.

Řešení

Filip se s ostatními opatrně podíval ven. Kuba neustále popojížděl po parkovišti, hýbal ramenem vysavače na všechny strany a zdálo se, že pořád není spokojený s výběrem vozidel. Najednou se ale ozvalo zaburácení motoru a do prostoru parkoviště vjelo auto. Byl to Jirka a jeho Volkswagen! Než se kdokoliv stačil vzpamatovat, udělal Jirka pár obratů na ruční brzdě a naštrádoval si to přímo pod rameno vysavače.

„Ne! Tam nejezdi!“ vykřikl Filip. Všiml si, že se Kuba v kabině stroje zachechtal a sáhl po velké páce, která jistě zapínala vysávání. Než jí ale stačil pohnout, světla v kabině zhasla a motor stroje se zastavil.

„Zatracená kraksna!“ ozvalo se nadávání. Zdálo se, že Kubovi se také zablokovaly dveře, protože bylo slyšet, jak s nimi lomcuje. Jirka vylezl ven ze svého auta, jakoby nic. „Vy jste si mysleli, že nevím, co dělám?“ ušklíbl se na ostatní. „Věděl jsem, že tahle mašina je naprogramovaná hrozně mizerně. Ta řídící jednotka nedokáže odhadovat ceny podobně vytuněných aut, jako toho mého. Když naskenuje všechna moje vylepšení, tak se prostě uvaří a pošle do kopru celý vysavač.“

Z budovy vyběhla Janka. „Už som na to prišla! Virus je v labu a vykonával tie transplantácie, o ktorých ste sa bavili. Jaj,“ řekla zkroušeně, když viděla pohromu na parkovišti.

„Vsadím se, že Kuba, ehm, ten šílenec, který teď ovládá Kubovo tělo, tu nezůstával jenom kvůli tomu, že by chtěl jen tak ničit auta,“ řekl Filip. „Dávalo by smysl, aby utekl do Tokia za Nápovědou. No jasně,“ došlo mu, „nechtěl on jenom odkrýt vstup do parlamentního metra? Už jsme jeho síť zmapovali, dostal by se jím minimálně na letiště.“


Teoretická úloha30-2-6 Parlamentní metro (12 bodů)


Za časů studené války vznikla v Praze tajná podzemní dráha s jediným účelem – evakuovat politiky a státníky v případě hrozící jaderné apokalypsy. Metro má mnoho stanic, které jsou propojeny tunely. Do jedné stanice může ústit libovolný počet tunelů, a ať už vlak přijede z jakéhokoliv směru, může se vydat dál jakýmkoliv tunelem.

Z bezpečnostních důvodů není metro napojeno na elektrickou síť, místo toho je každý vlak poháněn svou vlastní baterií. Aby měly vlaky co nejmenší tření, je z tunelů vypuštěn vzduch a vlaky se pohybují jen po magnetické kolejnici. Znamená to, že energii musí vlak vyvinout jen v momentě, když vyjíždí ze stanice, a to tím větší energii, čím větší rychlost chce vyvinout. Na délce úseku mezi stanicemi spotřeba vůbec nezávisí. Vlak ale musí (z bezpečnostních důvodů) zastavit v každé stanici na cestě.

Formálněji, vede-li mezi dvěma stanicemi tunel s délkou  s a vlak jím projede rychlostí v, urazí celou trasu za s/v jednotek času a bude ho to stát v jednotek energie (čas na rozjíždění a brždění zanedbáváme).

Pro danou počáteční a cílovou stanici a zadané nabití baterie rozhodněte, jak se co nejrychleji dostat ze startu do cíle, abychom si přitom vystačili pouze s energií z baterie. Kromě seznamu stanic v pořadi, v jakém je navštívíme, nás zajímají i rychlosti, kterými budeme projíždět tunely mezi nimi.

Příklad: Pro následující rozložení stanic, počáteční stanici A, koncovou stanici C a baterii s kapacitou 6 jednotek je optimálním řešením jet přes stanici B, a to následovně: Na úseku AB pojedeme rychlostí 4, na úseku BC rychlostí 2, dohromady tedy využijeme celou baterii. Celkový čas cesty bude 4/4 + 1/2 = 3/2 jednotek. Kdybychom místo toho jeli přes D, nestihli bychom se do cíle dostat dříve než za dvě časové jednotky.

Mapa s ukázkovým příkladem

Lehčí variantaLehčí varianta (za 7 bodů): Všechny stanice tvoří jednu souvislou trasu – z koncových stanic vede jediný tunel, ze všech ostatních právě dva. Najděte nejrychlejší způsob jak se přepravit mezi koncovými stanicemi.

Řešení

Po tom bláznivém večeru byl Kuba zatčen, ale naštěstí nebylo těžké ukázat, že je „posedlý“ a že do něj bylo transplantováno vědomí jednoho z pomocníků pana Nápovědy. Naštěstí se schopným medikům podařilo najít způsob, kterým posednutí zvrátit. O měsíc později se před místností S322 konala oslava na počest navrátivšího se Kuby. Sice se s obavami tvářil na jakýkoliv displej okolo sebe, ale byl pevně rozhodnutý pokračovat ve studiu.

„Kdy jen toho padoucha dostaneme,“ povzdechl si. „Neboj,“ uklidnil ho Jirka. „Už jsme prošli všechen jeho software. Žádné další viry tu být nemůžou, na všechny počítače jsme nainstalovali ochranný software.“ „Tak teda jo, budu ti věřit,“ usmál se Kuba.

* * *

Byla to pořádná párty. Poslední skupinka orgů odešla až nad ránem. Ten úplně poslední org zhasl světlo, takže v chodbě zůstalo šero, přerušované jen nesmělými paprsky ranního slunce.

Náhle šero přeruší bliknutí. A další. A zase další. Co to? V rohu chodby stojí velký kopírovací stroj. A ta červená ledka, co na něm bliká, by určitě blikat neměla…

Pak zhasne. Nevadí. Ono na ni ještě dojde.

Kuba Maroušek (snad)


Seriálová úloha30-2-7 Paměť očima assembleru (15 bodů)


Asi vás při čtení minulého dílu napadlo, že pro spoustu úloh si s třinácti 32-bitovými registry nevystačíme. Pojďme se naučit pracovat s pamětí – té máme obvykle k dispozici řádově gigabajty.

Co je paměť vlastně zač?

V běžných programovacích jazycích většinou přistupujeme k paměti prostřednictvím proměnných, polí, objektů atp. Ale to vše jsou jen abstrakce poskytované naším překladačem či interpretem.

Procesor

Z pohledu procesoru je paměť prostě dlouhá řada okének, každé z kterýchž si pamatuje jeden bajt, tedy číslo od 0 do 255. Těmto okénkům se občas říká paměťové buňky. Každé okénko je jednoznačně určené svým pořadovým číslem, kterému říkáme adresa.

Pro začátek řekněme, že adresy mají rozsah od 0 do N-1, kde N je velikost paměti v bajtech. Časem se ukáže, že situace je o malinko složitější.

Přístup k paměti

ARM patří mezi takzvané load/store architektury. To znamená, že většina instrukcí neumí přímo pracovat s pamětí, pouze s registry. Namísto toho existují speciální instrukce sloužící k přenosu dat z paměti do registrů (kde s nimi pak můžeme provádět nějaké výpočty) a z registrů do paměti.

Začneme tím nejjednodušším: čtením a zápisem jednoho bajtu. K tomu slouží instrukce:

  • LDRB cílový-registr, zdrojová-adresa (LoaD Register Byte) pro čtení z paměti do registru,
  • STRB zdrojový-registr, cílová-adresa (STore Register Byte) pro zápis z registru do paměti.

Registr se zapisuje, jak jste zvyklí, např. r3. Adresu lze zapsat vícero způsoby, ale překvapivě ne jako číselnou konstantu. Asi nejjednodušší zápis je [registr], který použije jako adresu obsah nějakého registru.

Takže například instrukce LDRB r1, [r5] načte do registru r1 bajt z adresy uložené v registru r5. Obdobně následující posloupnost instrukcí zapíše bajt s hodnotou 42 na adresu 0x10000:

MOV  r0, #42
MOV  r1, #0x10000
STRB r0, [r1]

Pozor je třeba dát na to, že přístup k paměti je výrazně pomalejší než práce s registry – zhruba 3×100×. Proč tak velké rozpětí by bylo na delší povídání – souvisí to s takzvanou cache procesoru, o které možná bude řeč v některém z dalších dílů. Zjednodušeně lze říct, že opakovaný přístup k částem paměti, ke kterým jste přistupovali nedávno, bude rychlejší.

Každopádně se vyplatí hodnoty, se kterými provádíte spoustu výpočtů za sebou, držet v registrech a do paměti uložit třeba až na samém konci nějaké série výpočtů, kdy potřebujete registr uvolnit pro jiné účely.

Paměťová reprezentace čísel

Když registry i aritmetické operace pracují s 32-bitovými čísly, hodilo by se nám tato čísla ukládat do paměti. Do jednoho bajtu se vejde 8 bitů, takže k uložení jednoho čísla potřebujeme 4 bajty. Uvažujme například číslo 0x1234567. To můžeme rozdělit na bajty následovně:

Rozdělení 32-bitového čísla na bajty

Existují dva běžné způsoby, jak takové číslo do paměti uložit, které se liší pořadím těchto bajtů v paměti. Big endian znamená uložení bajtů v pořadí od nejvýznamnějšího po nejméně významný, jak by je asi přirozeně zapsal člověk. little endian znamená pořadí přesně opačné, tedy naše číslo by bylo zapsané sekvencí bajtů 0x67 0x45 0x23 0x01. Na ARMu se obvykle používá právě little endian (stejně jako na intelských procesorech).

A přestože by se dalo číslo uložit a načíst vhodnou kombinací STRB/LDRB a aritmetických instrukcí, je to natolik běžná operace, že pro ni ARM nabízí speciální instrukce: STR registr, adresa a LDR registr, adresa. Význam parametrů je stejný jako u bajtových verzí, k uložení čísla se použijí paměťové buňky adresaadresa + 3.

Pokud se v r0 nachází číslo 0x1234567 a provedeme instrukce:

MOV r1, #0x10000
STR r0, [r1]
bude výsledek vypadat následovně:
Průběh 32-bitové instrukce STR

Je dobrým zvykem ukládat čísla na adresy, které jsou násobkem velikosti daného typu – v tomto případě násobkem čtyř.

Existují i další varianty load/store instrukcí. Kompletní přehled ukazuje následující tabulka:

bitů znaménkovost min. max.
LDRB 8 bezznaménkově 0 255
LDRSB 8 znaménkově -128 127
STRB 8 nezáleží dle znaménkovosti
LDRH 16 bezznaménkově 0 65535
LDRSH 16 znaménkově -32 768 32 767
STRH 16 nezáleží dle znaménkovosti
LDR 32 nezáleží dle znaménkovosti
STR 32 nezáleží dle znaménkovosti

Úkol 1 [1b]

Vysvětlete, proč zatímco LDRB a LDRH mají znaménkovou a bezznaménkovou variantu, LDR a všechny store instrukce jsou společné pro znaménková i bezznaménková čísla.

Proměnné a paměťové reprezentace

Ve vyšších programovacích jazycích jsme zvyklí pracovat i s jinými typy, než jen čísly. Ukážeme si, jak různé typy reprezentovat v paměti. Paměťovou reprezentací nějakého typu rozumíme schéma určující, jak převést libovolnou hodnotu daného typu na posloupnost bajtů v paměti (a zpět). Reprezentace obvykle mají pevnou velikost, abychom si pro ně mohli vyhradit místo v paměti.

Proměnná pak je prostě vyhrazený úsek paměti obsahující hodnotu proměnné uloženou dle paměťové reprezentace dané typem proměnné.

  • Celá čísla jsou reprezentována 1 až 4 bajty, jak bylo popsáno výše, dle potřebného rozsahu. Velikost reprezentace je neměnná: udává maximální číslo, které daná proměnná může uchovat. Ale pokud do 32-bitové proměnné uložíte třeba jedničku, stále bude zabírat v paměti 4 bajty.
  • Desetinná čísla se ukládají v takzvaném formátu s plovoucí čárkou (floating-point, IEEE 754). Čísla se ukládají ve tvaru m·2e, kde čísla m (tzv. mantisa) a e (exponent) jsou uložena zvlášť a každému je vyhrazen nějaký počet bitů. Díky tomuto zápisu mají floatové typy obrovský rozsah, ale omezenou přesnost.

    Existuje několik variant, které se liší velikostí, nejčastěji potkáte takzvanou double precision (typ double v Céčku), která zabírá 64 bitů, z toho 53 bitů tvoří mantisa a 11 exponent. Díky tomu umožňuje reprezentovat čísla v rozsahu řádově od -21000 do 21000, ale pamatuje si jen 53 nejvýznamnějších dvojkových číslic. Práce s desetinnými čísly je na ARMu trochu komplikovanější a v seriálu se jí věnovat nebudeme.

  • Pole vytvoříme tak, že prostě uložíme spoustu reprezentací daného typu těsně za sebe. Například pole 32-bitových celých čísel o  prvcích bude souvislý úsek paměti délky 4ℓ. Tady je důležité, že reprezentace jednotlivých prvků mají pevnou velikost. Díky tomu dokážeme spočítat v konstantním čase vzdálenost libovolného prvku od začátku pole (tzv. offset, a tedy i jeho adresu. V našem příkladu má i-tý prvek offset 4i a nachází se na adrese A+4i, kde A je adresa začátku pole. ARM navíc nabízí užitečné zkratky pro přístup k prvkům pole, které si ukážeme níže.
    Paměťová reprezentace pole

    Abychom si pro ně mohli vyhradit místo v paměti, musí mít pole pevnou nejen velikost jednoho prvku, nýbrž i počet prvků.

    Občas by se nám ale hodilo mít pole s proměnlivým počtem prvků. Pokud známe nějakou rozumnou horní hranici na to, kolik prvků v poli nejvýše bude, můžeme si v paměti vyhradit prostor odpovídající tomuto limitu (obvykle nazývanému kapacita pole) a pak z něj využít jen aktuálně potřebnou část. To typicky znamená, že si někam vedle uložíme počet prvků, které jsou v poli opravdu uložené (k), potom prvních k prvků pole obsahuje smysluplné hodnoty a zbytek ignorujeme.

    Alternativně můžeme konec obsazené části pole poznat tak, že si za něj přidáme nějakou speciální značku, která se v normálních datech nevyskytuje, například 0 nebo -1.

  • Řetězce můžeme chápat prostě jako pole znaků, ať už „znak“ znamená cokoli. Pokud se obejdeme bez diakritiky, můžeme znaky reprezentovat ASCII kódy a každý znak zabere jeden bajt. Povídání o tom, jak zacházet s unicodovými řetězci, by vydalo na samostatný seriál.

    U řetězců opět narážíme na to, že mohou mít proměnlivou délku. Zde je nejčastějším řešením (vycházejícím z Céčkové tradice) ukončit platnou část řetězce nulovým bajtem. Například řetězec obsahující slovo BAGR by mohl vypadat následovně:

    paměťová reprezentace řetězce
  • Struktury a objekty reprezentujeme podobně jako pole, tedy prostě nasázíme do paměti jednou položku za druhou, jen tentokrát může mít každá jiný typ a velikost. Ale protože seznam položek, jejich typy i pořadí jsou pevné a dopředu známé, můžeme opět snadno spočítat offset každé položky od začátku struktury. Ten je neměnný, můžeme si ho klidně spočítat s tužkou a papírem a napsat do programu jako konstantu (v kompilovaných jazycích tohle udělá překladač).

    Ještě je třeba dát pozor na jednu věc. V minulém dílu jsme zmiňovali, že je vhodné, aby číselné typy byly v paměti zarovnané na násobek své velikosti (některé verze ARMu to dokonce vyžadují). Proto se občas mezi prvky struktury nechává vhodně velká mezera (padding), aby následující prvek byl správně zarovnaný. Příklad struktury, která obsahuje 32bitové, 16bitové a znovu 32bitové číslo (v Céčku bychom zapsali jako struct s { int a; short b; int c; }):

    paměťová reprezentace struktury s paddingem
    V takovéto struktuře víme, že 3. položka bude mít vždy offest 8, takže pro přístup k ní stačí přičíst 8 k adrese začátku struktury. Aby nám zarovnání správně vyšlo, je třeba i začátek struktury mít zarovnaný na násobek čtyř.
  • Ukazatele či reference (např. vzájemné odkazy mezi prvky spojového seznamu či vrcholy binárního stromu) ukládáme prostě jako 32-bitové číslo obsahující adresu cíle (kterým je často nějaká struktura). Níže si ukážeme příklad reprezentace spojového seznamu. Null pointer („ukazatel nikam“) reprezentujeme číslem 0. To znamená, že na adresu 0 bychom neměli nic ukládat, protože ukazatel na takovou věc by nešlo rozpoznat od null pointeru. Ale to vám operační systém ani nedovolí.

Pole a režimy adresování

Pojďme se nyní pozorněji podívat na to, jakými způsoby se dá zapsat adresa v load/store instrukcích. Tři základní varianty jsou následující:

  • [registr], např. [r4] – adresou je obsah registru. Tento zápis už jsme potkali výše.
  • [registr,#konstanta], např. [r2,#10] – jako adresa se použije součet obsahu registru a číselné konstanty (může být i záporná).
  • [registr,registr], např. [r2,r7] – jako adresa se použije součet hodnot obou registrů.
  • [registr,registr,LSL #posun] – k hodnotě prvního registru se přičte hodnota druhého registru posunutá o daný počet bitů doleva, tedy vynásobená 2posun. Například [r3,r2,LSL #3] odpovídá adrese r3 + 23·r2= r3 + 8·r2.

Varianta s konstantním posunem se hodí, když máme spoustu číselných proměnných. Pokud je máme v paměti blízko sebe, nemusíme si před každým čtením nějaké proměnné připravit do registru její přesnou adresu. Místo toho si v jednom registru budeme udžovat začátek celé oblasti a k jednotlivým proměnným přistupovat pomocí tohoto registru a různých offsetů. Například pokud máme číselné proměnné na adresách 0x10000, 0x10004, 0x10008, …, uložíme si např. 0x10000 do r10 a pak k jednotlivým proměnným přistupujeme instrukcemi typu LDR r1, [r10, #8].

Taktéž se hodí pro přístup k položkám struktur: pokud máme v r0 adresu struktury a chceme přistoupit k její položce s offsetem 10, můžeme použít LDR r5, [r0, #10].

Dvouregistrové adresování, zvlášť ve verzi s bitovým posunem, se naopak hodí pro práci s poli. Například máme-li v r0 adresu pole a v r1 index, můžeme příslušný prvek přečíst pomocí LDR r5, [r0, r1, LSL #2].

Následující kód projde pole 32-bitových čísel bez znaménka začínající na adrese 0x10000 o 1024 prvcích a vypíše index maximálního prvku:

MOV r0, #0x10000
MOV r1, #0  // index při procházení
MOV r2, #0  // prozatímní maximum
MOV r3, #-1 // index prozatímního maxima
smycka:
  LDR r4, [r0, r1, LSL #2] // načte prvek pole
                           // z adresy r0 + 4*r1
  CMP r4, r2
  BLO neni_vetsi
    // pokud je větší nebo rovno...
    MOV r2, r4 // ...nahradíme maximum...
    MOV r3, r1 // ...a uložíme jeho index
  neni_vetsi:
  ADD r1, #1
CMP r1, #1024
BLO smycka
// na konci je v r3 index maxima

Z toho, jak fungují pole, vidíme, proč musí mít pevnou velikost. Poté, co si pro pole najdeme nějaké místo v paměti, můžeme přidávat prvky maximálně tak dlouho, dokud konec pole nenarazí na začátek něčeho jiného, co je v paměti uložené o kus dál.

ARM má ještě nabízí ještě další nezvyklé adresovací módy, které usnadňují procházení polí:

  • [registr,offset]! – použije registr+offset jako adresu pro load/store instrukci a na konci ji zapíše zpátky do registru.
  • [registr],offset – použije hodnotu registru jako adresu pro load/store instrukci a po jejím provedení do něj zapíše hodnotu registr+offset.

Offset může opět být konstanta, další registr nebo registr s posunem. Tyto instrukce se chovají trochu podobně jako Céčkový prefixový a postfixový operátor ++. Použitím těchto adresovacích módů můžeme jednou instrukcí přečíst prvek z pole a skočit na další, ušetříme si tak jednu instrukci ADD.

Ukážeme si to na příkladu kódu, který sečte všechny prvky pole (opět od 0x10000 délky 1024):

MOV r0, #0x10000
MOV r2, #0
smycka:
  LDR r1, [r0], #4
  ADD r2, r1
CMP r0, 0x10400 // pokud je r0 před koncem pole
BLO smycka

Úkol 2 [3b]

V paměti na adrese 0x10004 máte pole 32-bitových celých čísel a na adrese 0x10000 32-bitové celé číslo udávající jeho délku. Vaším úkolem je toto pole obrátit pozpátku na místě (aby se na místě prvního prvku ocitl poslední, …, až na místě posledního první). Ideálně byste se měli obejít bez další pomocné paměti.

Úkol 3 [3b]

V registru r0 dostanete číslo N. Napište program, který vynuluje souvislý blok N bajtů v paměti začínající od adresy 0x10000. Program smí celkem provést nejvýše 0.3·N instrukcí (plus konstanta nezávislá na N).

Úkol 4 [5b]

V paměti na adrese 0x10004 máte pole 32-bitových celých čísel se znaménkem a na adrese 0x10000 32-bitové celé číslo udávající jeho délku (můžete předpokládat, že je to mocnina dvojky). Vaším úkolem je toto pole setřídit. Pro plný počet bodů implementujte nějaký efektivní třídící algoritmus (s lepší než kvadratickou složitostí).

Můžeme doporučit např. nerekurzivní (bottom-up) variantu MergeSortu popsanou v naší kuchařce, případně vhodně implementovaný RadixSort. Máte k dispozici pomocnou paměť velkou jako původní pole (plus nějaká konstanta) od adresy 0x8000000. Výsledné setříděné pole můžete uložit buď místo původního, nebo do této pomocné oblasti.

Příklad: spojové seznamy

Uložení spojového seznamu
v paměti Podíváme se na příklad trochu složtější datové struktury, totiž (jednosměrného) spojového seznamu. Ten ve vyšších programovacích jazycích obvykle reprezentujeme jako spoustu struktur (objektů) provázaných ukazateli.

S tím, co jsme si ukázali výše, už víme, jak tyto struktury reprezentovat. Např. bude-li náš seznam obsahovat 32-bitová čísla, bude každý jeho prvek reprezentován souvislým osmibajtovým úsekem paměti. První čtyři bajty budou obsahovat hodnotu prvku, druhé čtyři bajty adresu následujícího prvku.

Poslední prvek má místo adresy následovníka uložen null pointer, tedy nulu.

Na obrázku vpravo je příklad spojového seznamu obsahujícího dvě čísla, 0xAABBCCDD a 0x11223344. Připomínáme, že prvky seznamu mohou být v paměti rozmístěné naprosto libovolně: s mezerami, pozpátku, napřeskáčku, etc.

Následujcí kód projde seznam a spočítá jeho délku. První prvek seznamu je uložen na adrese 0x10000.

MOV r0, #0x10000
MOV r1, #0
smycka:
ADD r1, #1
LDR r0, [r0, 4] // na pozici r0+4 je ukazatel
                // na následující prvek
CMP r0, 0
BNE smycka
// v r1 je délka seznamu

Úkol 5 [3b]

V paměti na adrese 0x10000 máte uložený ukazatel na první prvek spojového seznamu (pozor, nikoli přímo první prvek). Seznam obsahuje 32-bitová celá čísla setříděná ve vzestupném pořadí. V registru r0 dostanete adresu nového prvku – kompletní struktury včetně zatím nevyplněného odkazu na následníka. Vaším úkolem je připojit nový prvek na správné místo do původního seznamu, aby zůstal setříděný a aby na adrese 0x10000 stále byl ukazatel na jeho začátek.

Příklad: vyhledávání v telefonním seznamu

Máme v paměti pole struktur popisující něco jako telefonní seznam. Každá položka obsahuje dva řetězce: jméno (osmibajtový buffer pro řetězec proměnlivé délky ukončený nulovým bajtem) a číslo (4-znakový řetězec pevné délky bez ukončení). Jednoduchý seznam o dvou položkách by mohl v paměti vypadat takto:

Struktura telefonního seznamu v paměti

Na obrázku pro přehlednost ukazujeme znaky místo jejich ASCII kódů. „\0“ značí nulový bajt (konvenční zápis z Céčka). Odpovídající Céčková deklarace by vypadala takto:

struct polozka {
  char jmeno[8];
  char cislo[4];
};
struct polozka seznam[2];

V registru r0 dostaneme ukazatel na řetězec se jménem, ke kterému bychom chtěli v seznamu najít odpovídající číslo. O to se postará následující kus kódu:

// r0 - vyhledáváný řetězec
// r1 - adresa začátku pole
// r2 - počet záznamů
// r3 - adresa aktuálně zkoumaného záznamu

MOV r3, r1
MOV r10, #12
MUL r10, r10, r2
ADD r10, r1
// r10 = r1 + 12*r2 (adresa konce pole)

porovnej:
// Porovná řetězce na adresách r0 (hledaný)
// a r3 (aktuální jméno v seznamu). Skočí
// na label "stejne" nebo "ruzne" podle výsledku

MOV r4, #0
znak: // porovnání r4-tého znaku obou řetězců
LDRB r5, [r0, r4] // znak hledaného řetězce
LDRB r6, [r3, r4] // znak jména ze seznamu
CMP r5, r6
BNE ruzne
CMP r5, 0
// narazili jsme na konec řetězce, aniž bychom
// předtím našli neshodu
BEQ stejne
ADD r4, #1
CMP r4, #8
BHS ruzne // řetězec neukončený nulou - chyba
B znak

ruzne:
ADD r3, #12 // přejdeme na další záznam
CMP r3, r10
BLO porovnej
B nenalezeno

stejne: // našli jsme shodující se jméno
ADD r3, #8 // o 8 bajtů dál je číslo
// tady bychom ho mohli třeba vypsat, kdybychom
// to uměli

nenalezeno:

Při porovnávání je třeba dát si pozor, abychom se zastavili, když libovolný z řetězců skončí (narazíme na nulový bajt). Protože zbytek bufferu za koncem řetězce může být vyplněný nějakým náhodným smetím, pokud bychom neskončili, mohli bychom dva shodné krátké řetězce vyhodnotit jako různé, protože se liší v této ignorované části.

V kódu výše je to trochu schované: pokud jsou řetězce různě dlouhé a narazíme na konec jednoho z nich, porovnávání skončí, protože se na daném místě znaky liší (jeden řetězec obsahuje nulový bajt a druhý smysluplný znak). Pokud jsou oba řetězce stejně dlouhé, narazíme na oba nulové bajty současně a pomůže nám podmínka CMP r5, 0.

Ale zrovna tak je dobré si pohlídat i maximální délku, pokud by se někde nedopatřením objevil řetězec neukončený nulou, aby porovnávání nepokračovalo donekončena (případně do doby, než narazí na nulu v úplně nesouvisející části paměti).

Paměťová reprezentace instrukcí

Paměť kromě dat, které si tam uložíme, osahuje také kód našeho programu. Tomuto principu se říká von Neumannnova architektura: kód je uložený ve stejné paměti jako data, není pro něj vyhrazené žádné speciální místo. To má spoustu výhod: pokud operační systém načítá kód programu z disku do paměti, může použít stejné instrukce pro práci s pamětí jako pro data (v tuto chvíli ten kód jsou pro něj data). Občas se taky programu může hodit generovat části svého kódu až za běhu.

Jak už jsme naznačili v minulém dílu, program není v paměti uložen jako textový zápis v assembleru, tomu by procesor nerozuměl. Místo toho je každá instrukce (včetně svých parametrů) zakódovaná jedním 32-bitovým celým číslem. Tohle je jedna z vlastností, která činí ARM příjemně jednoduchým; na jiných procesorech mají často různé druhy instrukcí různě dlouhé kódy. Instrukce musí být v paměti zarovnané (uložené na adresách, které jsou násobkem čtyř).

V procesoru existuje speciální registr označovaný pc (Program Counter, dostupný též jako r15), který obsahuje paměťovou adresu aktuálně vykonávané instrukce. Velmi zjednodušeně bychom si činnost procesoru mohli představit jako neustálé opakování následujících kroků:

  1. Načti instrukci z adresy pc v paměti
  2. Dekóduj a proveď instrukci
  3. Pokud instrukce nezměnila pc (neprovedla skok), zvyš automaticky pc o 4 (přejdi na následující instrukci).

Skutečnost je o dost složitější, protože procesor například zpracovává několik instrukcí částečně paralelně (zatímco se jedna provádí, další už se načítá atp.). To má občas trochu podivné důsledky. Například pokusíte-li se přečíst hodnotu registru pc instrukcí typu MOV r0, pc, neuloží se do r0 adresa této instrukce MOV, jak by možná člověk čekal, nýbrž hodnota o 8 vyšší (o 2 instrukce dál).

Podívejme se nyní, jak takový kód nějaké instrukce vypadá. Existuje několik různých kódování pro různé druhy instrukcí: jedno pro všechny aritmetické, jedno pro load/store, jedno pro skoky, atd.

Ukážeme si například kódování aritmetických instrukcí (používané i pro další podobné instrukce, jako např. MOV):

kódování aritmetických instrukcí

Podíváme-li se na kód instrukce, najdeme v něm:

  • Podmínku. Už v minulém díle jsme stručně zmiňovali, že podmínku jde připojit k většině instrukcí, nejen ke skoku. Daná instrukce se pak provede, pouze pokud je podmínka splněná. Každá podmínka z minulého dílu má svůj 4-bitový kód, např. 0000=EQ, 0010=HS, …Existuje speciální podmínka AL (ALways, 1110), která zařídí nepodmíněné spuštění dané instrukce. Ale v assemblerovém zápisu ji lze vynechat (píšeme MOV místo MOVAL).
  • Typ druhého argumentu („T“ v diagramu výše). Pokud T=1, druhý argument je konstanta, jinak je to registr.
  • Kód operace (opkód), který říká, o jakou instrukci vůbec jde. Např. ADD=0100, MOV=1101.
  • Bit S udávající, zda se dle výsledků má nastavit stavový registr. Tímto jsou odlišeny například instrukce ADDS a ADD.
  • Číslo registru, který tvoří první argument. Prvním argumentem musí být vždy registr. Číslo registru je přesně to, které je obsažené v jeho názvu – např. r5 je reprezentován číslem 5 (0101).
  • Číslo cílového registru, kam se uloží výsledek operace.
  • Druhý argument (registr nebo konstanta).

Pravděpodobně jste při hraní s naším simulátorem narazili na to, že některé konstanty nejde v assembleru zapsat (překladač si stěžuje, že jsou příliš velké). Teď už víte proč: na konstantní argument v instrukci zbývá jen 12 bitů, takže tam určitě libovolné 32-bitové číslo nevtěstnáme.

Autoři ARMu ale těchto 12 bitů využili velmi chytře. Namísto jednoho 12-bitového čísla (s rozsahem 0 až 4096) je rozdělili na dvě části: 8-bitovou hodnotu (x) a 4-bitovou rotaci (r). Hodnota druhého operandu vznikne jako x doplněné nulami zleva na 32 bitů a následně bitově zrotované doprava o 2r bitů. Pro r ≥ 4 se tato rotace chová jako bitový posun doleva (rozmyslete si). Takže takto dokážeme snadno vytvářet konstanty tvaru x·2k pro malá x.

Filip Štědronský

Řešení