Obsah
1. Nuitka: pokročilý AOT překladač jazyka Python
4. Překlad jednoduchého skriptu typu „Hello world“
5. Prozkoumání výsledku překladu
6. Překlad a slinkování za účelem přenosu aplikace na jiný počítač
7. Výsledný spustitelný soubor a jeho závislosti
8. Překlad se slinkováním potřebných knihoven do jediného souboru
9. Aplikace využívající knihovnu Tkinter pro grafické uživatelské rozhraní
10. Překlad projektu tak, aby výsledkem byl jediný spustitelný soubor
11. Výsledný spustitelný soubor a jeho závislosti
12. Porovnání kvality překladu pomocí nástroje Nuitka s dalšími technologiemi
15. Výsledky benchmarků v tabulkové podobě
16. Vizualizace výsledků benchmarků
17. Porovnání výsledků benchmarků: interpretry
18. Porovnání výsledků benchmarků: překladače
19. Příloha: skript pro vytištění grafů s výsledky benchmarků
1. Nuitka: pokročilý AOT překladač jazyka Python
Na stránkách Roota jsme se již setkali s několika AOT (Ahead of Time) překladači jazyka Python. Tyto překladače většinou transformují (transpilují) skript či celý projekt napsaný v Pythonu do jazyka C a poté ho nechají přeložit překladačem céčka, typicky s povolenými optimalizacemi. Připomeňme si, že mezi tyto projekty patří Cython (ostatně před pár dny jsme si ho připomenuli) a mypyc (ovšem částečně i Numba). Dnes se zaměříme na projekt, který se jmenuje Nuitka. Opět se jedná o AOT překladač a jeho výhodou je, že v případě potřeby dokáže celou aplikaci přeložit do jediného spustitelného souboru, který je přenositelný na další počítače (které ani nemusí obsahovat Python a dokonce například ani některé knihovny pro GUI atd.).
Obrázek 1: Logo projektu Nuitka.
Na závěr si tento projekt porovnáme se všemi ostatními AOT i JIT překladači Pythonu, a to opět na mém oblíbeném benchmarku založeném na výpočtu Mandelbrotovy množiny. Zjistíme tak, kdy může být výhodnější použít JIT typu Numba a kdy AOT překladač.
2. Instalace balíčku Nuitka
Balíček Nuitka je, ostatně podobně jako většina užitečných balíčků pro jazyk Python, dostupná na PyPi, takže je jeho instalace velmi snadná. Pro jednoduchost použiji nástroj pip, i když lze pochopitelně použít i PDM či další podobné nástroje:
$ pip3 install nuitka
Průběh instalace (povšimněte si, že Nuitka je relativně malý balíček):
Defaulting to user installation because normal site-packages is not writeable Collecting nuitka Downloading Nuitka-2.2.3.tar.gz (3.7 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.7/3.7 MB 4.3 MB/s eta 0:00:00 Installing build dependencies ... done Getting requirements to build wheel ... done Preparing metadata (pyproject.toml) ... done Requirement already satisfied: ordered-set>=4.1.0 in /home/ptisnovs/.local/lib/python3.11/site-packages (from nuitka) (4.1.0) Requirement already satisfied: zstandard>=0.15 in /home/ptisnovs/.local/lib/python3.11/site-packages (from nuitka) (0.22.0) Building wheels for collected packages: nuitka Building wheel for nuitka (pyproject.toml) ... done Created wheel for nuitka: filename=Nuitka-2.2.3-cp311-cp311-linux_x86_64.whl size=3277344 sha256=dd58819d552c3cfd2e744848cd5a9676b2820a36d3d202560bc7ef0e8f41ac6d Stored in directory: /home/ptisnovs/.cache/pip/wheels/d8/48/83/b339d592b2bf8ae0d73c8fe6dd14dca269b691157696c3d7bc Successfully built nuitka Installing collected packages: nuitka Successfully installed nuitka-2.2.3
3. Kontrola instalace
Pro rychlé otestování, zda je balíček nainstalován korektně, postačuje spustit modul nuitka, například s parametrem –version:
$ python -m nuitka --version 2.2.3 Commercial: None Python: 3.11.8 (main, Feb 7 2024, 00:00:00) [GCC 13.2.1 20231011 (Red Hat 13.2.1-4)] Flavor: Fedora Python Executable: /usr/bin/python OS: Linux Arch: x86_64 Distribution: Fedora 38 Version C compiler: /usr/bin/gcc (gcc 13).
4. Překlad jednoduchého skriptu typu „Hello world“
Praktickou část dnešního článku zahájíme triviálním skriptem typu „Hello world“, který může v Pythonu vypadat následovně:
def say_hello(): print("Hello world!") say_hello()
Základní způsob překladu tohoto skriptu (tedy vlastně „projektu“) do spustitelného nativního kódu se provede takto:
$ python -m nuitka hello.py
Následně proběhne překlad, který bude trvat několik sekund (na starším HW pravděpodobně i desítek sekund!):
Nuitka-Options: Used command line options: hello.py Nuitka-Options:WARNING: You did not specify to follow or include anything but main program. Check options and make sure that is Nuitka-Options:WARNING: intended. Nuitka: Starting Python compilation with Nuitka '2.2.3' on Python '3.11' commercial grade 'not installed'. Nuitka: Completed Python level compilation and optimization. Nuitka: Generating source code for C backend compiler. Nuitka: Running data composer tool for optimal constant value handling. Nuitka: Running C compilation via Scons. Nuitka-Scons: Backend C compiler: gcc (gcc 13). Nuitka-Scons: Backend linking program with 6 files (no progress information available for this stage). Nuitka-Scons:WARNING: You are not using ccache, re-compilation of identical code will be slower than necessary. Use your OS package Nuitka-Scons:WARNING: manager to install it. Nuitka: Keeping build directory 'hello.build'. Nuitka: Successfully created 'hello.bin'.
Výsledkem je spustitelný (nativní) program, který si skutečně spustíme:
$ ./hello.bin
Výsledkem by měla být pochopitelně tato zpráva:
Hello world!
$ python -m nuitka hello.py
Výsledky změřené na stroji s procesorem i7–1270P (počet jader nehraje zásadní roli) ukazují, že i pro několikařádkový skript si chvíli počkáme:
real 0m3.062s user 0m5.044s sys 0m0.332s
5. Prozkoumání výsledku překladu
Prozkoumejme ještě blíže soubor hello.bin, který byl vytvořen překladačem Nuitka. Nejprve si ověříme, že se skutečně jedná o spustitelný soubor s nativním kódem:
$ file hello.bin hello.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=96c80b076b56dbdf3da519fdbc564294288a6b8d, for GNU/Linux 3.2.0, stripped
Velikost tohoto souboru není na dnešní poměry nijak závratná (i když například FASM přeloží sémanticky stejný program do 231 bajtů):
$ ls -l hello.bin -rwxr-xr-x. 1 ptisnovs ptisnovs 251216 May 20 13:46 hello.bin
Ovšem velikost spustitelného souboru není zdaleka vše, protože ještě záleží, jaké knihovny se musí načíst v průběhu inicializace. Takže si zjistěme, o jaké knihovny se jedná:
$ ldd hello.bin
Seznam dynamicky linkovaných knihoven:
linux-vdso.so.1 (0x00007ffe87df4000) libm.so.6 => /lib64/libm.so.6 (0x00007f763cafb000) libpython3.11.so.1.0 => /lib64/libpython3.11.so.1.0 (0x00007f763c400000) libc.so.6 => /lib64/libc.so.6 (0x00007f763c222000) /lib64/ld-linux-x86-64.so.2 (0x00007f763cbf2000)
Zde již není vše tak růžové, jak by se mohlo zdát, protože je zapotřebí celá runtime knihovna libpython3.11.so, a ta již objemná je:
$ ls -lh /lib64/libpython3.11.so.1.0 -rwxr-xr-x. 1 root root 5.2M Feb 7 01:00 /lib64/libpython3.11.so.1.0
6. Překlad a slinkování za účelem přenosu aplikace na jiný počítač
Překladač Nuitka dokáže překlad provést i takovým způsobem, že se vytvoří adresář se všemi potřebnými soubory, které lze v případě potřeby následně přenést na jiný počítač, na němž ani nemusí být nainstalován Python – takže lze Nuitku použít i pro tvorbu instalačních balíčků. Dokonce existuje několik způsobů, jak takový překlad provést. Ukažme si nejprve použití přepínače –standalone:
$ python -m nuitka --standalone hello.py
Překlad nyní bude trvat poněkud delší dobu:
Nuitka-Options: Used command line options: --standalone hello.py Nuitka: Starting Python compilation with Nuitka '2.2.3' on Python '3.11' commercial grade 'not installed'. Nuitka: Completed Python level compilation and optimization. Nuitka: Generating source code for C backend compiler. Nuitka: Running data composer tool for optimal constant value handling. Nuitka: Running C compilation via Scons. Nuitka-Scons: Backend C compiler: gcc (gcc 13). Nuitka-Scons: Backend linking program with 7 files (no progress information available for this stage). Nuitka-Scons:WARNING: You are not using ccache, re-compilation of identical code will be slower than necessary. Use your OS package Nuitka-Scons:WARNING: manager to install it. Nuitka: Keeping build directory 'hello.build'. Nuitka: Successfully created 'hello.dist/hello.bin'.
Rychlost překladu si můžeme opět ověřit utilitkou time. Povšimněte si, že se skutečně doba překladu (a slinkování) poměrně razantně zvýšila:
real 0m12.592s user 0m12.609s sys 0m0.477s
Výsledkem nyní ovšem není pouze jeden spustitelný soubor, ale celý adresář určený pro přenos/instalaci na další počítač:
binascii.so _codecs_cn.so _codecs_hk.so _codecs_iso2022.so _codecs_jp.so _codecs_kr.so _codecs_tw.so _datetime.so fcntl.so hello.bin libpython3.11.so.1.0 math.so _multibytecodec.so _pickle.so _random.so _sha512.so _struct.so _typing.so unicodedata.so zlib.so
7. Výsledný spustitelný soubor a jeho závislosti
Spustitelný soubor vzniklý překladem je již mnohem větší:
$ ls -lh hello.bin -rwxr-xr-x. 1 ptisnovs ptisnovs 5.4M May 21 19:23 hello.bin
Nicméně stále je vyžadována knihovna libpython3.11.so, tentokrát ovšem načítaná nikoli ze systémové cesty, ale z místa, kde byl vytvořen adresář s instalačními soubory:
$ ldd hello.bin
linux-vdso.so.1 (0x00007ffdba5e1000) libm.so.6 => /lib64/libm.so.6 (0x00007f8fb1064000) libpython3.11.so.1.0 => /tmp/ramdisk/hello.dist/./libpython3.11.so.1.0 (0x00007f8fb0b02000) libc.so.6 => /lib64/libc.so.6 (0x00007f8fb0924000) /lib64/ld-linux-x86-64.so.2 (0x00007f8fb115b000)
8. Překlad se slinkováním potřebných knihoven do jediného souboru
Mnohem užitečnější (pro potřeby instalace) je však přepínač –onefile, který provádí operaci odpovídající jeho jménu. Vytvoří tedy spustitelný soubor, který závisí jen na systémových dynamicky linkovaných knihovnách, ale nikoli na knihovnách Pythonu:
$ python -m nuitka --onefile hello.py
Opět si počkáme, v mém konkrétním případě cca 17 sekund (to je již dost):
Nuitka-Options: Used command line options: --onefile hello.py Nuitka: Starting Python compilation with Nuitka '2.2.3' on Python '3.11' commercial grade 'not installed'. Nuitka: Completed Python level compilation and optimization. Nuitka: Generating source code for C backend compiler. Nuitka: Running data composer tool for optimal constant value handling. Nuitka: Running C compilation via Scons. Nuitka-Scons: Backend C compiler: gcc (gcc 13). Nuitka-Scons: Backend linking program with 7 files (no progress information available for this stage). Nuitka-Scons:WARNING: You are not using ccache, re-compilation of identical code will be slower than necessary. Use your OS package Nuitka-Scons:WARNING: manager to install it. Nuitka-Postprocessing: Creating single file from dist folder, this may take a while. Nuitka-Onefile: Running bootstrap binary compilation via Scons. Nuitka-Scons: Onefile C compiler: gcc (gcc 13). Nuitka-Scons: Onefile linking program with 1 files (no progress information available for this stage). Nuitka-Scons:WARNING: You are not using ccache, re-compilation of identical code will be slower than necessary. Use your OS package Nuitka-Scons:WARNING: manager to install it. Nuitka-Onefile: Using compression for onefile payload. Nuitka-Onefile: Onefile payload compression ratio (28.09%) size 13711475 to 3851813. Nuitka-Onefile: Keeping onefile build directory 'hello.onefile-build'. Nuitka: Keeping dist folder 'hello.dist' for inspection, no need to use it. Nuitka: Keeping build directory 'hello.build'. Nuitka: Successfully created 'hello.bin'.
Čas:
real 0m17.945s user 0m17.655s sys 0m0.811s
Velikost výsledného souboru nyní dosahuje přibližně 4MB:
$ ls -lh hello.bin -rwxr-xr-x. 1 ptisnovs ptisnovs 3.9M May 21 19:32 hello.bin
A nyní je program spustitelný a závislý pouze na základních systémových knihovnách:
$ ldd hello.bin linux-vdso.so.1 (0x00007ffd1dbe5000) libc.so.6 => /lib64/libc.so.6 (0x00007f8a23d29000) /lib64/ld-linux-x86-64.so.2 (0x00007f8a23f1d000)
9. Aplikace využívající knihovnu Tkinter pro grafické uživatelské rozhraní
V dalším kroku si vyzkoušíme překlad nepatrně složitějšího projektu, který ale ukáže, jak Nuitka pracuje se závislými knihovnami. Bude se jednat o projekt založený na knihovně Tkinter a budeme vyžadovat, aby výsledný spustitelný soubor byl přenositelný i na počítač bez Pythonu, Tk i Tkinteru.
Obrázek 2: Takto vypadá okno po spuštění našeho demonstračního projektu.
Zdrojový kód našeho projektu (či spíše „projektu“) vypadá následovně:
import tkinter from tkinter import ttk import sys WIDTH = 400 HEIGHT = 400 GRID_SIZE = 100 def exit(): sys.exit(0) def basic_canvas(root, width, height, grid_size): canvas = tkinter.Canvas(root, width=width, height=height, background="#ccffcc") canvas.pack() draw_grid(canvas, width, height, grid_size) return canvas def draw_grid(canvas, width, height, grid_size): for x in range(0, width, grid_size): canvas.create_line(x, 0, x, height, dash=7, fill="gray") for y in range(0, height, grid_size): canvas.create_line(0, y, width, y, dash=7, fill="gray") root = tkinter.Tk() canvas = basic_canvas(root, WIDTH, HEIGHT, GRID_SIZE) canvas.create_line(0, 0, 100, 100, fill="red", width=2, dash=8) canvas.create_arc( 100, 1, 200, 100, outline="blue", start=45, extent=180, style=tkinter.ARC, width=2 ) canvas.create_oval(200, 1, 300, 100) canvas.create_oval(325, 25, 375, 75, fill="#a0a0ff") canvas.create_rectangle(50, 125, 150, 175, fill="#a0a0ff") canvas.create_text(300, 150, text="Hello world!", font="Helvetica 20") canvas.create_polygon(50, 225, 200, 300, 50, 375, fill="#80ff80") canvas.create_polygon( 250, 225, 400, 300, 250, 375, fill="black", outline="red", width="5" ) root.mainloop()
10. Překlad projektu tak, aby výsledkem byl jediný spustitelný soubor
Pokusme se nyní výše uvedený projekt přeložit takovým způsobem, aby výsledkem byl jediný spustitelný (a přenositelný i instalovatelný) soubor, který nebude vyžadovat další nesystémové knihovny. Opět použijeme přepínač –onefile, který již známe:
$ python -m nuitka --onefile objects_on_canvas.py
Nyní ovšem překlad skončí s varováním, že knihovna Tkinter (ta se stará o grafické uživatelské rozhraní, včetně zpracování událostí atd.) závisí na ekosystému jazyka TCL a aby byl do výsledku zahrnut i tento jazyk, musíme použít ještě jednu volbu –enable-plugin=tk-inter:
Nuitka-Options: Used command line options: --onefile objects_on_canvas.py Nuitka: Starting Python compilation with Nuitka '2.2.3' on Python '3.11' commercial grade 'not installed'. Nuitka-Plugins:WARNING: Use '--enable-plugin=tk-inter' for: Tkinter needs TCL included.
Proto tedy navrhovanou volbu použijeme:
$ python -m nuitka --onefile --enable-plugin=tk-inter objects_on_canvas.py
Průběh překladu (nyní si skutečně musíme počkat delší dobu):
Nuitka-Options: Used command line options: --onefile --enable-plugin=tk-inter objects_on_canvas.py Nuitka: Starting Python compilation with Nuitka '2.2.3' on Python '3.11' commercial grade 'not installed'. Nuitka-Plugins:anti-bloat: Not including '_bisect' automatically in order to avoid bloat, but this may cause: may slow down by using Nuitka-Plugins:anti-bloat: fallback implementation. Nuitka-Plugins:anti-bloat: Not including 'socket' automatically in order to avoid bloat, but this may cause: can break calls of Nuitka-Plugins:anti-bloat: 'email.utils.make_msgid()'. Nuitka: Completed Python level compilation and optimization. Nuitka: Generating source code for C backend compiler. Nuitka: Running data composer tool for optimal constant value handling. Nuitka: Running C compilation via Scons. Nuitka-Scons: Backend C compiler: gcc (gcc 13). Nuitka-Scons: Backend linking program with 8 files (no progress information available for this stage). Nuitka-Scons:WARNING: You are not using ccache, re-compilation of identical code will be slower than necessary. Use your OS package Nuitka-Scons:WARNING: manager to install it. Nuitka-Plugins:tk-inter: Included 85 data files due to Tk needed for tkinter usage. Nuitka-Plugins:tk-inter: Included 222 data files due to Tcl needed for tkinter usage. Nuitka-Postprocessing: Creating single file from dist folder, this may take a while. Nuitka-Onefile: Running bootstrap binary compilation via Scons. Nuitka-Scons: Onefile C compiler: gcc (gcc 13). Nuitka-Scons: Onefile linking program with 1 files (no progress information available for this stage). Nuitka-Scons:WARNING: You are not using ccache, re-compilation of identical code will be slower than necessary. Use your OS package Nuitka-Scons:WARNING: manager to install it. Nuitka-Onefile: Using compression for onefile payload. Nuitka-Onefile: Onefile payload compression ratio (30.61%) size 36614803 to 11208588. Nuitka-Onefile: Keeping onefile build directory 'objects_on_canvas.onefile-build'. Nuitka: Keeping dist folder 'objects_on_canvas.dist' for inspection, no need to use it. Nuitka: Keeping build directory 'objects_on_canvas.build'. Nuitka: Successfully created 'objects_on_canvas.bin'.
real 0m27.803s user 0m27.476s sys 0m1.111s
11. Výsledný spustitelný soubor a jeho závislosti
Opět se podívejme na to, jaké knihovny jsou nutné pro spuštění aplikace s grafickým uživatelským rozhraním. Nyní se jedná pouze o základní systémové knihovny, protože všechny ostatní knihovny jsou součástí výsledného „distribučního souboru“:
$ ldd objects_on_canvas.bin linux-vdso.so.1 (0x00007ffd20b98000) libc.so.6 => /lib64/libc.so.6 (0x00007f0472b3a000) /lib64/ld-linux-x86-64.so.2 (0x00007f0472d2e000)
Zajímat nás bude taktéž celková velikost výsledného souboru vytvořeného nástrojem Nuitka:
$ ls -la objects_on_canvas.bin
Jedná se o cca 11MB velký soubor:
rwxr-xr-x. 1 ptisnovs ptisnovs 11M May 21 19:45 objects_on_canvas.bin
Mimochodem, aplikace používá tyto slinkované knihovny:
libb2.so.1 libbrotlicommon.so.1 libbrotlidec.so.1 libbz2.so.1 libcrypto.so.3 libfontconfig.so.1 libfreetype.so.6 libgcc_s.so.1 libglib-2.0.so.0 libgomp.so.1 libgraphite2.so.3 libharfbuzz.so.0 liblzma.so.5 libmpdec.so.3 libpcre2-8.so.0 libpng16.so.16 libpython3.11.so.1.0 libtcl8.6.so libtk8.6.so libX11.so.6 libXau.so.6 libxcb.so.1 libXft.so.2 libxml2.so.2 libXrender.so.1
12. Porovnání kvality překladu pomocí nástroje Nuitka s dalšími technologiemi
Ve druhé části dnešního článku si porovnáme kvalitu překladu nástrojem Nuitka s dalšími technologiemi, které jsme si již popsali. Konkrétně budeme měřit dobu výpočtu Mandelbrotovy množiny pro postupně se zvyšující rozlišení výsledného obrázku. Připomeňme si, že tento výpočet obsahuje vnořené smyčky a podmíněné bloky, pracuje s poli (ty jsou simulovány seznamem či n-ticí) a výpočty jsou prováděny s typem float. Jedná se tedy o program, na kterém se mohou optimalizační algoritmy dobře „vyřádit“.
Obrázek 3: Výsledek benchmarků by měl být totožný s tímto obrázkem.
13. Porovnávané technologie
V benchmarcích budeme porovnávat tyto technologie:
- Výpočet realizovaný céčkovským kódem
- Bez optimalizací (-O0)
- S optimalizacemi (-Ofast)
- Výpočet realizovaný standardním interpretrem Pythonu
- Python 3.8
- Python 3.9
- Python 3.10
- Python 3.11 (zde byly provedeny interní optimalizace)
- Python 3.12 (teoreticky by měl být nejrychlejší)
- Výpočet realizovaný kódem po AOT překladu pomocí mypyc
- Bez typových informací
- S typovými informacemi
- Výpočet realizovaný AOT překladačem Nuitka
- Výpočet realizovaný AOT překladačem CPython
- Základní varianta (v podstatě původní Pythonovský kód)
- S typovými informacemi
- S optimalizacemi + použitím nogil
- Výpočet realizovaný JIT překladačem Numba
- Základní varianta (v podstatě původní Pythonovský kód)
- S uvedením dekorátoru @jit
- S optimalizacemi + použitím nativní funkce print
14. Zdrojové kódy benchmarků
# | Benchmark | Stručný popis | Adresa |
---|---|---|---|
1 | mandelbrot-v1 | benchmark, v němž se nepoužívají anotace projektu Numba (čistý CPython) | https://github.com/tisnik/most-popular-python-libs/blob/master/numba/mandelbrot-v1/ |
2 | mandelbrot-v2 | použití anotace @jit (Numba) ve funkci, v níž se provádí mnoho výpočtů | https://github.com/tisnik/most-popular-python-libs/blob/master/numba/mandelbrot-v2/ |
3 | mandelbrot-v3 | volání zjednodušených variant funkce print (Numba) | https://github.com/tisnik/most-popular-python-libs/blob/master/numba/mandelbrot-v3/ |
4 | mandelbrot-v4 | použití anotace @jit s parametrem nopython | https://github.com/tisnik/most-popular-python-libs/blob/master/numba/mandelbrot-v4/ |
5 | mandelbrot-v5 | varianta benchmarku určená pro překlad s využitím mypyc | https://github.com/tisnik/most-popular-python-libs/blob/master/mypyc/mandelbrot5.py |
6 | mandelbrot-v6 | přidání typových informací využitelných AOT překladačem mypyc | https://github.com/tisnik/most-popular-python-libs/blob/master/mypyc/mandelbrot6.py |
7 | mandelbrot.c | varianta benchmarku naprogramovaná v ANSI C | https://github.com/tisnik/most-popular-python-libs/blob/master/mypyc/mandelbrot.c |
8 | mandelbrot/v1_python | benchmark s výpočtem Mandelbrotovy množiny, původní varianta naprogramovaná v čistém Pythonu | https://github.com/tisnik/most-popular-python-libs/blob/master/cython/mandelbrot/v1_python |
9 | mandelbrot/v2_cython | přidání typových informací do funkce calc_mandelbrot, původní syntaxe Cythonu/Pyrexu | https://github.com/tisnik/most-popular-python-libs/blob/master/cython/mandelbrot/v2_cython |
10 | mandelbrot/v2_python | přidání typových informací do funkce calc_mandelbrot, syntaxe kompatibilní s Pythonem | https://github.com/tisnik/most-popular-python-libs/blob/master/cython/mandelbrot/v2_python |
15. Výsledky benchmarků v tabulkové podobě
V případě, že preferujete studovat výsledky benchmarků v tabulkové podobě, zde jsou:
Varianta naprogramovaná v jazyku C bez optimalizací
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.00 | 1408 |
24×24 | 0.00 | 1408 |
32×32 | 0.00 | 1408 |
48×48 | 0.00 | 1152 |
64×64 | 0.00 | 1152 |
96×96 | 0.00 | 1280 |
128×128 | 0.01 | 1280 |
192×192 | 0.01 | 1280 |
256×256 | 0.01 | 1280 |
384×384 | 0.04 | 1280 |
512×512 | 0.07 | 1280 |
768×768 | 0.16 | 1152 |
1024×1024 | 0.27 | 1152 |
1536×1536 | 0.61 | 1152 |
2048×2048 | 1.08 | 1280 |
3072×3072 | 2.45 | 1280 |
4096×4096 | 4.37 | 1152 |
Varianta naprogramovaná v jazyku C s optimalizacemi
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.00 | 1152 |
24×24 | 0.00 | 1408 |
32×32 | 0.00 | 1408 |
48×48 | 0.00 | 1408 |
64×64 | 0.00 | 1280 |
96×96 | 0.00 | 1152 |
128×128 | 0.00 | 1152 |
192×192 | 0.00 | 1408 |
256×256 | 0.01 | 1280 |
384×384 | 0.03 | 1280 |
512×512 | 0.04 | 1152 |
768×768 | 0.10 | 1152 |
1024×1024 | 0.17 | 1280 |
1536×1536 | 0.38 | 1280 |
2048×2048 | 0.69 | 1280 |
3072×3072 | 1.53 | 1280 |
4096×4096 | 2.73 | 1280 |
Základní varianta v Pythonu, použit standardní interpret verze 3.8
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.01 | 8320 |
24×24 | 0.00 | 7296 |
32×32 | 0.01 | 7424 |
48×48 | 0.01 | 7296 |
64×64 | 0.03 | 7296 |
96×96 | 0.05 | 7424 |
128×128 | 0.08 | 7296 |
192×192 | 0.18 | 7424 |
256×256 | 0.33 | 7424 |
384×384 | 0.74 | 7296 |
512×512 | 1.32 | 7296 |
768×768 | 3.01 | 7424 |
1024×1024 | 6.24 | 7296 |
1536×1536 | 17.85 | 7296 |
2048×2048 | 31.51 | 7168 |
3072×3072 | 71.13 | 7296 |
4096×4096 | 128.46 | 7168 |
Základní varianta v Pythonu, použit standardní interpret verze 3.9
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.01 | 8576 |
24×24 | 0.00 | 7424 |
32×32 | 0.01 | 7424 |
48×48 | 0.01 | 7424 |
64×64 | 0.02 | 7424 |
96×96 | 0.04 | 7424 |
128×128 | 0.08 | 7424 |
192×192 | 0.17 | 7424 |
256×256 | 0.31 | 7424 |
384×384 | 0.69 | 7552 |
512×512 | 1.24 | 7168 |
768×768 | 2.83 | 7424 |
1024×1024 | 5.86 | 7424 |
1536×1536 | 18.28 | 7424 |
2048×2048 | 32.98 | 7424 |
3072×3072 | 71.25 | 7424 |
4096×4096 | 133.52 | 7424 |
Základní varianta v Pythonu, použit standardní interpret verze 3.10
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.01 | 8448 |
24×24 | 0.00 | 7424 |
32×32 | 0.01 | 7552 |
48×48 | 0.01 | 7424 |
64×64 | 0.02 | 7552 |
96×96 | 0.05 | 7424 |
128×128 | 0.08 | 7424 |
192×192 | 0.19 | 7552 |
256×256 | 0.33 | 7424 |
384×384 | 0.76 | 7552 |
512×512 | 1.36 | 7296 |
768×768 | 3.12 | 7424 |
1024×1024 | 6.35 | 7424 |
1536×1536 | 19.68 | 7552 |
2048×2048 | 34.05 | 7552 |
3072×3072 | 77.51 | 7552 |
4096×4096 | 142.95 | 7424 |
Základní varianta v Pythonu, použit standardní interpret verze 3.11
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.01 | 10240 |
24×24 | 0.01 | 9472 |
32×32 | 0.01 | 9472 |
48×48 | 0.01 | 9216 |
64×64 | 0.02 | 9472 |
96×96 | 0.04 | 9472 |
128×128 | 0.06 | 9472 |
192×192 | 0.13 | 9472 |
256×256 | 0.23 | 9344 |
384×384 | 0.51 | 9344 |
512×512 | 0.89 | 9472 |
768×768 | 2.01 | 9472 |
1024×1024 | 3.61 | 9472 |
1536×1536 | 12.80 | 9472 |
2048×2048 | 22.79 | 9472 |
3072×3072 | 51.13 | 9472 |
4096×4096 | 90.92 | 9344 |
Základní varianta v Pythonu, použit standardní interpret verze 3.12
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.01 | 8960 |
24×24 | 0.01 | 8960 |
32×32 | 0.01 | 9216 |
48×48 | 0.01 | 9088 |
64×64 | 0.02 | 9088 |
96×96 | 0.04 | 8960 |
128×128 | 0.07 | 9088 |
192×192 | 0.13 | 9216 |
256×256 | 0.28 | 9088 |
384×384 | 0.62 | 9216 |
512×512 | 1.10 | 9216 |
768×768 | 2.48 | 9216 |
1024×1024 | 4.38 | 9216 |
1536×1536 | 13.49 | 9088 |
2048×2048 | 23.79 | 9088 |
3072×3072 | 53.36 | 9088 |
4096×4096 | 94.92 | 9088 |
JIT překladač Numba bez dekorátoru @jit
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.72 | 158292 |
24×24 | 0.68 | 158752 |
32×32 | 0.70 | 158656 |
48×48 | 0.70 | 158704 |
64×64 | 0.74 | 158644 |
96×96 | 0.79 | 158428 |
128×128 | 1.30 | 158424 |
192×192 | 1.74 | 158664 |
256×256 | 2.15 | 158532 |
384×384 | 3.38 | 158552 |
512×512 | 4.99 | 158792 |
768×768 | 9.94 | 158160 |
1024×1024 | 16.12 | 157904 |
1536×1536 | 35.90 | 158452 |
2048×2048 | 61.63 | 158504 |
3072×3072 | 141.17 | 158800 |
4096×4096 | 249.37 | 158524 |
JIT překladač Numba s použitím jednodušší varianty funkce print
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 3.04 | 266960 |
24×24 | 3.04 | 266844 |
32×32 | 3.06 | 267092 |
48×48 | 4.06 | 267108 |
64×64 | 4.67 | 267132 |
96×96 | 4.62 | 267220 |
128×128 | 4.58 | 267172 |
192×192 | 4.58 | 267036 |
256×256 | 4.58 | 267220 |
384×384 | 4.70 | 267084 |
512×512 | 4.74 | 267208 |
768×768 | 5.00 | 267080 |
1024×1024 | 5.31 | 266884 |
1536×1536 | 6.23 | 267228 |
2048×2048 | 7.58 | 267092 |
3072×3072 | 11.51 | 267096 |
4096×4096 | 17.19 | 267184 |
JIT překladač Numba s dekorátorem @jit(nopython=True)
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 3.03 | 266836 |
24×24 | 3.02 | 267100 |
32×32 | 3.95 | 267172 |
48×48 | 4.61 | 267092 |
64×64 | 4.59 | 267088 |
96×96 | 4.95 | 267220 |
128×128 | 4.65 | 267084 |
192×192 | 4.61 | 267088 |
256×256 | 4.67 | 267220 |
384×384 | 4.64 | 267088 |
512×512 | 4.73 | 267052 |
768×768 | 4.97 | 267088 |
1024×1024 | 5.29 | 267080 |
1536×1536 | 6.31 | 267216 |
2048×2048 | 7.63 | 267076 |
3072×3072 | 11.43 | 267220 |
4096×4096 | 16.89 | 267104 |
AOT překladač Mypyc, varianta bez typových informací
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.01 | 10624 |
24×24 | 0.01 | 9472 |
32×32 | 0.01 | 9472 |
48×48 | 0.02 | 9472 |
64×64 | 0.02 | 9472 |
96×96 | 0.05 | 9472 |
128×128 | 0.08 | 9472 |
192×192 | 0.17 | 9472 |
256×256 | 0.29 | 9472 |
384×384 | 0.66 | 9472 |
512×512 | 1.19 | 9472 |
768×768 | 2.87 | 9472 |
1024×1024 | 7.27 | 9472 |
1536×1536 | 16.00 | 9472 |
2048×2048 | 28.36 | 9344 |
3072×3072 | 64.32 | 9472 |
4096×4096 | 116.64 | 9472 |
AOT překladač Mypyc, varianta s typovými informacemi
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.02 | 10488 |
24×24 | 0.01 | 10356 |
32×32 | 0.01 | 10228 |
48×48 | 0.02 | 10224 |
64×64 | 0.02 | 10352 |
96×96 | 0.04 | 10352 |
128×128 | 0.07 | 10360 |
192×192 | 0.13 | 10232 |
256×256 | 0.23 | 10232 |
384×384 | 0.50 | 10364 |
512×512 | 0.91 | 10360 |
768×768 | 2.11 | 10228 |
1024×1024 | 3.83 | 10228 |
1536×1536 | 13.65 | 10360 |
2048×2048 | 24.07 | 10232 |
3072×3072 | 55.30 | 10356 |
4096×4096 | 93.65 | 10356 |
AOT překladač Nuitka
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.02 | 11776 |
24×24 | 0.02 | 11904 |
32×32 | 0.02 | 11904 |
48×48 | 0.02 | 11904 |
64×64 | 0.03 | 12032 |
96×96 | 0.04 | 11904 |
128×128 | 0.07 | 11776 |
192×192 | 0.12 | 11776 |
256×256 | 0.21 | 11904 |
384×384 | 0.48 | 12032 |
512×512 | 0.81 | 11776 |
768×768 | 1.87 | 11904 |
1024×1024 | 3.47 | 11904 |
1536×1536 | 7.63 | 11776 |
2048×2048 | 14.03 | 11776 |
3072×3072 | 31.97 | 11904 |
4096×4096 | 53.58 | 11904 |
AOT překladač Cython, základní varianta
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.01 | 9472 |
24×24 | 0.01 | 9472 |
32×32 | 0.01 | 9472 |
48×48 | 0.01 | 9600 |
64×64 | 0.02 | 9472 |
96×96 | 0.05 | 9600 |
128×128 | 0.07 | 9600 |
192×192 | 0.14 | 9600 |
256×256 | 0.27 | 9600 |
384×384 | 0.59 | 9600 |
512×512 | 1.06 | 9600 |
768×768 | 2.38 | 9600 |
1024×1024 | 4.24 | 9472 |
1536×1536 | 9.55 | 9600 |
2048×2048 | 16.62 | 9600 |
3072×3072 | 39.06 | 9472 |
4096×4096 | 69.38 | 9600 |
AOT překladač Cython, varianta s typovými informacemi
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.01 | 9472 |
24×24 | 0.01 | 9216 |
32×32 | 0.01 | 9344 |
48×48 | 0.01 | 9472 |
64×64 | 0.01 | 9472 |
96×96 | 0.01 | 9216 |
128×128 | 0.02 | 9344 |
192×192 | 0.02 | 9472 |
256×256 | 0.04 | 9472 |
384×384 | 0.09 | 9472 |
512×512 | 0.14 | 9344 |
768×768 | 0.32 | 9472 |
1024×1024 | 0.58 | 9472 |
1536×1536 | 1.31 | 9472 |
2048×2048 | 2.34 | 9472 |
3072×3072 | 5.26 | 9472 |
4096×4096 | 9.33 | 9344 |
AOT překladač Cython, plně optimalizovaná varianta
Rozlišení | Čas [s] | Paměť [kB] |
---|---|---|
16×16 | 0.01 | 9600 |
24×24 | 0.01 | 9856 |
32×32 | 0.01 | 9728 |
48×48 | 0.01 | 9856 |
64×64 | 0.01 | 9600 |
96×96 | 0.01 | 9600 |
128×128 | 0.01 | 9728 |
192×192 | 0.01 | 9600 |
256×256 | 0.02 | 9728 |
384×384 | 0.03 | 9600 |
512×512 | 0.05 | 9728 |
768×768 | 0.11 | 9728 |
1024×1024 | 0.18 | 9856 |
1536×1536 | 0.40 | 9728 |
2048×2048 | 0.71 | 9728 |
3072×3072 | 1.58 | 9600 |
4096×4096 | 2.77 | 9728 |
16. Vizualizace výsledků benchmarků
Mnohem přehlednější, než výsledky ve formě tabulky, je vizualizace benchmarků v podobě grafů:
Obrázek 4: Čas výpočtu v závislosti na rozlišení výsledného obrázku počítaného benchmarkem.
Obrázek 5: Vliv času startu aplikace. Zde vychází nejhůře Numba, protože se jedná o JIT překladač a je tedy nutné provést JIT překlad před vlastními výpočty.
Obrázek 6: Společně se zvětšujícím se rozlišením obrázku klesá vliv JIT překladu, ovšem zde (pro rozlišení do 512×512 pixelů) je stále jasně patrný – dokonce i běžný interpret je rychlejší než Numba.
Obrázek 7: Teprve pro velká rozlišení a tedy i delší dobu běhu výpočtu se ukazuje, že JIT překlad Numby je vlastně velmi dobrý a překonává Nuitku.
Obrázek 8: JIT překlad trvá cca 5 sekund, takže kratší skripty je lepší spouštět interpretrem a teprve delší JITovat (viz průsečíky křivek).
17. Porovnání výsledků benchmarků: interpretry
Obrázek 9: Porovnání rychlosti různých interpretrů Pythonu. Můžeme zde vidět výkonnostní skok mezi verzí 3.10 a 3.11 (což bylo taktéž oznámeno při vydávání), ovšem kupodivu je verze 3.12 nepatrně horší, než 3.11.
18. Porovnání výsledků benchmarků: překladače
Obrázek 10: Zde jsou zobrazeny jen výsledky céčkového kódu v porovnání s AOTovaným kódem, tedy vlastně vždy programy, které vznikly překladem céčkovského zdrojového kódu (ať již ručně napsaného či vzniklého transpilerem). Nuitka v tomto porovnání nevychází špatně – spadá do oblasti mezi nejhorším Cythonem a Cythonem s typovými informacemi.
Obrázek 11: Z tohoto grafu je patrné, jak dobrý překlad (resp. transpřeklad) dokáže provést Cython v případě, že má k dispozici typové informace.
19. Příloha: skript pro vytištění grafů s výsledky benchmarků
Všechny grafy, které jsme mohli vidět v kapitolách 17 a 18, byly vytvořeny s využitím následujícího skriptu, který je založen na knihovně Matplotlib. Následuje výpis zdrojového kódu tohoto pomocného skriptu:
#!/usr/bin/env python # coding: utf-8 # ### Set of input files with benchmark results to be processed input_files = ( "native.times", "native_optim.times", "python_3_8.times", "python_3_9.times", "python_3_10.times", "python_3_11.times", "python_3_12.times", "mypyc_no_type_hints.times", "mypyc_with_type_hints.times", "numba2.times", "numba3.times", "numba4.times", "nuitka.times", "cython_basic.times", "cython_types.times", "cython_optim.times", ) import pandas as pd # ### Helper functions to read benchmark results def read_benchmark_result(filename): return pd.read_csv(filename, sep=" ", header=0, names=("size", "time", "memory")) def filename2description(filename): return filename.split(".")[0].replace("_", " ") def read_all_results(input_files): return { filename2description(input_file): read_benchmark_result(input_file) for input_file in input_files } # ### Combine all results into one DataFrame r = read_all_results(input_files) results = pd.DataFrame() # column to be transformed into index results["size"] = r["native"]["size"] for description, df in r.items(): results[description] = df["time"] # create meaningful index results.set_index("size", inplace=True) results # ### Set plot size import matplotlib as mpl import matplotlib.pyplot as plt mpl.rcParams["figure.dpi"] = 150 mpl.rcParams["figure.figsize"] = (12, 4.8) xticks = df.index # color palette to be used colormap = [ "#a00000", "#a0a000", # native "#00a000", "#00b030", "#00c060", "#00d090", "#00e0a0", # Python interpreters "#ff0000", "#ff8000", # mypyc "#0000ff", "#0060ff", "#00c0ff", # Numba "#ff8080", # Nuitka "#808080", "#a0a0a0", "#c0c0c0", # Cython ] # ### Plot results # Startup times results[0:5].plot( kind="bar", stacked=False, width=0.9, title="Startup time", color=colormap ) plt.legend(loc=(1.04, 0)) plt.tight_layout() plt.savefig("1st.png") plt.show() # Computation with some startup time influence results[5:10].plot( kind="bar", stacked=False, width=0.9, title="Computation with startup time influence", color=colormap, ) plt.legend(loc=(1.04, 0)) plt.tight_layout() plt.savefig("Startup time and computation.png") plt.show() # Just the computation, w/o startup time results[10:].plot( kind="bar", stacked=False, width=0.9, title="Extensive computation", color=colormap ) plt.tight_layout() plt.savefig("Extensive computation.png") plt.show() # Approximation, including startup time results.plot(title="Approximation, incl. startup time", color=colormap) plt.tight_layout() plt.savefig("Approximation computation.png") plt.show() # Numba/CPython thresholds results[8:12].plot( title="Numba/CPython thresholds", color=[ "#a00000", "#a0a000", "#00a000", "#00b030", "#00c060", "#00d090", "#00e0a0", "#ff0000", "#ff8000", "#0000ff", "#0060ff", "#00c0ff", ], ) plt.tight_layout() plt.savefig("Numba CPython thresholds.png") plt.show() # Python interpreters only results[11:][ ["python 3 8", "python 3 9", "python 3 10", "python 3 11", "python 3 12"] ].plot(kind="bar", title="Python interpreters only", color=colormap[2:]) plt.tight_layout() plt.savefig("Python interpreters only.png") plt.show() colormap = [ "#a00000", "#a0a000", # native "#ff0000", "#ff8000", # mypyc "#8080ff", # Nuitka "#808080", "#a0a0a0", "#c0c0c0", # Cython ] # Compiled code only compiled = results[11:][ [ "native", "native optim", "mypyc no type hints", "mypyc with type hints", "nuitka", "cython basic", "cython types", "cython optim", ] ] compiled.plot(kind="bar", color=colormap, title="Compiled code only") plt.tight_layout() plt.savefig("Compiled code only.png") plt.show() compiled.plot(title="Approximation, compiled", color=colormap) plt.tight_layout() plt.savefig("Approximation computation compiled.png") plt.show()
20. Odkazy na Internetu
- Stránka projektu Nuitka
https://nuitka.net/ - Dokumentace k projektu Nuitka
https://nuitka.net/user-documentation/ - Nuitka na PyPi
https://pypi.org/project/Nuitka/ - Cython (home page)
http://cython.org/ - Cython (wiki)
https://github.com/cython/cython/wiki - Cython (Wikipedia)
https://en.wikipedia.org/wiki/Cython - Cython (GitHub)
https://github.com/cython/cython - Rychlost CPythonu 3.11 a 3.12 v porovnání s JIT a AOT překladači
https://www.root.cz/clanky/rychlost-cpythonu-3–11-a-3–12-v-porovnani-s-jit-a-aot-prekladaci-pythonu/ - Rychlost CPythonu 3.11 a 3.12 v porovnání s JIT a AOT překladači Pythonu (2)
https://www.root.cz/clanky/rychlost-cpythonu-3–11-a-3–12-v-porovnani-s-jit-a-aot-prekladaci-pythonu-2/ - Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy
https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy/ - Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy (2.část)
https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy-2-cast/ - Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy (3)
https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy-3/ - Praktické použití nástroje Cython při překladu Pythonu do nativního kódu
https://www.root.cz/clanky/prakticke-pouziti-nastroje-cython-pri-prekladu-pythonu-do-nativniho-kodu-1/ - Pyrex
https://wiki.python.org/moin/Pyrex - RPython vs Cython aneb dvojí přístup k překladu Pythonu do nativního kódu
https://www.root.cz/clanky/rpython-vs-cython-aneb-dvoji-pristup-k-prekladu-pythonu-do-nativniho-kodu/ - Python Implementations: Compilers
https://wiki.python.org/moin/PythonImplementations#Compilers - EmbeddingCython
https://github.com/cython/cython/wiki/EmbeddingCython - The Basics of Cython
http://docs.cython.org/en/latest/src/tutorial/cython_tutorial.html - Overcoming Python's GIL with Cython
https://lbolla.info/python-threads-cython-gil - GlobalInterpreterLock
https://wiki.python.org/moin/GlobalInterpreterLock - The Magic of RPython
https://refi64.com/posts/the-magic-of-rpython.html - RPython: Frequently Asked Questions
http://rpython.readthedocs.io/en/latest/faq.html - RPython’s documentation
http://rpython.readthedocs.io/en/latest/index.html - RPython (Wikipedia)
https://en.wikipedia.org/wiki/PyPy#RPython - Getting Started with RPython
http://rpython.readthedocs.io/en/latest/getting-started.html - Duck typing
https://en.wikipedia.org/wiki/Duck_typing - PyPy (home page)
https://pypy.org/ - PyPy (dokumentace)
http://doc.pypy.org/en/latest/ - Localized Type Inference of Atomic Types in Python (2005)
http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.90.3231 - Numba
http://numba.pydata.org/ - Tutorial: Writing an Interpreter with PyPy, Part 1
https://morepypy.blogspot.com/2011/04/tutorial-writing-interpreter-with-pypy.html - List of numerical analysis software
https://en.wikipedia.org/wiki/List_of_numerical_analysis_software - Pixie: lehký skriptovací jazyk s „kouzelnými“ schopnostmi
https://www.root.cz/clanky/pixie-lehky-skriptovaci-jazyk-s-kouzelnymi-schopnostmi/ - Programovací jazyk Pixie: funkce ze základní knihovny a použití FFI
https://www.root.cz/clanky/programovaci-jazyk-pixie-funkce-ze-zakladni-knihovny-a-pouziti-ffi/ - The future can be written in RPython now (článek z roku 2010)
http://blog.christianperone.com/2010/05/the-future-can-be-written-in-rpython-now/ - PyPy is the Future of Python (článek z roku 2010)
https://alexgaynor.net/2010/may/15/pypy-future-python/ - Portal:Python programming
https://en.wikipedia.org/wiki/Portal:Python_programming - RPython Frontend and C Wrapper Generator
http://www.codeforge.com/article/383293 - PyPy’s Approach to Virtual Machine Construction
https://bitbucket.org/pypy/extradoc/raw/tip/talk/dls2006/pypy-vm-construction.pdf - Tutorial: Writing an Interpreter with PyPy, Part 1
https://morepypy.blogspot.com/2011/04/tutorial-writing-interpreter-with-pypy.html - A simple interpreter from scratch in Python (part 1)
http://www.jayconrod.com/posts/37/a-simple-interpreter-from-scratch-in-python-part-1 - Brainfuck Interpreter in Python
https://helloacm.com/brainfuck-interpreter-in-python/