Obsah
1. Překlad funkcí přímo do nativního kódu MicroPythonem
2. Tři způsoby překladu funkcí v MicroPythonu
3. Překlad funkcí do nativního kódu
5. 32bitová aritmetika pro co nejvyšší výpočetní výkon
6. Rychlost výpočtů – programové smyčky
8. Povinné přidání typových informací
9. Rychlost výpočtů – volání funkce
10. Realizace stejné funkce přímo s využitím strojových instrukcí
12. Jak vlastně vypadají funkce přeložené do nativního kódu?
14. Zpětný překlad strojového kódu
15. Překlad funkce s dvojicí vnořených programových smyček
16. Další optimalizace v MicroPythonu
17. Nativní kód uložený v souborech .mpy
18. Repositář s demonstračními příklady
1. Překlad funkcí přímo do nativního kódu MicroPythonem
V předchozí trojici článků o projektu MicroPython [1], [2], [3] jsme se seznámili s tím, jakým způsobem je možné přímo v MicroPythonu vytvářet funkce obsahující strojové instrukce patřící do instrukčních sad Thumb a Thumb-2. Jedná se o (pochopitelně z mého pohledu) velmi užitečnou techniku, která může programování na úrovni instrukcí přiblížit i začátečníkům, protože nevyžaduje časové investice do zaučení práce s assemblerem (včetně všech jeho zákeřností) a dalších nástrojů (nehledě na to, že propojení nativního kódu s Pythonem není jednoduché a vyžaduje další znalosti). Ovšem ani v případech, v nichž je zapotřebí urychlit běh některých funkcí, není nutné přecházet přímo „ke strojáku“. MicroPython totiž umožňuje překlad vybraných funkcí do nativního kódu a nikoli do bajtkódu Pythonu. To je velmi užitečné především v oblasti mikrořadičů, kde například reakce na různé události musí být co nejrychlejší atd.
2. Tři způsoby překladu funkcí v MicroPythonu
Ve skutečnosti v současném MicroPythonu existují celkem tři různé způsoby, jakými mohou být funkce přeloženy. K překladu dochází v každém případě, liší se ovšem výsledek i vlastní způsob překladu. V každém případě je zdrojový kód funkce nejprve načten, následně je provedena lexikální analýza a dále je zparsován a transformován do abstraktního syntaktického stromu (AST). Další krok je již volitelný a je možné si vybrat:
- Funkce může být přeložena do bajtkódu Pythonu. Tento bajtkód je posléze spuštěn ve virtuálním stroji Pythonu. Toto řešení se vlastně nijak neodlišuje od standardního CPythonu. A pochopitelně se kvůli nutnosti interpretace bajtkódu jedná o obecně nejpomalejší řešení.
- Funkce ovšem může být přeložena i do nativního kódu dané platformy. Výsledný nativní (strojový) kód sice zabere v operační paměti (či ve Flash) více místa než bajtkód, ovšem výsledek bude rychlejší, jak ostatně uvidíme na benchmarcích.
- Překlad může být proveden i s využitím technologie nazvanéViper, která kromě jiného dokáže pracovat s 32bitovými celočíselnými hodnotami namísto typu long. Výsledek by měl být v tomto případě nejrychlejší (předpokládáme běh na 32bitových mikrořadičích a mikroprocesorech), ovšem oproti bajtkódu se opět jedná o paměťově náročnější variantu.
3. Překlad funkcí do nativního kódu
Ukažme si nejprve běžnou Pythonovskou funkci, která po svém zavolání získá jeden číselný argument, sečte ho se sebou samým a vrátí výsledek tohoto součtu. Do této funkce pro zajímavost předáme celočíselné hodnoty pohybující se okolo 231 (proč, to uvidíme dále):
def foo(x): return x+x for i in range(0x7ffffffc, 0x80000008): print(hex(foo(i)))
Výsledkem budou tyto hodnoty, které plně odpovídají očekávání:
0xfffffff8 0xfffffffa 0xfffffffc 0xfffffffe 0x100000000 0x100000002 0x100000004 0x100000006 0x100000008 0x10000000a 0x10000000c 0x10000000e
Předchozí funkce byla po lexikální analýze a parsingu převedena do bajtkódu. Stejnou funkci je ovšem možné přeložit do nativního kódu dané platformy. Postačí před ní zapsat dekorátor @micropython.native:
@micropython.native def foo(x): return x+x for i in range(0x7ffffffc, 0x80000008): print(hex(foo(i)))
I tímto způsobem přeložená funkce bude stále vracet korektní výsledky:
0xfffffff8 0xfffffffa 0xfffffffc 0xfffffffe 0x100000000 0x100000002 0x100000004 0x100000006 0x100000008 0x10000000a 0x10000000c 0x10000000e
>>> foo("abc") 'abcabc' >>> foo([1,2,3]) [1, 2, 3, 1, 2, 3]
4. Použití překladače Viper
Namísto výše uvedeného dekorátoru @micropython.native ovšem můžeme zapsat i dekorátor @micropython.viper. Tento dekorátor povoluje odlišný typ překladu do strojového kódu, v němž může docházet k více optimalizacím, ovšem výsledné chování nemusí odpovídat klasickému Pythonu:
@micropython.viper def foo(x): return x+x for i in range(0x7ffffffc, 0x80000008): print(hex(foo(i)))
Výsledky získané po zavolání této funkce budou stále korektní:
0xfffffff8 0xfffffffa 0xfffffffc 0xfffffffe 0x100000000 0x100000002 0x100000004 0x100000006 0x100000008 0x10000000a 0x10000000c 0x10000000e
5. 32bitová aritmetika pro co nejvyšší výpočetní výkon
Překladači (či možná přesněji řečeno „emitoru strojového kódu“) Viper můžeme předat i funkci s typovými informacemi (type hints). Lze například specifikovat, že parametr funkce je typu uint a i výsledkem funkce bude hodnota typu uint. V kontextu Viperu znamená uint 32bitovou celočíselnou hodnotu bez znaménka:
@micropython.viper def foo(x: uint) -> uint: return x+x for i in range(0x7ffffffc, 0x80000008): print(hex(foo(i)))
Nyní se bude funkce chovat odlišně – všechny výpočty budou skutečně prováděny v 32bitové aritmetice a bude tedy docházet k přetečení:
0xfffffff8 0xfffffffa 0xfffffffc 0xfffffffe 0x0 0x2 0x4 0x6 0x8 0xa 0xc 0xe
Navíc již nebude takto specifikovaná funkce tak univerzální, jako funkce předcházející, což si můžeme snadno ověřit:
>>> foo("abc") Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can't convert str to int >>> foo([1,2,3]) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can't convert list to int
Ve skutečnosti ovšem nejsme omezeni na beznaménkové typy:
@micropython.viper def bar() -> int: return int(0xffffffff) print(bar())
Tento příklad po svém překladu a spuštění v MicroPythonu vypíše hodnotu –1.
Naproti tomu:
@micropython.viper def bar() -> uint: return uint(0xffffffff) print(bar())
vypíše hodnotu 4294967295.
6. Rychlost výpočtů – programové smyčky
Nyní již víme, jakým způsobem lze nějakou funkci přeložit do bajtkódu, do nativního kódu a popř. do optimalizovaného nativního kódu. Zbývá zjistit, zda se vůbec vyplatí překlad do nativního kódu. Pokusme se nejdříve o změření doby trvání funkce se dvěma vnořenými smyčkami. Celkově se hodnota lokální proměnné x zvýší milionkrát a posléze se vypíše:
def loop(): x = 0 for i in range(1000): for j in range(1000): x+=1 print(x) import utime t1 = utime.ticks_us() loop() t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
Zkusme nyní stejnou funkci přeložit do strojového kódu a posléze změřit dobu jejího běhu:
@micropython.native def loop(): x = 0 for i in range(1000): for j in range(1000): x+=1 print(x) import utime t1 = utime.ticks_us() loop() t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
A konečně přichází na řadu technologie Viper:
@micropython.viper def loop(): x = 0 for i in range(1000): for j in range(1000): x+=1 print(x) import utime t1 = utime.ticks_us() loop() t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
7. Výsledky benchmarků
Podívejme se nyní na dobu běhu všech tří variant funkcí:
Varianta | Doba běhu |
---|---|
bajtkód | 5348108 |
nativní kód | 2398861 |
překlad Viperem | 236460 |
Přeložená funkce je tedy více než dvojnásobně rychlejší než varianta přeložená do bajtkódu (a poté interpretovaná). Ovšem přesvědčivě vyhrála technologie Viper, která je ještě desetkrát rychlejší, než funkce přeložená do nativního kódu přes dekorátor @micropython.native a tedy více než dvacetkrát rychlejší, než funkce interpretovaná.
8. Povinné přidání typových informací
Pokusme se nyní funkci s dvojicí vnořených smyček upravit tak, aby namísto tisku výsledku vrátila hodnotu naakumulovanou v proměnné x:
@micropython.viper def loop(): x = 0 for i in range(1000): for j in range(1000): x+=1 return x import utime t1 = utime.ticks_us() loop() t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
Tato funkce, pokud bude překládaná přes Viper, nebude akceptována:
Traceback (most recent call last): File "<stdin>", line 7, in loop ViperTypeError: return expected 'object' but got 'int'
Proč tomu tak je? V Pythonu jsou všechny hodnoty považovány za objekty, a přístup k nim je řešen přes reference. Ovšem Viper odvodil, že proměnná x je typu int a nyní neví, jak tuto hodnotu převést na objekt (předpokládá totiž, že funkce vrací objekt). Pomoc je v tomto případě relativně snadná – opět použijeme typové informace:
@micropython.viper def loop() -> int: x = 0 for i in range(1000): for j in range(1000): x+=1 return x import utime t1 = utime.ticks_us() loop() t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
9. Rychlost výpočtů – volání funkce
U funkcí, v nichž se požívají vnořené programové smyčky, spouští se výpočty s 32bitovými hodnotami atd., již máme alespoň rámcovou představu, jakého urychlení lze jejich překladem do nativního kódu dosáhnout. Ovšem ještě je nutné zjistit, jak je časově náročné volání funkcí, konkrétně funkcí přeložených do bajtkódu, nativního kódu či nativního kódu emitovaného Viperem.
Vyzkoušejme si například jednoduchý benchmark, v němž budeme opakovaně volat funkci add. První varianta funkce je překládaná do bajtkódu:
def add(x, y): return x+y import utime t1 = utime.ticks_us() x = 0 for i in range(10000): x = add(x, 10) print(x) t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
Podobně budeme volat funkci přeloženou do nativního kódu:
@micropython.native def add(x, y): return x+y import utime t1 = utime.ticks_us() x = 0 for i in range(10000): x = add(x, 10) print(x) t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
Stejným způsobem si můžeme nechat funkci přeložit Viperem:
@micropython.viper def add(x, y): return x+y import utime t1 = utime.ticks_us() x = 0 for i in range(10000): x = add(x, 10) print(x) t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
Ve čtvrté variantě taktéž použijeme Viper, ovšem s plným uvedením typových informací u parametrů i u návratové hodnoty:
@micropython.viper def add(x: int, y: int) -> int: return x+y import utime t1 = utime.ticks_us() x: int = 0 for i in range(10000): x = add(x, 10) print(x) t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
10. Realizace stejné funkce přímo s využitím strojových instrukcí
Pro zajímavost a porovnání si funkci přepišme z Pythonu do instrukcí z instrukční sady Thumb. To pro nás není nic nového a postačuje vědět, že první parametr funkce se předává v registru R0 a druhý parametr v registru R1. Výsledek funkce je očekáván v registru R0, takže se nám vlastně celý problém „smrskne“ na jedinou strojovou instrukci add, ke které je přidána dvojice instrukcí push a pop:
@micropython.asm_thumb def add(r0, r1): add(r0, r0, r1) import utime t1 = utime.ticks_us() x: int = 0 for i in range(10000): x = add(x, 10) print(x) t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
11. Výsledky benchmarků
Z výsledků benchmarků vyplývá, že nativní kód je opět rychlejší (což se dalo očekávat), ovšem urychlení již není tak významné jako v předchozím benchmarku s výpočetně složitými zanořenými programovými smyčkami). To je vlastně i pochopitelné, protože se hodně času stráví v kódu pro předávání argumentů a v neoptimalizovaném Pythonovském kódu. Povšimněte si však, že přidání typových informací do funkce překládané Viperem výsledky paradoxně zhoršilo, i když nepatrně. A realizace přímo ve strojovém kódu kupodivu není rychlejší, což souvisí s transformacemi hodnot z Pythonních objektů do nativních 32bitových hodnot a zpět:
Varianta | Doba běhu |
---|---|
bajtkód | 208311 |
nativní kód | 163235 |
překlad Viperem | 136255 |
překlad Viperem + typové informace | 145537 |
použití instrukcí Thumb | 137725 |
12. Jak vlastně vypadají funkce přeložené do nativního kódu?
V předchozím textu jsme si řekli, že překlad do nativního kódu obecně vyžaduje větší kapacitu paměti, než prostý překlad do bajtkódu (ten totiž obsahuje poměrně vysokoúrovňové instrukce). Můžeme se ostatně relativně snadno přesvědčit, jak vypadá strojový kód přeložených funkcí, tedy funkcí označených dekorátorem @microptython.native či @micropython.viper. Pro zjištění, jak vlastně vypadá tělo funkce přeložené do strojového kódu, slouží následující kód, který vznikl úpravou skriptu zmíněného v této diskuzi https://github.com/orgs/micropython/discussions/12257 (ovšem jeho autor je ještě někdo další):
import machine import array def inspect(f): baddr = bytes(array.array("O", [f])) addr = int.from_bytes(baddr, "little") print("function object at: 0x%08x" % addr) print("number of args: %u" % machine.mem32[addr + 8]) code_addr = machine.mem32[addr + 12] print("machine code at: 0x%08x" % code_addr) previous = -1 size = 0 while True: current = machine.mem8[code_addr + size] size += 1 if current == 0xbd and previous == 0xfe: break previous = current print(f"machine code size: {size} bytes") print("-- code --") for i in range(size): print(f"{machine.mem8[code_addr + i]:02x}", end=" ") if i % 16 == 15: print() print("\n----------")
- Liší se výpočet adresy s počtem argumentů
- Liší se výpočet adresy, od níž je strojový kód uložený
- Taktéž poslední instrukce bývá odlišná (jedná se opět o instrukci POP, ale s jinými registry)
13. Strojový kód funkce add
Prozkoumejme nyní strojový kód vzniklý překladem funkce add, zde ve variantě bez uvedení typových informací:
@micropython.viper def add(x, y): return x+y
Strojový kód přeložené funkce prozkoumáme (povšimněte si nekorektního výpočtu počtu argumentů):
>>> inspect(add) function object at: 0x2000e2b0 number of args: 0 machine code at: 0x2000e510 machine code size: 66 bytes -- code -- fe b5 47 68 ff 68 3f 68 08 46 11 46 1e 46 00 29 00 d0 03 e0 02 22 90 42 00 d1 07 e0 00 4a 01 e0 04 00 04 00 3b 46 34 33 db 6f 98 47 30 68 04 46 70 68 05 46 2a 46 21 46 1b 20 bb 6c 98 47 ff e7 fe bd ----------
Funkce je přeložena do 66 bajtů, což odpovídá 33 instrukcím. To je poměrně hodně, ovšem musíme si uvědomit, že jsme nepředali typové informace argumentů a proto se interně bude volat metoda _add_ pro různé možné typy objektů atd. Kód tedy nebude příliš optimální.
Prozkoumáme výsledek překladu funkce s přidanými typovými informacemi:
@micropython.viper def add(x: int, y: int) -> int: return x+y
Výsledek kupodivu bude ještě delší, konkrétně osmdesát bajtů:
>>> inspect(add) function object at: 0x2000f340 number of args: 0 machine code at: 0x2000f5a0 machine code size: 80 bytes -- code -- fe b5 47 68 ff 68 3f 68 08 46 11 46 1e 46 00 29 00 d0 03 e0 02 22 90 42 00 d1 07 e0 00 4a 01 e0 04 00 04 00 3b 46 34 33 db 6f 98 47 30 68 02 21 fb 68 98 47 04 46 70 68 02 21 fb 68 98 47 05 46 21 46 49 19 08 46 02 21 3b 69 98 47 ff e7 fe bd
14. Zpětný překlad strojového kódu
Zpětný překlad prvního strojového kódu vypadá následovně. Za povšimnutí stojí způsob přečtení argumentů (jedná se o ustálenou šablonu) a taktéž odlišné registry v instrukcích PUSH a POP na začátku a konci sekvence instrukcí:
push {r1, r2, r3, r4, r5, r6, r7, lr} ldr r7, [r0, #4] ldr r7, [r7, #0xc] ldr r7, [r7] mov r0, r1 mov r1, r2 mov r6, r3 cmp r1, #0 beq #0x14 b #0x1c movs r2, #2 cmp r0, r2 bne #0x1c b #0x2c ldr r2, [pc, #0] b #0x24 movs r4, r0 movs r4, r0 mov r3, r7 adds r3, #0x34 ldr r3, [r3, #0x7c] blx r3 ldr r0, [r6] mov r4, r0 ldr r0, [r6, #4] mov r5, r0 mov r2, r5 mov r1, r4 movs r0, #0x1b ldr r3, [r7, #0x48] blx r3 b #0x40 pop {r1, r2, r3, r4, r5, r6, r7, pc}
Druhá verze funkce s typovými informacemi je přeložena mírně odlišně, ale můžeme opět vidět „šablonu“ na začátku a konci (zcela shodné instrukce). Ovšem samotný výpočet (který by šel realizovat jedinou instrukcí) zde nalézt můžeme. Tuto část kódu jsme zvýraznili:
push {r1, r2, r3, r4, r5, r6, r7, lr} ldr r7, [r0, #4] ldr r7, [r7, #0xc] ldr r7, [r7] mov r0, r1 mov r1, r2 mov r6, r3 cmp r1, #0 beq #0x14 b #0x1c movs r2, #2 cmp r0, r2 bne #0x1c b #0x2c ldr r2, [pc, #0] b #0x24 movs r4, r0 movs r4, r0 mov r3, r7 adds r3, #0x34 ldr r3, [r3, #0x7c] blx r3 ldr r0, [r6] movs r1, #2 ldr r3, [r7, #0xc] blx r3 mov r4, r0 ldr r0, [r6, #4] movs r1, #2 ldr r3, [r7, #0xc] blx r3 mov r5, r0 mov r1, r4 adds r1, r1, r5 mov r0, r1 movs r1, #2 ldr r3, [r7, #0x10] blx r3 b #0x4e pop {r1, r2, r3, r4, r5, r6, r7, pc}
A takto si můžeme zobrazit rozdíly (resp. spíše shodné části) obou sekvencí instrukcí:
push {r1, r2, r3, r4, r5, r6, r7, lr} push {r1, r2, r3, r4, r5, r6, r7, lr} ldr r7, [r0, #4] ldr r7, [r0, #4] ldr r7, [r7, #0xc] ldr r7, [r7, #0xc] ldr r7, [r7] ldr r7, [r7] mov r0, r1 mov r0, r1 mov r1, r2 mov r1, r2 mov r6, r3 mov r6, r3 cmp r1, #0 cmp r1, #0 beq #0x14 beq #0x14 b #0x1c b #0x1c movs r2, #2 movs r2, #2 cmp r0, r2 cmp r0, r2 bne #0x1c bne #0x1c b #0x2c b #0x2c ldr r2, [pc, #0] ldr r2, [pc, #0] b #0x24 b #0x24 movs r4, r0 movs r4, r0 movs r4, r0 movs r4, r0 mov r3, r7 mov r3, r7 adds r3, #0x34 adds r3, #0x34 ldr r3, [r3, #0x7c] ldr r3, [r3, #0x7c] blx r3 blx r3 ldr r0, [r6] ldr r0, [r6] > movs r1, #2 > ldr r3, [r7, #0xc] > blx r3 mov r4, r0 mov r4, r0 ldr r0, [r6, #4] ldr r0, [r6, #4] > movs r1, #2 > ldr r3, [r7, #0xc] > blx r3 mov r5, r0 mov r5, r0 mov r2, r5 < mov r1, r4 mov r1, r4 movs r0, #0x1b | adds r1, r1, r5 ldr r3, [r7, #0x48] | mov r0, r1 > movs r1, #2 > ldr r3, [r7, #0x10] blx r3 blx r3 b #0x40 | b #0x4e pop {r1, r2, r3, r4, r5, r6, r7, pc} pop {r1, r2, r3, r4, r5, r6, r7, pc}
15. Překlad funkce s dvojicí vnořených programových smyček
Podívejme se na závěr na způsob překladu jedné složitější funkce do strojového kódu. Jedná se o nám již dobře známou funkci obsahující dvojici vnořených programových smyček. Do strojového kódu ji budeme překládat Viperem:
@micropython.viper def loop() -> int: x = 0 for i in range(1000): for j in range(1000): x+=1 return x
Délka strojového kódu vzniklého překladem bude (možná až překvapivě) velká: celých 152 bajtů, což odpovídá 76 strojovým instrukcím. A navíc, jak uvidíme dále, se ještě z tohoto kódu volají další subrutiny:
>>> inspect(loop) function object at: 0x200100e0 number of args: 0 machine code at: 0x20010500 machine code size: 152 bytes -- code -- fe b5 82 b0 47 68 ff 68 3f 68 08 46 11 46 1e 46 00 29 00 d0 03 e0 00 22 90 42 00 d1 04 e0 00 22 3b 46 34 33 db 6f 98 47 00 24 00 21 00 91 20 e0 00 98 05 46 00 90 00 21 01 91 0a e0 01 98 06 46 01 22 21 46 89 18 0c 46 01 90 01 22 01 99 89 18 01 91 01 98 7d 22 d2 00 01 46 01 90 91 42 01 db 00 20 00 e0 01 20 00 28 e8 d1 01 22 00 99 89 18 00 91 00 98 7d 22 d2 00 01 46 00 90 91 42 01 db 00 20 00 e0 01 20 00 28 d2 d1 00 20 02 21 3b 69 98 47 ff e7 02 b0 fe bd
Při pohledu na výsledek zpětného překladu do assembleru lze ony programové smyčky v kódu nalézt – viz lokální nepodmíněné a podmíněné skoky. Z tohoto důvodu jsem do vypsaného kódu ručně přidal i adresy instrukcí, aby bylo patrné, že všechny nepodmíněné skoky B i všechny podmíněné skoky jsou skoky lokálními (v rámci funkce). Jediné skoky „mimo“ jsou realizovány instrukcemi BLX (a adresa takového skoku musí být nejdříve vypočtena):
0x00 push {r1, r2, r3, r4, r5, r6, r7, lr} 0x02 sub sp, #8 0x04 ldr r7, [r0, #4] 0x06 ldr r7, [r7, #0xc] 0x08 ldr r7, [r7] 0x0a mov r0, r1 0x0c mov r1, r2 0x0e mov r6, r3 0x10 cmp r1, #0 0x12 beq #0x16 0x14 b #0x1e 0x16 movs r2, #0 0x18 cmp r0, r2 0x1a bne #0x1e 0x1c b #0x28 0x1e movs r2, #0 0x20 mov r3, r7 0x22 adds r3, #0x34 0x24 ldr r3, [r3, #0x7c] 0x26 blx r3 0x28 movs r4, #0 0x2a movs r1, #0 0x2c str r1, [sp] 0x2e b #0x72 0x30 ldr r0, [sp] 0x32 mov r5, r0 0x34 str r0, [sp] 0x36 movs r1, #0 0x38 str r1, [sp, #4] 0x3a b #0x52 0x3c ldr r0, [sp, #4] 0x3e mov r6, r0 0x40 movs r2, #1 0x42 mov r1, r4 0x44 adds r1, r1, r2 0x46 mov r4, r1 0x48 str r0, [sp, #4] 0x4a movs r2, #1 0x4c ldr r1, [sp, #4] 0x4e adds r1, r1, r2 0x50 str r1, [sp, #4] 0x52 ldr r0, [sp, #4] 0x54 movs r2, #0x7d 0x56 lsls r2, r2, #3 0x58 mov r1, r0 0x5a str r0, [sp, #4] 0x5c cmp r1, r2 0x5e blt #0x64 0x60 movs r0, #0 0x62 b #0x66 0x64 movs r0, #1 0x66 cmp r0, #0 0x68 bne #0x3c 0x6a movs r2, #1 0x6c ldr r1, [sp] 0x6e adds r1, r1, r2 0x70 str r1, [sp] 0x72 ldr r0, [sp] 0x74 movs r2, #0x7d 0x76 lsls r2, r2, #3 0x78 mov r1, r0 0x7a str r0, [sp] 0x7c cmp r1, r2 0x7e blt #0x84 0x80 movs r0, #0 0x82 b #0x86 0x84 movs r0, #1 0x86 cmp r0, #0 0x88 bne #0x30 0x8a movs r0, #0 0x8c movs r1, #2 0x8e ldr r3, [r7, #0x10] 0x90 blx r3 0x92 b #0x94 0x94 add sp, #8 0x96 pop {r1, r2, r3, r4, r5, r6, r7, pc}
16. Další optimalizace v MicroPythonu
Překlad funkcí do nativního kódu je jen jednou z forem optimalizací, které je možné v MicroPythonu provádět. Tvůrci MicroPythonu do tohoto jazyka (či dialektu) totiž přidali i některé další vlastnosti specifické pouze pro MicroPython.
V první řadě je možné do jisté míry řídit automatického správce paměti (garbage collector) a určit ty objekty, které se nebudou zvětšovat, což může ovlivnit způsob jejich alokace). To se v praxi týká poměrně velkého množství typů objektů; například se může jednat o různé buffery atd.
Dále lze s výhodou používat kontejnery typu array, bytes a bytearray, navíc kombinované s memoryview, což je pohled na řez pole (neobsahuje tedy data, resp. kopii dat, ale jen offset a délku – podobně jako řezy v programovacím jazyku Go). A konečně MicroPython podporuje deklaraci konstant s využitím const(). Jedná se o obdobu symbolických konstant známých z programovacího jazyka C.
17. Nativní kód uložený v souborech .mpy
Za zmínku stojí ještě jedna užitečná vlastnost MicroPythonu. Tento jazyk totiž umožňuje pracovat se soubory s koncovkou .mpy, které obsahují buď bajtkód Pythonu nebo strojový kód pro danou architekturu mikrořadiče nebo mikroprocesoru (jedná se vlastně o formát definující kontejner pro různá binární data, včetně spustitelného kódu). Instrukce strojového kódu mohou vzniknout buď překladem assembleru nebo i překladem zdrojového kódu napsaného v nějakém vyšším programovacím jazyku (což bude typicky C, popř. Rust). V současnosti MicroPython podporuje tyto architektury:
- x86 (32 bit)
- x64 (64 bit)
- armv6m (Thumb)
- armv7m (Thumb-2)
- armv7emsp (Thumb-2, single precision float)
- armv7emdp (Thumb-2, double precision float)
- xtensa (ESP8266)
- xtensawin (ESP32)
Aby mohl být strojový kód slinkován s VM MicroPythonu, musí být nezávislý na svém umístění (PIC – Position Independent Code) v operační paměti, tj. pro lokální skoky či adresování proměnných se nesmí používat absolutní adresy. A typicky tento kód navíc obsahuje i tabulku GOT (Global Offset Table). Jak tyto soubory vznikají a jak je možné volat nativní funkce v nich uložené, si řekneme v navazujícím článku.
18. Repositář s demonstračními příklady
Zdrojové kódy všech prozatím popsaných demonstračních příkladů určených pro MicroPython (a otestovaných na RP2040) byly uloženy do repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs:
Pro úplnost ještě přidávám odkazy na demonstrační příklady, v nichž se používají funkce se strojovými instrukcemi ze sady Thumb a Thumb-2. Ty jsme si popsali v předchozí trojici článků:
19. Odkazy na Internetu
- Maximising MicroPython speed
https://docs.micropython.org/en/latest/reference/speed_python.html - Online ARM converter
https://armconverter.com/?disasm - The 3 different code emitters
https://www.kickstarter.com/projects/214379695/micro-python-python-for-microcontrollers/posts/664832 - The 3 different code emitters, part 2
https://www.kickstarter.com/projects/214379695/micro-python-python-for-microcontrollers/posts/665145 - Fast Filters for the Pyboard
https://github.com/peterhinch/micropython-filters - How to load 32 bit constant from assembler with @micropython.asm_thumb
https://forum.micropython.org/viewtopic.php?f=21&t=12931&sid=25de8871fa9cfcf8cafb6318f9d8ba3a - Pi pico, micropython.asm_thumb: ADR Rd, <label> and LDR Rd, <label> not implemented?
https://github.com/orgs/micropython/discussions/12257 - MicroPython documentation
https://docs.micropython.org/en/latest/index.html - Inline assembler for Thumb2 architectures
https://docs.micropython.org/en/latest/reference/asm_thumb2_index.html - Inline assembler in MicroPython
https://docs.micropython.org/en/latest/pyboard/tutorial/assembler.html#pyboard-tutorial-assembler - MCU market turns to 32-bits and ARM
http://www.eetimes.com/document.asp?doc_id=1280803 - Cortex-M0 Processor (ARM Holdings)
http://www.arm.com/products/processors/cortex-m/cortex-m0.php - Cortex-M0+ Processor (ARM Holdings)
http://www.arm.com/products/processors/cortex-m/cortex-m0plus.php - ARM Processors in a Mixed Signal World
http://www.eeweb.com/blog/arm/arm-processors-in-a-mixed-signal-world - RISCové mikroprocesory s komprimovanými instrukčními sadami
https://www.root.cz/clanky/riscove-mikroprocesory-s-komprimovanymi-instrukcnimi-sadami/ - RISCové mikroprocesory s komprimovanými instrukčními sadami (2)
https://www.root.cz/clanky/riscove-mikroprocesory-s-komprimovanymi-instrukcnimi-sadami-2/ - ARM Architecture (Wikipedia)
https://en.wikipedia.org/wiki/ARM_architecture - Cortex-M0 (Wikipedia)
https://en.wikipedia.org/wiki/ARM_Cortex-M0 - Cortex-M0+ (Wikipedia)
https://en.wikipedia.org/wiki/ARM_Cortex-M#Cortex-M0.2B - Improving ARM Code Density and Performance
New Thumb Extensions to the ARM Architecture Richard Phelan - The ARM Processor Architecture
http://www.arm.com/products/processors/technologies/instruction-set-architectures.php - Thumb-2 instruction set
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0344c/Beiiegaf.html - Introduction to ARM thumb
http://www.eetimes.com/discussion/other/4024632/Introduction-to-ARM-thumb - ARM, Thumb, and ThumbEE instruction sets
http://www.keil.com/support/man/docs/armasm/armasm_CEGBEIJB.htm - An Introduction to ARM Assembly Language
http://dev.emcelettronica.com/introduction-to-arm-assembly-language - Processors – ARM
http://www.arm.com/products/processors/index.php - The ARM Instruction Set
http://simplemachines.it/doc/arm_inst.pdf - The Thumb instruction set
http://apt.cs.manchester.ac.uk/ftp/pub/apt/peve/PEVE05/Slides/05_Thumb.pdf - Why Learn Assembly Language?
http://www.codeproject.com/Articles/89460/Why-Learn-Assembly-Language - Is Assembly still relevant?
http://programmers.stackexchange.com/questions/95836/is-assembly-still-relevant - Why Learning Assembly Language Is Still a Good Idea
http://www.onlamp.com/pub/a/onlamp/2004/05/06/writegreatcode.html - Assembly language today
http://beust.com/weblog/2004/06/23/assembly-language-today/ - Assembler: Význam assembleru dnes
http://www.builder.cz/rubriky/assembler/vyznam-assembleru-dnes-155960cz - Assembler pod Linuxem
http://phoenix.inf.upol.cz/linux/prog/asm.html - AT&T Syntax versus Intel Syntax
https://www.sourceware.org/binutils/docs-2.12/as.info/i386-Syntax.html - Linux Assembly website
http://asm.sourceforge.net/