Překlad funkcí přímo do nativního kódu MicroPythonem

6. 2. 2024
Doba čtení: 28 minut

Sdílet

Ilustrační snímek Autor: Depositphotos
Ilustrační snímek
MicroPython, s jehož podporou pro zápis strojových instrukcí ze sad Thumb a Thumb-2 jsme se částečně seznámili v předchozích článcích, navíc umožňuje překlad vybraných funkcí do nativního kódu a nikoli „pouze“ do bajtkódu Pythonu.

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

4. Použití překladače Viper

5. 32bitová aritmetika pro co nejvyšší výpočetní výkon

6. Rychlost výpočtů – programové smyčky

7. Výsledky benchmarků

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í

11. Výsledky benchmarků

12. Jak vlastně vypadají funkce přeložené do nativního kódu?

13. Strojový kód funkce add

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

19. Odkazy na Internetu

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.

Poznámka: dnes se zaměříme na MicroPython provozovaný na 32bitových mikrořadičích (s různými architekturami, typicky se ovšem jedná o architektury ARM). Ovšem na tomto místě je vhodné upozornit na to, že MicroPython lze provozovat i na běžných PC (což může být pro některé projekty užitečné, a to přesto, že se nejedná o jazyk plně kompatibilní s posledním verzemi CPythonu). Zde se ovšem budou výsledky benchmarků lišit (nejenom v absolutních číslech, ale i při porovnání).

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:

  1. 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í.
  2. 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.
  3. 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.
Poznámka: někdy se výše uvedené nástroje nazývají code emmiter(s), protože se jedná pouze o jeden z kroků překladu.

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
Poznámka: ve skutečnosti je funkce foo mnohem obecnější, protože kromě číselných hodnot akceptuje i některé další hodnoty, pro které je definován operátor +. O tom se ostatně můžeme velmi snadno přesvědčit:
>>> 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
Poznámka: tato funkce bude po svém zavolání provedena nejrychleji, ovšem je nutné mít na paměti, že její chování není kompatibilní s CPythonem (což ale v těchto konkrétních případech nemusí být na škodu).

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á.

Poznámka: samozřejmě nás zajímají relativní poměry mezi výsledky a nikoli absolutní hodnoty. Ty totiž závisí na rychlosti použitého mikrořadiče.
Poznámka2: jen na okraj – výsledky benchmarků jsou velmi stabilní a při každém běhu dostaneme prakticky stejné hodnoty. To je výhoda malých mikrořadičů bez plnohodnotného operačního systému s multitaskingem a mnoha na pozadí běžícími úlohami. Navíc je architektura mikrořadičů poměrně dobře predikovatelná, na rozdíl od mikroprocesorů pro PC, které mohou úlohu předávat mezi výkonnými a „zelenými“ jádry, přiškrcovat frekvenci při zahřátí CPU a provádět další operace, které výsledky benchmarků mohou zkreslit (někdy i dost významně).

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))
Poznámka: doba běhu této funkce se stále pohybuje v hodnotách 236 milisekund na použitém mikrořadiči.

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
Poznámka: u takto krátkých funkcí, u nichž se více času stráví samotným voláním funkce a předáváním argumentů, se pravděpodobně překlad do nativního kódu nemusí vyplácet.

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/mi­cropython/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----------")
Poznámka: sice se jedná o podobnou funkci, jakou jsme použili při zkoumání strojového kódu u funkcí označených dekorátorem @micropython.thumb, ovšem bylo v něm provedeno několik změ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}
Poznámka: instrukce BLX provádí volání jiné funkce (resp. subrutiny). Zde se konkrétně jedná o volání subrutin, jejichž adresa je uložena v pracovním registru R3.

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}
Poznámka: v žádném případě se tedy nejedná o optimální kód; a přesto je takto realizovaný výpočet rychlejší, než interpretace bajtkódu.

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:

  1. x86 (32 bit)
  2. x64 (64 bit)
  3. armv6m (Thumb)
  4. armv7m (Thumb-2)
  5. armv7emsp (Thumb-2, single precision float)
  6. armv7emdp (Thumb-2, double precision float)
  7. xtensa (ESP8266)
  8. 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.

ict ve školství 24

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:

# Demonstrační příklad Stručný popis příkladu Cesta
1 loop1.py vnořené programové smyčky, překládáno do bajtkódu Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/loop1.py
2 loop2.py vnořené programové smyčky, překládáno do nativního kódu https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/loop2.py
3 loop3.py vnořené programové smyčky, překládáno do nativního kódu Viperem https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/loop3.py
4 loop4.py problematika návratové hodnoty z funkce překládané Viperem https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/loop4.py
5 loop5.py typové informace přidané do překládané funkce https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/loop5.py
       
6 add1.py funkce překládaná do bajtkódu Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/add1.py
7 add2.py funkce překládaná do nativního kódu https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/add2.py
8 add3.py funkce překládaná do nativního kódu Viperem https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/add3.py
9 add4.py přidání typových informací, překlad Viperem https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/add4.py
10 add5.py varianta naprogramovaná přímo v instrukcích Thumb https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/add5.py
       
11 foo1.py součet dvou hodnot blízko 231, Pythonní varianta https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/foo1.py
12 foo2.py stejná funkce, ovšem přeložená do nativního kódu https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/foo2.py
13 foo3.py stejná funkce, ovšem přeložená do nativního kódu Viperem https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/foo3.py
14 foo4.py stejná funkce, ovšem přeložená do nativního kódu Viperem, přidání informací o datových typech https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/foo4.py
       
15 type_int.py explicitní použití datového typu int pro návratovou hodnotu funkce https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/type_int.py
16 type_uint.py explicitní použití datového typu uint pro návratovou hodnotu funkce https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/type_uint.py
       
17 inspect_function.py získání strojového kódu funkce přeložené Viperem https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-viper/inspect_function.py

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ů:

# Demonstrační příklad Stručný popis příkladu Cesta
1 return_constant.py návratová hodnota z funkce s Thumb instrukcemi https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/return_constant.py
2 return_big_constant1.py pokus o vrácení příliš velké konstanty (nekorektní) https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/return_big_constant1.py
3 return_big_constant2.py pokus o vrácení příliš velké konstanty (nekorektní na Cortex-M0+) https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/return_big_constant2.py
4 return_big_constant3.py vrácení 32bitové konstanty https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/return_big_constant3.py
       
5 inc1.py předání argumentu do funkce s Thumb instrukcemi https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/inc1.py
6 inc2.py pokus o předání argumentu v parametru špatného jména https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/inc2.py
       
7 add.py součet dvou předaných argumentů https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/add.py
8 add_four.py součet čtyř předaných argumentů (nekorektní) https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/add_four.py
9 add_five.py součet pěti předaných argumentů https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/add_five.py
       
10 inspect_function.py získání strojového kódu přeložených funkcí https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/inspect_function.py
11 no_op.py funkce bez příkazů, která se má přeložit do strojového kódu https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/no_op.py
12 branch1.py využití instrukce nepodmíněného skoku https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/branch1.py
13 branch2.py skok dopředu o několik instrukcí https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/branch2.py
14 branch3.py skok vzad https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/branch3.py
15 loop1.py programová smyčka s počitadlem a podmíněným skokem https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/loop1.py
16 loop2.py zjednodušená varianta programové smyčky https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/loop2.py
17 loop3.py vnořené programové smyčky https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/loop3.py
18 loop4.py benchmark: vnořené smyčky naprogramované v assembleru https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/loop4.py
19 loop5.py benchmark: vnořené smyčky naprogramované v Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/loop5.py
       
20 inspect_function2.py získání strojového kódu přeložených funkcí https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/inspect_function2.py
21 loop6.py programová smyčka s testem na začátku https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/loop6.py
22 loop7.py programová smyčka s testem na začátku (další varianta) https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/loop7.py
23 mul.py instrukce součinu https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/mul.py
24 loop_mul1.py benchmark: rychlost násobení (strojové instrukce) https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/loop_mul1.py
25 loop_mul2.py benchmark: rychlost násobení (Python) https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/loop_mul2.py
26 ldrb.py načtení jediného bajtu s rozšířením hodnoty na 32 bitů https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/ldrb.py
27 ldrh.py načtení jediného 16bitového slova s rozšířením hodnoty na 32 bitů https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/ldrh.py
       
28 and.py ukázka použití strojové instrukce AND https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/and.py
29 or.py ukázka použití strojové instrukce OR https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/or.py
30 bic.py ukázka použití strojové instrukce BIC https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/bic.py
31 mvn.py ukázka použití strojové instrukce MVN https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/mvn.py
32 lsl.py ukázka použití strojové instrukce LSL https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/lsl.py
33 lsr.py ukázka použití strojové instrukce LSR https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/lsr.py
34 asr.py ukázka použití strojové instrukce ASR https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/asr.py
35 ror.py ukázka použití strojové instrukce ROR https://github.com/tisnik/most-popular-python-libs/blob/master/micropython-thumb/ror.py

19. Odkazy na Internetu

  1. Maximising MicroPython speed
    https://docs.micropython.or­g/en/latest/reference/spe­ed_python.html
  2. Online ARM converter
    https://armconverter.com/?disasm
  3. The 3 different code emitters
    https://www.kickstarter.com/pro­jects/214379695/micro-python-python-for-microcontrollers/posts/664832
  4. The 3 different code emitters, part 2
    https://www.kickstarter.com/pro­jects/214379695/micro-python-python-for-microcontrollers/posts/665145
  5. Fast Filters for the Pyboard
    https://github.com/peterhin­ch/micropython-filters
  6. How to load 32 bit constant from assembler with @micropython.asm_thumb
    https://forum.micropython­.org/viewtopic.php?f=21&t=12931&sid=25­de8871fa9cfcf8cafb6318f9d8ba3a
  7. Pi pico, micropython.asm_thumb: ADR Rd, <label> and LDR Rd, <label> not implemented?
    https://github.com/orgs/mi­cropython/discussions/12257
  8. MicroPython documentation
    https://docs.micropython.or­g/en/latest/index.html
  9. Inline assembler for Thumb2 architectures
    https://docs.micropython.or­g/en/latest/reference/asm_thum­b2_index.html
  10. Inline assembler in MicroPython
    https://docs.micropython.or­g/en/latest/pyboard/tutori­al/assembler.html#pyboard-tutorial-assembler
  11. MCU market turns to 32-bits and ARM
    http://www.eetimes.com/do­cument.asp?doc_id=1280803
  12. Cortex-M0 Processor (ARM Holdings)
    http://www.arm.com/produc­ts/processors/cortex-m/cortex-m0.php
  13. Cortex-M0+ Processor (ARM Holdings)
    http://www.arm.com/produc­ts/processors/cortex-m/cortex-m0plus.php
  14. ARM Processors in a Mixed Signal World
    http://www.eeweb.com/blog/arm/arm-processors-in-a-mixed-signal-world
  15. RISCové mikroprocesory s komprimovanými instrukčními sadami
    https://www.root.cz/clanky/riscove-mikroprocesory-s-komprimovanymi-instrukcnimi-sadami/
  16. RISCové mikroprocesory s komprimovanými instrukčními sadami (2)
    https://www.root.cz/clanky/riscove-mikroprocesory-s-komprimovanymi-instrukcnimi-sadami-2/
  17. ARM Architecture (Wikipedia)
    https://en.wikipedia.org/wi­ki/ARM_architecture
  18. Cortex-M0 (Wikipedia)
    https://en.wikipedia.org/wi­ki/ARM_Cortex-M0
  19. Cortex-M0+ (Wikipedia)
    https://en.wikipedia.org/wi­ki/ARM_Cortex-M#Cortex-M0.2B
  20. Improving ARM Code Density and Performance
    New Thumb Extensions to the ARM Architecture Richard Phelan
  21. The ARM Processor Architecture
    http://www.arm.com/produc­ts/processors/technologies/in­struction-set-architectures.php
  22. Thumb-2 instruction set
    http://infocenter.arm.com/hel­p/index.jsp?topic=/com.ar­m.doc.ddi0344c/Beiiegaf.html
  23. Introduction to ARM thumb
    http://www.eetimes.com/dis­cussion/other/4024632/Intro­duction-to-ARM-thumb
  24. ARM, Thumb, and ThumbEE instruction sets
    http://www.keil.com/suppor­t/man/docs/armasm/armasm_CEG­BEIJB.htm
  25. An Introduction to ARM Assembly Language
    http://dev.emcelettronica­.com/introduction-to-arm-assembly-language
  26. Processors – ARM
    http://www.arm.com/produc­ts/processors/index.php
  27. The ARM Instruction Set
    http://simplemachines.it/doc/ar­m_inst.pdf
  28. The Thumb instruction set
    http://apt.cs.manchester.ac­.uk/ftp/pub/apt/peve/PEVE05/Sli­des/05_Thumb.pdf
  29. Why Learn Assembly Language?
    http://www.codeproject.com/Ar­ticles/89460/Why-Learn-Assembly-Language
  30. Is Assembly still relevant?
    http://programmers.stackex­change.com/questions/95836/is-assembly-still-relevant
  31. Why Learning Assembly Language Is Still a Good Idea
    http://www.onlamp.com/pub/a/on­lamp/2004/05/06/writegreat­code.html
  32. Assembly language today
    http://beust.com/weblog/2004/06/23/as­sembly-language-today/
  33. Assembler: Význam assembleru dnes
    http://www.builder.cz/rubri­ky/assembler/vyznam-assembleru-dnes-155960cz
  34. Assembler pod Linuxem
    http://phoenix.inf.upol.cz/li­nux/prog/asm.html
  35. AT&T Syntax versus Intel Syntax
    https://www.sourceware.or­g/binutils/docs-2.12/as.info/i386-Syntax.html
  36. Linux Assembly website
    http://asm.sourceforge.net/
ikonka

Zajímá vás toto téma? Chcete se o něm dozvědět víc?

Objednejte si upozornění na nově vydané články do vašeho mailu. Žádný článek vám tak neuteče.

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.