V poslednom čase narážam stále častejšie na istú tendenciu – vyhýbať sa za každú cenu programovaniu v assembleri. Dokonca som v diskuziách na webe narazil na vyslovených odporcov assembleru. Vo svete mikrokontrolérov mi tento názor pripadá mimoriadne scestný a myslím si, že assembler má veľmi silnú pozíciu.
Takže kedy sa nám hodí znalosť assembleru?
… ak potrebujeme vedieť čo sa naozaj deje
Keď profesor Donald E. Knuth odôvodňoval, prečo vo svojich knihách na popis algoritmov používa assembler, uviedol:
People who are more than casually interested in computers should have at least some idea of what the underlying hardware is like. Otherwise the programs they write will be pretty weird.
V tomto citáte sa skrýva hlboká pravda. Čo je to vlastne program? Súbor príkazov, ktoré chceme vykonať na procesore. V reálnom živote, keď chceme niekomu zadať príkaz, musíme mať predstavu ako sa dá ten príkaz vykonať. V opačnom prípade sa môže stať, že tento príkaz nebude vykonaný efektívne, bude to priveľa stáť, prípadne príkaz nebude vykonaný vôbec. Podobný problém sa môže vyskytnúť pri programovaní mikrokontrolérov vo vyššom jazyku.
Predstavme si jednoduchý prípad. Píšeme program pre 8 bitový mikrokontrolér a potrebujeme nejakú premennú ktoré bude nadobúdať hodnotu 0 až 150. A predstavme si, že túto premennú zadefinujeme ako 16 bitový integer. Bude program fungovať? Bude. Bude fungovať optimálne? Nebude – každá jednoduchá operácia – ako napríklad inkrementácia – sa musí vykonať niekoľkými inštrukciami.
Predstavme si inú situáciu: ten istý kontrolér, iná premenná s rozsahom 0 až 500. Predstavme si, že túto premennú nastavujeme v prerušení a v hlavnom programe sa podľa nej rozhodujeme. Bude program fungovať? Nebude. Respektíve bude, ale občas sa vyskytne záhadné zlyhanie. Porovnávanie v hlavnom programe totiž musí byť realizované viacerými inštrukciami a prerušenie môže prísť uprostred porovnávania. Takisto ak premennú v hlavnom programe nastavujeme a v prerušení používame – prerušenie môže prísť uprostred priradenia. Inými slovami, tieto operácie nie sú atomické.
Pokiaľ máme nejakú prax v programovaní v assembleri, dokážeme si predstaviť, ako bude náš program preložený a ako bude procesorom vykonávaný. Ak túto predstavu nemáme, výsledný program nám dokáže pripraviť mnohé prekvapenia – či už rýchlosťou alebo spoľahlivosťou. So znalosťou programovania v assembleri vieme, že v prvom prípade výrazne pomôže zmena typu premennej na unsigned char, v druhom prípade musíme nájsť iné riešenie. Zakazovať prerušenia alebo zmeniť štruktúru programu tak, aby sme si vystačili s 8 bitovou premennou, prípadne implementovať semafor alebo zámok – možností je niekoľko.
Bez znalosti programovania v assembleri nemôžeme mať predstavu o tom, ako bude výsledný program pracovať. Znalosť inštrukčného súboru nestačí – treba vedieť, ako pomocou týchto inštrukcií implementovať bežné programové konštrukcie – cykly, vetvenia, aritmetiku s viacbytovými premennými. Niektoré veci sa dajú naučiť – napríklad zakazovať prerušenie pri práci so zdieľanými premennými – ale reálny život bude prinášať nové neznáme problémy a nie je možné zadefinovať pravidlo pre každú situáciu.
… ak chceme hospodáriť so zdrojmi
Pozrime sa na iný problém – zdroje, ktoré máme pri programovaní mikrokontrolérov k dispozícii, sú obmedzené – registre, pamäť, výkon. Ak potrebujeme ísť až na doraz, potrebujeme nájsť spôsob, ako zdroje využiť efektívnejšie. V príklade, uvedenom ďalej sú ilustrované určité možnosti. Je pravda, že aj vo vyššom jazyku máme možnosť použiť rozne algoritmy, ale so základnými konnštrukciami, ako priradenie alebo jednoduchý cyklus toho veľa neurobíme. Trochu môže pomôcť optimalizácia implementovaná v kompilátore, ale naše možnosti stále ostávajú obmedzené.
Malý vplyv na použitie zdrojov kompilátorom sa môže naplno prejaviť v aplikáciách kritických na čas. Predstavme si, že sme horko-ťažko vyladili nejakú funkciu závislú na presnom časovaní. Vhodnou kombináciou prepínačov kompilátora, poradím deklarácií premenných a podobnými trikmi dosiahneme požadovanú rýchlosť. A teraz do tej funkcie pridáme nejakú maličkosť a funkcia prestane fungovať. Príčina? Kompilátor sa rozhodol inak použiť rýchle registre. Záver: časovo kritické funkcie by nemali byť závislé na kompilátore a jeho schopnostiach optimalizácie. Takže nám neostáva nič iné ako assembler.
Aj pri aplikáciách, ktoré nie sú náročné na presné časovanie môžeme naraziť na problém. Napriek všetkému úsiliu sa nám pri programovaní vo vyššom jazyku môže stať, že narazíme na bariéru – nevieme ako program urýchliť alebo zmenšiť. Čo teraz? Zvýšiť hodinovú frekvenciu (a tým aj spotrebu), nahradiť mikrokontrolér iným (obvykle drahším)? Toto sú riešenia patriace do sveta PC a Windows, vo svete mikrokontrolérov nemajú čo hľadať. Skúsme radšej možnosť obrátiť sa na assembler. Kto to nikdy neskúsil, neuverí, aké zázraky sa môžu udiať. Niekedy práve rozhodnutie použiť assembler na kritickú časť programu urobí z nerealizovateľného projektu projekt úspešný. Assembler nám dáva podstatne širšie možnosti čo sa týka optimalizácie, máme pod kontrolou každý hodinový takt a každý register. Ostatne, v nasledujúcom príklade je to dostačne ilustrované.
Malý príklad
Zoberme si jednoduchú úlohu – kopírovanie reťazca.
Môžeme ju implementovať viacerými spôsobmi. Jednou možnosťou je smerníkový spôsob – podobne je riešené kopírovanie reťazca (funkcia strcpy) v štandardnej knižnici C:
char *src,*dsc; src=SRC; dst=DST; while ((*dst++ = *src++) != '\0');
A teraz ju prepíšme do assemblera:
MOV #SRC,R15 ; 2 ((1000+1)*9)+5 = 9014 MOV #DST,R14 ; 2 3 registre CLR R13 ; 1 LOOP: MOV.B @R15+,0(R14) ; 5 CMP @R14+,R13 ; 2 JNZ LOOP ; 2
Výpočty uvedené vpravo hore hovoria o čase potrebnom na skopírovanie 1000 znakov (plus záverečná nula) a o počte spotrebovaných registrov. Čísla za bodkočiarkou hovoria o počte taktov pre jednotlivé inštrukcie.
V našom prípade potrebujeme jeden register (R13) len na držanie konštanty 0, ktorá sa používa na porovnávanie. Asi vás napadlo, prečo nevyužiť generátor konštánt. Ale nejde to. &adresový mód @R14+ sa dá použiť len pre zdrojový operand, takisto ako generátor konštánt. Verte mi. Skúšal som to.
Takže máme 9000 taktov a tri registre. Čo tak skúsiť spôsob s poliami:
int i; i=0; while ((DST[i] = SRC[i]) != '\0') i++;
Tu nám už stačí jeden register pre držanie indexu. Ale je tu iný problém: tá istá operácia nám zaberie 15000 taktov:
CLR R15 ; 1 (1000*15)+12+1 = 15013 LOOP: ; 1 register MOV.B SRC(R15),DST(R15) ; 6 TST.B DST(R15) ; 4 JZ QUIT ; 2 INC R15 ; 1 JMP LOOP ; 2 QUIT:
Okrem iného je to spôsobené tým, že v každom cykle máme dva skoky. Skúsme použiť fintu, aby sme počet skokov znížili – presuňme inkrementáciu indexu na začiatok cyklu a znížme inicializačnú hodnotu indexu:
MOV #-1,R15 ; 1 ((x+1)*13)+1 = 13014 LOOP: ; 1 register INC R15 ; 1 MOV.B SRC(R15),DST(R15) ; 6 TST.B DST(R15) ; 4 JNZ LOOP ; 2
Výsledok je lepší: 13000 cyklov. Skúsme ešte ďalšiu variantu – skúsme nahradiť inštrukcie s veľkým počtom hodinových cyklov inými, napríklad operáciami s registrom. Výsledkom je opäť zlepšenie – 11000 cyklov, ale za cenu použitia dvoch registrov:
MOV #-1,R15 ; 1 ((x+1)*11)+1 = 11012 LOOP: ; 2 registre INC R15 ; 1 MOV.B SRC(R15),R14 ; 3 MOV.B R14,DST(R15) ; 4 TST.B R14 ; 1 JNZ LOOP ; 2
Na príkladoch vidíme, ako sa môžme hrať s použitím registrov, počítaním taktov a optimalizáciou. Niekedy je až prekvapujúce, koľko času sa dá ušetriť vhodnou optimalizáciou. Druhou zaujímavosťou je závislosť medzi počtom registrov a rýchlosťou algoritmu. Ale tento vzťah je silne závislý od architektúry. Práve možnosť hrania sa s registrami a počtom taktov je vlastnosť assembleru, ktorá ho stavia nad vyššie jazyky.
Na záver ma napadlo pozrieť sa, ako si s týmito dvomi spôsobmi kopírovania poradí kompilátor gcc. A prišlo nemilé prekvapenie. Smerníková metóda, ktorá bola v assembleri jednoznačne najrýchlejšia a ktorú som aj ja považoval za efektívnejšiu v C, sa ukázala o niečo pomalšia.
Smerníková metóda na skopírovanie 1000 znakového reťazca potrebuje nekonečných 12000 taktov a 3 registre:
; char *src,*dsc; ; ; src=SRC; ; dst=DST; ; while ((*dst++ = *src++) != '\0');mov #DST,r13 ; 2 ((1000+1) * 12) + 4 = 12016 mov #SRC,r14 ; 2 3 registre .L2: mov.b @r14,@r13 ; 5 mov.b @r14,r12 ; 2 add #llo(1),r14 ; 1 add #llo(1),r13 ; 1 cmp.b #llo(0),r12 ; 1 jne .L2 ; 2
Indexovej metóde to zabralo iba 11000 taktov. Počet registrov bol rovnaký. Ale assemblerovslému riešeniu sa to stále nevyrovná.
; int i; ; ; i=0; ; while ((DST[i] = SRC[i]) != '\0') i++; mov #llo(0), r14 ; 1 ((1000+1) * 11) + 11 = 11022 mov.b &SRC, r12 ; 3 š registre mov.b r12, &DST ; 4 cmp.b #llo(0), r12 ; 1 jeq .L10 ; 2 .L8: add #llo(1), r14 ; 1 mov.b SRC(r14), r13 ; 3 mov.b r13, DST(r14) ; 4 cmp.b #llo(0), r13 ; 1 jne .L8 ; 2 .L10:
Ako posledný príklad si pozrime klasický zápis (pascalovský štýl, aby som bol hnusný). 18000 taktov je dvojnásobok nášho najlepšieho asssemblerovského riešenia. A je to o 50-60 percent viac, ako odporný jednoriadkový C-čkový zápis. Komentár si radšej odpustím.
; i=0; ; ; while (1) { ; DST[i] = SRC[i]; ; if (SRC[i] == 0) break; ; i++; ; } mov #llo(0), r13 ; 1 ((1000) * 18) +15 + 5 = 18020 mov #DST, r11 ; 2 5 registrov mov #SRC, r12 ; 2 .L6: mov r13, r14 ; 1 add r11, r14 ; 1 mov r13, r15 ; 1 add r12, r15 ; 1 mov.b @r15, @r14 ; 5 cmp.b #llo(0), @r15 ; 4 jeq .L3 ; 2 add #llo(1), r13 ; 1 jmp .L6 ; 2 .L3:
Ale aby som bol fair, použil som rovnakú fintu na zefektívnenie cyklu, tak ako v assemblerovských príkladoch. A dostal som sa na 16000 taktov.
; i=-1; ; ; do { ; i++; ; DST[i] = SRC[i]; ; } while (SRC[i]); mov #llo(-1), r13 ; 1 ((1000) * 16) +15 + 5 = 16020 mov #DST, r11 ; 2 5 registrov mov #SRC, r12 ; 2 .L2: add #llo(1), r13 ; 1 mov r13, r14 ; 1 add r11, r14 ; 1 mov r13, r15 ; 1 add r12, r15 ; 1 mov.b @r15, @r14 ; 5 cmp.b #llo(0), @r15 ; 4 jne .L2 ; 2
Čo povedať na záver?
Snáď len odpovedať na otázku v nadpise tohoto článku. Má programovanie v assembleri zmysel? Áno, má.
To neznamená, že by sme mali zavrhnúť programovanie mikrokontrolérov vo vyšších jazykoch. Ale mali by sme dokázať posúdiť, kde je assembler prínosom a tam ho použiť. Striktné trvanie na použití vyšších jazykov je vážnou chybou.
Ja osobne používam C na vytvorenie prototypu a potom niektoré časti urýchľujem, optimalizujem, prepisujem do assembleru. Niekedy vytváram celý projekt v assembleri a niektoré algoritmicky náročné časti vyviniem v C a potom ich prepíšem do assembleru. Niekedy si nechám vytvoriť assemblerový výpis programu v C a skúšam hľadať najoptimálnejšie konštrukcie v C.
Takže nebojme sa assembleru. Vo svete mikrokontrolérov má stále svoje miesto.