Obsah
1. Just in time překlad programů psaných v Pythonu nástrojem Numba
3. Získání systémových informací i aktuální konfigurace Numby
4. Anotace skriptů zpracovávaných nástrojem Numba
5. Ukázka anotace jednoduché funkce sčítající dvě celočíselné hodnoty
6. Co z anotovaného kódu vyčteme?
7. Anotace stejné funkce, ovšem zavolané s jinými typy argumentů
8. Funkce, která je postupně volána s různými typy argumentů
9. Just in time překlad volaných funkcí do mezikódu LLVM
10. Rozdíly mezi funkcí volanou s celočíselnými parametry a s parametry typu double
11. Zobrazení optimalizovaného mezikódu
12. Překlad do assembleru cílové architektury
13. Využití SIMD instrukcí systémem Numba
14. Anotovaný zdrojový kód a optimalizovaný mezikód
15. Vygenerovaný kód v assembleru využívající SIMD instrukce
17. Paralelizace výsledného kódu
18. Kdy se paralelizace vyplatí a kdy nikoli?
19. Repositář s demonstračními příklady
1. Just in time překlad programů psaných v Pythonu nástrojem Numba
Ve druhém pokračování miniseriálu o nástroji Numba (viz též poněkud postarší článek [1]) si ukážeme především interní procesy, které Numba provádí při JITování kódu. Setkáme se tedy s projektem LLVM, který je velmi populární, a to v mnoha oblastech (stačí jen připomenout zajímavý projekt Emscripten atd.).
Nástroj Numba podporuje překlad vybraných částí kódu aplikace psané v Pythonu do nativního kódu cílové platformy (x86–64 apod.), přičemž cílovou platformou může být i GPU (přes CUDA). Jedná se tedy o takzvaný JIT neboli o just-in-time překladač, který má tu výhodu, že dokáže odvodit datové typy proměnných a argumentů funkcí na základě skutečného chování aplikace, tedy na základě typů předávaných parametrů a kontextu. To samozřejmě neznamená, že by JIT již při prvním volání funkce přesně věděl, jak má funkci přeložit.
Ve skutečnosti se dozví pouze informace o jediné konkrétní větvi, kterou může přeložit. V případě, že bude ta samá funkce později volána s odlišnými typy parametrů, popř. se její chování změní jiným způsobem (Python je velmi dynamický jazyk), provede se just-in-time překlad znovu, takže zde zaplatíme za vyšší výpočetní výkon poněkud většími paměťovými nároky a pomalejším během prvních volání funkce. Na druhou stranu mnoho náročných výpočtů používá Numpy a Numba s Numpy dokáže spolupracovat velmi dobře.
Z pohledu běžného vývojáře je největší předností tohoto způsobu překladu fakt, že není zapotřebí samotný zdrojový kód měnit (až na uvedení anotace před funkci). Nepříjemný je přesun času překladu do runtime, což sice nevadí u aplikací, které běží delší dobu, ovšem u jednorázových skriptů může být použití JITu spíše kontraproduktivní.
Samotný překlad je prováděn na několika úrovních, přičemž Numba na nižších úrovních využívá možností nabízených LLVM. Jedná se o relativně složitou problematiku, které se budeme věnovat v samostatném článku.
2. Instalace nástroje Numba
V současnosti existuje hned několik možností, jak projekt Numba nainstalovat. Pravděpodobně nejjednodušší možnost představuje použití nástroje conda (viz též platformu Anaconda. Instalace bude v tomto případě probíhat takto:
$ conda install numba
popř. pro přechod na vyšší verzi:
$ conda update numba
Pokud namísto nástroje conda použijete nástroj pip (jenž je taktéž podporován), je nejprve vhodné provést upgrade pipu na novější verzi, protože instalace Numby se starým pipem není podporována:
$ sudo python3 -m pip install --upgrade pip Collecting pip Downloading https://files.pythonhosted.org/packages/a4/6d/6463d49a933f547439d6b5b98b46af8742cc03ae83543e4d7688c2420f8b/pip-21.3.1-py3-none-any.whl (1.7MB) 100% |████████████████████████████████| 1.7MB 764kB/s Installing collected packages: pip Found existing installation: pip 9.0.3 Uninstalling pip-9.0.3: Successfully uninstalled pip-9.0.3 Successfully installed pip-21.3.1
Pro jistotu zkontrolujeme, zda se používá skutečně poslední nainstalovaná verze nástroje pip:
$ pip3 --version pip 21.3.1 from /usr/local/lib/python3.8/site-packages/pip
Nyní provedeme instalaci balíčku llvmlite, který obsahuje část projektu LLVM, který je nástrojem Numba interně používán:
$ pip3 install --user llvmlite Collecting llvmlite Downloading llvmlite-0.36.0-cp36-cp36m-manylinux2010_x86_64.whl (25.3 MB) |████████████████████████████████| 25.3 MB 3.2 MB/s Installing collected packages: llvmlite Successfully installed llvmlite-0.36.0
A následně již můžeme nainstalovat samotnou Numbu:
$ pip3 install --user numba Collecting numba Downloading numba-0.53.1-cp36-cp36m-manylinux2014_x86_64.whl (3.4 MB) |████████████████████████████████| 3.4 MB 1.4 MB/s Requirement already satisfied: setuptools in /usr/lib/python3.8/site-packages (from numba) (37.0.0) Requirement already satisfied: numpy>=1.15 in ./.local/lib/python3.8/site-packages (from numba) (1.19.4) Requirement already satisfied: llvmlite<0.37,>=0.36.0rc1 in ./.local/lib/python3.8/site-packages (from numba) (0.36.0) Installing collected packages: numba Successfully installed numba-0.53.1
Dalším krokem ihned po instalaci bude zjištění, zda se Numba nainstalovala korektně. Prvním pokusem bude pokus o spuštění příkazu numba, tj. především test, jestli tento příkaz leží na PATH:
$ numba numba: error: the following arguments are required: filename
V případě, že se tento příkaz nepodaří spustit, většinou to znamená, že do PATH není zahrnuta cesta ~/.local/bin, což lze snadno napravit (.bashrc atd.).
Zobrazení nápovědy:
$ numba --help usage: numba [-h] [--annotate] [--dump-llvm] [--dump-optimized] [--dump-assembly] [--dump-cfg] [--dump-ast] [--annotate-html ANNOTATE_HTML] [-s] [--sys-json SYS_JSON] [filename] positional arguments: filename Python source filename optional arguments: -h, --help show this help message and exit --annotate Annotate source --dump-llvm Print generated llvm assembly --dump-optimized Dump the optimized llvm assembly --dump-assembly Dump the LLVM generated assembly --dump-cfg [Deprecated] Dump the control flow graph --dump-ast [Deprecated] Dump the AST --annotate-html ANNOTATE_HTML Output source annotation as html -s, --sysinfo Output system information for bug reporting --sys-json SYS_JSON Saves the system info dict as a json file
3. Získání systémových informací i aktuální konfigurace Numby
Numba dokáže, pokud je korektně provedena konfigurace, spouštět některé výpočty na GPU, což je výhodné například v případě výpočtů prováděných s n-dimenzionálními poli balíčku Numpy, jejichž výslednou hodnotu potřebujeme získat až na konci celého výpočtu. Ovšem jak použití GPU (CUDA), tak i dalších „vychytávek“ nabízených Numpy, vyžaduje korektní konfiguraci. Aktuální konfiguraci je přitom možné získat snadno následujícím příkazem:
$ numba -s
Nejprve se vypíšou základní informace o systému i o použitém mikroprocesoru:
System info: -------------------------------------------------------------------------------- __Time Stamp__ Report started (local time) : 2023-05-20 12:01:57.835226 UTC start time : 2023-05-20 10:01:57.835229 Running time (s) : 0.829354 __Hardware Information__ Machine : x86_64 CPU Name : skylake CPU Count : 8 Number of accessible CPUs : 8 List of accessible CPUs cores : 0 1 2 3 4 5 6 7 CFS Restrictions (CPUs worth of runtime) : None CPU Features : 64bit adx aes avx avx2 bmi bmi2 clflushopt cmov cx16 cx8 f16c fma fsgsbase fxsr invpcid lzcnt mmx movbe pclmul popcnt prfchw rdrnd rdseed rtm sahf sgx sse sse2 sse3 sse4.1 sse4.2 ssse3 xsave xsavec xsaveopt xsaves Memory Total (MB) : 15466 Memory Available (MB) : 13824 __OS Information__ Platform Name : Linux-4.18.19-100.fc27.x86_64-x86_64-with-fedora-27-Twenty_Seven Platform Release : 4.18.19-100.fc27.x86_64 OS Name : Linux OS Version : #1 SMP Wed Nov 14 22:04:34 UTC 2018 OS Specific Version : ? Libc Version : glibc 2.3.4 __Python Information__ Python Compiler : GCC 7.3.1 20180303 (Red Hat 7.3.1-5) Python Implementation : CPython Python Version : 3.8.6 Python Locale : en_US.UTF-8
Dále se vypíšou důležité informace o LLVM a CUDA:
__LLVM Information__ LLVM Version : 10.0.1 __CUDA Information__ CUDA Device Initialized : False CUDA Driver Version : ? CUDA Detect Output: None CUDA Libraries Test Output: None __ROC information__ ROC Available : False ROC Toolchains : None HSA Agents Count : 0 HSA Agents: None HSA Discrete GPUs Count : 0 HSA Discrete GPUs : None
Ovšem Numba dokáže používat i další knihovny, například Short Vector Math Library Operations neboli SVML, knihovnu pro spouštění výpočtů ve více vláknech (TBB) atd.:
__SVML Information__ SVML State, config.USING_SVML : False SVML Library Loaded : False llvmlite Using SVML Patched LLVM : True SVML Operational : False __Threading Layer Information__ TBB Threading Layer Available : False +--> Disabled due to Unknown import problem. OpenMP Threading Layer Available : True +-->Vendor: GNU Workqueue Threading Layer Available : True +-->Workqueue imported successfully. __Numba Environment Variable Information__ None found. __Conda Information__ Conda not available.
Dále se vypíše seznam nainstalovaných balíčků Pythonu (zde jsou schválně použity starší balíčky):
__Installed Packages__ Package Version ------------------------ ----------------- absl-py 0.11.0 alabaster 0.7.9 aniso8601 3.0.0 ... ... ... xmltodict 0.11.0 zc.thread 1.0.0 zipp 3.1.0
A na konci výpisu se zobrazí různá varování, například v mém případě informace o tom, že není možné použít CUDA:
__Warning log__ Warning (cuda): CUDA driver library cannot be found or no CUDA enabled devices are present. Exception class: <class 'numba.cuda.cudadrv.error.CudaSupportError'> Warning (roc): Error initialising ROC: No ROC toolchains found. Warning (roc): No HSA Agents found, encountered exception when searching: Error at driver init: NUMBA_HSA_DRIVER /opt/rocm/lib/libhsa-runtime64.so is not a valid file path. Note it must be a filepath of the .so/.dll/.dylib or the driver: Warning: Conda not available.
4. Anotace skriptů zpracovávaných nástrojem Numba
Nástroj Numba dokáže skripty naprogramované v Pythonu překládat do strojového (tedy nativního) kódu cílové platformy. Ovšem nejedná se (a v případě Pythonu se ani nemůže jednat) o operaci provedenou v jediném kroku. Celý překlad je totiž rozdělen na mnoho fází. První operací, kterou Numba provádí, je analýza AST (abstraktního syntaktického stromu) se snahou o „porozumění“ kódu zapsaného vývojářem. Druhou operací je odvození datových typů argumentů zpracovávaných funkcí a tím pádem i typů lokálních proměnných těchto funkcí. Numba navíc dokáže původní zdrojový kód doplnit o komentáře, které naznačí, jak vstupnímu zdrojovému kódu „rozumí“ po tomto kroku (tedy analýze) a jaké další informace (typy, živost proměnných) byly z kódu odvozeny. Do původního zdrojového kódu jsou formou poznámek doplněny další informace, jak ostatně uvidíme dále.
5. Ukázka anotace jednoduché funkce sčítající dvě celočíselné hodnoty
Vyzkoušejme si nyní, jak vlastně bude vypadat v předchozí kapitole zmíněná anotace zdrojového kódu zpracovávaného nástrojem Numba, na následujícím jednoduchém demonstračním příkladu, který obsahuje jednu funkci s dekorátorem @jit – tím Numbě naznačujeme, že chceme JITovat právě tuto funkci (význam parametrů dekorátoru si vysvětlíme dále, na anotaci však nemají vliv). Funkce je volána s parametry typu int:
from numba import jit @jit(nopython=True,nogil=True) def sum(a, b): return a+b x = sum(1, 2) print(x)
Vytvoření anotovaného kódu s jeho následujícím výpisem zajišťuje tento příkaz:
$ numba --annotate sum1.py
Podívejme se nyní na výsledek vypsaný nástrojem Numba:
-----------------------------------ANNOTATION----------------------------------- # File: sum1.py # --- LINE 4 --- @jit(nopython=True,nogil=True) # --- LINE 5 --- def sum(a, b): # --- LINE 6 --- # label 0 # a = arg(0, name=a) :: int64 # b = arg(1, name=b) :: int64 # $0.3 = a + b :: int64 # del b # del a # $0.4 = cast(value=$0.3) :: int64 # del $0.3 # return $0.4 return a+b ================================================================================
6. Co z anotovaného kódu vyčteme?
Výsledek uvedený v páté kapitole ukazuje, jaké užitečné informace nástroj Numba v rámci prvních dvou kroků JIT překladu získal:
- Typy argumentů předávaných do funkce sum – oba argumenty jsou v našem konkrétním případě typu int64 (což ovšem znamená zúžení původního typu, protože Pythonovská celá čísla mají neomezený rozsah!)
- Typ výsledku operace (operací) prováděných ve funkci. Konkrétně je typ hodnoty získané operací a+b taktéž roven int64 (mezivýsledek je představován pseudoproměnnou $0.3).
- Původní argumenty mohou být po provedení operace součtu z paměti odstraněny, což Numba korektně detekuje a naznačí příkazem del.
- Poté je zapsáno explicitní přetypování, které bude později při optimalizacích opět odstraněno.
- I mezivýsledek může být z paměti odstraněn, což naznačuje poslední příkaz del.
7. Anotace stejné funkce, ovšem zavolané s jinými typy argumentů
Funkci sum uvedenou v páté kapitole je pochopitelně možné v Pythonu volat s prakticky jakýmikoli typy parametrů, protože operace + je definována například pro pravdivostní hodnoty (i když výsledek může někoho překvapit), pro celočíselné hodnoty, hodnoty s plovoucí řádovou čárkou, řetězce, n-tice, seznamy, množiny atd. Co se tedy stane v případě, kdy stejnou funkci nyní zavoláme s parametry typu float? Můžeme si to velmi snadno ověřit:
from numba import jit @jit(nopython=True,nogil=True) def sum(a, b): return a+b x = sum(1.1, 2.2) print(x)
Nyní bude výsledek analýzy AST, odvození typů a živosti proměnných vypadat poněkud odlišně:
$ numba --annotate sum2.py -----------------------------------ANNOTATION----------------------------------- # File: sum2.py # --- LINE 4 --- @jit(nopython=True,nogil=True) # --- LINE 5 --- def sum(a, b): # --- LINE 6 --- # label 0 # a = arg(0, name=a) :: float64 # b = arg(1, name=b) :: float64 # $0.3 = a + b :: float64 # del b # del a # $0.4 = cast(value=$0.3) :: float64 # del $0.3 # return $0.4 return a+b ================================================================================
$ numba --annotate sum1.py > sum1_annotated $ numba --annotate sum2.py > sum2_annotated $ diff -y sum1_annotated sum2_annotated -----------------------------------ANNOTATION---------------- -----------------------------------ANNOTATION---------------- # File: sum1.py | # File: sum2.py # --- LINE 4 --- # --- LINE 4 --- @jit(nopython=True,nogil=True) @jit(nopython=True,nogil=True) # --- LINE 5 --- # --- LINE 5 --- def sum(a, b): def sum(a, b): # --- LINE 6 --- # --- LINE 6 --- # label 0 # label 0 # a = arg(0, name=a) :: int64 | # a = arg(0, name=a) :: float64 # b = arg(1, name=b) :: int64 | # b = arg(1, name=b) :: float64 # $0.3 = a + b :: int64 | # $0.3 = a + b :: float64 # del b # del b # del a # del a # $0.4 = cast(value=$0.3) :: int64 | # $0.4 = cast(value=$0.3) :: float64 # del $0.3 # del $0.3 # return $0.4 # return $0.4 return a+b return a+b ============================================================= ============================================================= 3 | 3.3000000000000003
Pro zajímavost si ještě v rychlosti ukažme stejnou funkci, ovšem nyní spojující dvě n-tice:
from numba import jit @jit(nopython=True,nogil=True) def sum(a, b): return a+b x = sum((1, 2), (3, 4)) print(x)
Anotovaný zdrojový kód:
$ numba --annotate sum3.py -----------------------------------ANNOTATION----------------------------------- # File: sum3.py # --- LINE 4 --- @jit(nopython=True,nogil=True) # --- LINE 5 --- def sum(a, b): # --- LINE 6 --- # label 0 # a = arg(0, name=a) :: UniTuple(int64 x 2) # b = arg(1, name=b) :: UniTuple(int64 x 2) # $0.3 = a + b :: UniTuple(int64 x 4) # del b # del a # $0.4 = cast(value=$0.3) :: UniTuple(int64 x 4) # del $0.3 # return $0.4 return a+b ================================================================================
8. Funkce, která je postupně volána s různými typy argumentů
Viděli jsme, že Numpy dokáže správně detekovat a odvodit parametry volané funkce v případě, že je daná funkce v programovém kódu volána pouze jedenkrát. Ovšem v praxi nás Python nijak neomezuje v tom, jak a kolikrát bude nějaká funkce volána, což znamená, že stejná funkce může být volána vícekrát, pokaždé s různými typy (a někdy i počtem) parametrů. To pro Numbu ve skutečnosti znamená jen nepatrný problém – danou funkci bude muset přeložit vícekrát, pokaždé pro jiné typy parametrů (a tím pádem i typy lokálních proměnných atd.). A řešení tohoto problému by se mělo projevit i na anotaci, takže si tuto domněnku otestujme na tomto příkladu:
from numba import jit @jit(nopython=True,nogil=True) def sum(a, b): return a+b x = sum(1, 2) y = sum(1.1, 2.2) z = sum((1, 2), (3, 4)) print(x) print(y) print(z)
Skript spustíme a přitom si necháme vypsat anotace JITovaných funkcí:
$ numba --annotate sum3.py
Z vypsaných výsledků je patrné, že se tatáž funkce skutečně ve výpisu objeví ve třech různých variantách:
-----------------------------------ANNOTATION----------------------------------- # File: sum3.py # --- LINE 4 --- @jit(nopython=True,nogil=True) # --- LINE 5 --- def sum(a, b): # --- LINE 6 --- # label 0 # a = arg(0, name=a) :: int64 # b = arg(1, name=b) :: int64 # $0.3 = a + b :: int64 # del b # del a # $0.4 = cast(value=$0.3) :: int64 # del $0.3 # return $0.4 return a+b ================================================================================ -----------------------------------ANNOTATION----------------------------------- # File: sum3.py # --- LINE 4 --- @jit(nopython=True,nogil=True) # --- LINE 5 --- def sum(a, b): # --- LINE 6 --- # label 0 # a = arg(0, name=a) :: float64 # b = arg(1, name=b) :: float64 # $0.3 = a + b :: float64 # del b # del a # $0.4 = cast(value=$0.3) :: float64 # del $0.3 # return $0.4 return a+b ================================================================================ -----------------------------------ANNOTATION----------------------------------- # File: sum3.py # --- LINE 4 --- @jit(nopython=True,nogil=True) # --- LINE 5 --- def sum(a, b): # --- LINE 6 --- # label 0 # a = arg(0, name=a) :: UniTuple(int64 x 2) # b = arg(1, name=b) :: UniTuple(int64 x 2) # $0.3 = a + b :: UniTuple(int64 x 4) # del b # del a # $0.4 = cast(value=$0.3) :: UniTuple(int64 x 4) # del $0.3 # return $0.4 return a+b ================================================================================
9. Just in time překlad volaných funkcí do mezikódu LLVM
V okamžiku, kdy má nástroj Numba k dispozici podrobné informace o typech parametrů funkcí a odvodí jak typy, tak i živost lokálních proměnných, může dojít k další fázi překladu – k vygenerování mezikódu pro LLVM. Tento mezikód si můžeme nechat snadno zobrazit příkazem:
$ numba --dump-llvm sum1.py
Vygenerovaný mezikód obsahuje mnoho pseudoinstrukcí. V následujícím výpisu se setkáme zejména s pseudoinstrukcemi store, alloca, ret, br, load a add:
--------------------LLVM DUMP <function descriptor 'sum$1'>--------------------- ; ModuleID = "sum$1" target triple = "x86_64-unknown-linux-gnu" target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" @"_ZN08NumbaEnv8__main__7sum$241Exx" = common global i8* null define i32 @"_ZN8__main__7sum$241Exx"(i64* noalias nocapture %"retptr", {i8*, i32, i8*}** noalias nocapture %"excinfo", i64 %"arg.a", i64 %"arg.b") { entry: %"a" = alloca i64 store i64 0, i64* %"a" %"b" = alloca i64 store i64 0, i64* %"b" %"$0.3" = alloca i64 store i64 0, i64* %"$0.3" %"$0.4" = alloca i64 store i64 0, i64* %"$0.4" br label %"B0" B0: %".7" = load i64, i64* %"a" store i64 %"arg.a", i64* %"a" %".10" = load i64, i64* %"b" store i64 %"arg.b", i64* %"b" %".12" = load i64, i64* %"a" %".13" = load i64, i64* %"b" %".14" = add nsw i64 %".12", %".13" %".16" = load i64, i64* %"$0.3" store i64 %".14", i64* %"$0.3" %".18" = load i64, i64* %"b" store i64 0, i64* %"b" %".20" = load i64, i64* %"a" store i64 0, i64* %"a" %".22" = load i64, i64* %"$0.3" %".24" = load i64, i64* %"$0.4" store i64 %".22", i64* %"$0.4" %".26" = load i64, i64* %"$0.3" store i64 0, i64* %"$0.3" %".28" = load i64, i64* %"$0.4" store i64 %".28", i64* %"retptr" ret i32 0 } ================================================================================ 3
Pro zajímavost se můžeme podívat, jak se do mezikódu LLVM přeloží druhá funkce, kterou voláme s parametry typu double:
$ numba --dump-llvm sum2.py
Výsledek:
--------------------LLVM DUMP <function descriptor 'sum$1'>--------------------- ; ModuleID = "sum$1" target triple = "x86_64-unknown-linux-gnu" target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" @"_ZN08NumbaEnv8__main__7sum$241Edd" = common global i8* null define i32 @"_ZN8__main__7sum$241Edd"(double* noalias nocapture %"retptr", {i8*, i32, i8*}** noalias nocapture %"excinfo", double %"arg.a", double %"arg.b") { entry: %"a" = alloca double store double 0.0, double* %"a" %"b" = alloca double store double 0.0, double* %"b" %"$0.3" = alloca double store double 0.0, double* %"$0.3" %"$0.4" = alloca double store double 0.0, double* %"$0.4" br label %"B0" B0: %".7" = load double, double* %"a" store double %"arg.a", double* %"a" %".10" = load double, double* %"b" store double %"arg.b", double* %"b" %".12" = load double, double* %"a" %".13" = load double, double* %"b" %".14" = fadd double %".12", %".13" %".16" = load double, double* %"$0.3" store double %".14", double* %"$0.3" %".18" = load double, double* %"b" store double 0.0, double* %"b" %".20" = load double, double* %"a" store double 0.0, double* %"a" %".22" = load double, double* %"$0.3" %".24" = load double, double* %"$0.4" store double %".22", double* %"$0.4" %".26" = load double, double* %"$0.3" store double 0.0, double* %"$0.3" %".28" = load double, double* %"$0.4" store double %".28", double* %"retptr" ret i32 0 } ================================================================================ 3.3000000000000003
10. Rozdíly mezi funkcí volanou s celočíselnými parametry a s parametry typu double
Opět se podívejme na rozdíly mezi oběma výsledky. V této fázi by neměly být velké, protože se liší „jen“ datové typy, s nimiž se pracuje:
$ numba --dump-llvm sum1.py > sum1_llvm $ numba --dump-llvm sum2.py > sum2_llvm
Rozdíly spočívají v náhradě i64 na double:
$ diff -y sum1_llvm sum2_llvm --------------------LLVM DUMP <function descriptor 'sum$1'>-- --------------------LLVM DUMP <function descriptor 'sum$1'>-- ; ModuleID = "sum$1" ; ModuleID = "sum$1" target triple = "x86_64-unknown-linux-gnu" target triple = "x86_64-unknown-linux-gnu" target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i @"_ZN08NumbaEnv8__main__7sum$241Exx" = common global i8* null | @"_ZN08NumbaEnv8__main__7sum$241Edd" = common global i8* null define i32 @"_ZN8__main__7sum$241Exx"(i64* noalias nocapture | define i32 @"_ZN8__main__7sum$241Edd"(double* noalias nocaptu { { entry: entry: %"a" = alloca i64 | %"a" = alloca double store i64 0, i64* %"a" | store double 0.0, double* %"a" %"b" = alloca i64 | %"b" = alloca double store i64 0, i64* %"b" | store double 0.0, double* %"b" %"$0.3" = alloca i64 | %"$0.3" = alloca double store i64 0, i64* %"$0.3" | store double 0.0, double* %"$0.3" %"$0.4" = alloca i64 | %"$0.4" = alloca double store i64 0, i64* %"$0.4" | store double 0.0, double* %"$0.4" br label %"B0" br label %"B0" B0: B0: %".7" = load i64, i64* %"a" | %".7" = load double, double* %"a" store i64 %"arg.a", i64* %"a" | store double %"arg.a", double* %"a" %".10" = load i64, i64* %"b" | %".10" = load double, double* %"b" store i64 %"arg.b", i64* %"b" | store double %"arg.b", double* %"b" %".12" = load i64, i64* %"a" | %".12" = load double, double* %"a" %".13" = load i64, i64* %"b" | %".13" = load double, double* %"b" %".14" = add nsw i64 %".12", %".13" | %".14" = fadd double %".12", %".13" %".16" = load i64, i64* %"$0.3" | %".16" = load double, double* %"$0.3" store i64 %".14", i64* %"$0.3" | store double %".14", double* %"$0.3" %".18" = load i64, i64* %"b" | %".18" = load double, double* %"b" store i64 0, i64* %"b" | store double 0.0, double* %"b" %".20" = load i64, i64* %"a" | %".20" = load double, double* %"a" store i64 0, i64* %"a" | store double 0.0, double* %"a" %".22" = load i64, i64* %"$0.3" | %".22" = load double, double* %"$0.3" %".24" = load i64, i64* %"$0.4" | %".24" = load double, double* %"$0.4" store i64 %".22", i64* %"$0.4" | store double %".22", double* %"$0.4" %".26" = load i64, i64* %"$0.3" | %".26" = load double, double* %"$0.3" store i64 0, i64* %"$0.3" | store double 0.0, double* %"$0.3" %".28" = load i64, i64* %"$0.4" | %".28" = load double, double* %"$0.4" store i64 %".28", i64* %"retptr" | store double %".28", double* %"retptr" ret i32 0 ret i32 0 } } ============================================================= ============================================================= 3 | 3.3000000000000003
11. Zobrazení optimalizovaného mezikódu
Dalším krokem, který nástroj Numba (nyní již nepřímo) zajišťuje, je optimalizace mezikódu LLVM. I optimalizovaný mezikód si pochopitelně můžeme nechat zobrazit, a to s využitím přepínače –dump-optimized:
$ numbra --show-optimized sum1.py
Výsledek je poněkud dlouhý, protože obsahuje i realizaci různých mezních stavů, ovšem pro nás je nejpodstatnější následující část, která opravdu obsahuje velmi optimalizovaný kód:
; Function Attrs: nofree norecurse nounwind writeonly define i32 @"_ZN8__main__7sum$241Exx"(i64* noalias nocapture %retptr, { i8*, i32, i8* }** noalias nocapture readnone %excinfo, i64 %arg.a, i64 %arg.b) local_unnamed_addr #0 { entry: %.14 = add nsw i64 %arg.b, %arg.a store i64 %.14, i64* %retptr, align 8 ret i32 0 }
Podobně si můžeme nechat zobrazit optimalizovaný mezikód i pro druhou variantu funkce sum:
$ numbra --show-optimized sum2.py
; Function Attrs: nofree norecurse nounwind writeonly define i32 @"_ZN8__main__7sum$241Edd"(double* noalias nocapture %retptr, { i8*, i32, i8* }** noalias nocapture readnone %excinfo, double %arg.a, double %arg.b) local_unnamed_addr #0 { entry: %.14 = fadd double %arg.a, %arg.b store double %.14, double* %retptr, align 8 ret i32 0 }
12. Překlad do assembleru cílové architektury
LLVM v navazujícím kroku provádí překlad z optimalizovaného mezikódu do strojového kódu cílové architektury, což dnes bude pro jednoduchost prozatím x86–64, ovšem v dalším článku si ukážeme i další nabízené možnosti. A i způsob provedení tohoto kroku můžeme velmi snadno zkontrolovat, protože nástroj Numba nabízí přepínač –dump-assembly, jenž vypíše sekvenci instrukcí v assembleru, které odpovídají výslednému strojovému kódu.
Vyzkoušíme si to nejprve na funkci sum ve variantě, kdy je volána s dvojicí celých čísel:
$ numba --dump-assembly sum1.py
Překlad do assembleru cílové architektury bude vypadat následovně:
_ZN8__main__7sum$241Exx: addq %rcx, %rdx movq %rdx, (%rdi) xorl %eax, %eax retq
A jak bude vypadat způsob překladu stejné funkce, ovšem volané s parametry typu double?
$ numba --dump-assembly sum2.py
Nyní vidíme využití registrů přidaných v rámci z rozšíření instrukční sady SSE popř. SSE2 a instrukce vaddsd a vmovsd jsou definovány v AVX:
_ZN8__main__7sum$241Edd: vaddsd %xmm1, %xmm0, %xmm0 vmovsd %xmm0, (%rdi) xorl %eax, %eax retq
13. Využití SIMD instrukcí systémem Numba
Jednou z velkých předností nástroje Numba oproti dalším konkurenčním překladačům Pythonu je jeho schopnost detekovat ty části programového kódu, které lze realizovat s využitím SIMD instrukcí (což je mnohdy důležitější, než snaha o souběžný běh ve více vláknech). Tato „vektorizace“ se nejvíce projeví u kódu založeného na manipulaci s n-dimenzionálními poli z balíčku Numpy, přičemž výsledek může být lepší, než původní strojový kód v Numpy a navíc je možné v kódu používat Pythonovské smyčky, které nemají žádný velký negativní vliv na výsledný čas výpočtu. Ostatně se podívejme na jednoduchý demonstrační příklad, který počítá součet prvků v poli, a to s využitím explicitně zapsané programové smyčky:
from numba import jit import numpy as np @jit(nopython=True) def sum_array(array): result = 0 for i in range(array.shape[0]): result += array[i] return result array = np.arange(1, 1001) print(sum_array(array))
14. Anotovaný zdrojový kód a optimalizovaný mezikód
Podívejme se nyní na to, jak bude vypadat anotovaný zdrojový kód s funkcí sum_array zmíněnou v předchozí kapitole:
-----------------------------------ANNOTATION----------------------------------- # File: sum_array.py # --- LINE 6 --- @jit(nopython=True) # --- LINE 7 --- def sum_array(array): # --- LINE 8 --- # label 0 # array = arg(0, name=array) :: array(int64, 1d, C) # result = const(int, 0) :: Literal[int](0) result = 0 # --- LINE 9 --- # jump 6 # label 6 # jump 8 # label 8 # $8.1 = global(range: ) :: Function(<class 'range'>) # $8.3 = getattr(value=array, attr=shape) :: UniTuple(int64 x 1) # $const8.4 = const(int, 0) :: Literal[int](0) # $8.5 = static_getitem(value=$8.3, index=0, index_var=$const8.4, fn=<built-in function getitem>) :: int64 # del $const8.4 # del $8.3 # $8.6 = call $8.1($8.5, func=$8.1, args=[Var($8.5, sum_array.py:9)], kws=(), vararg=None) :: (int64,) -> range_state_int64 # del $8.5 # del $8.1 # $8.7 = getiter(value=$8.6) :: range_iter_int64 # del $8.6 # $phi22.1 = $8.7 :: range_iter_int64 # del $8.7 # jump 22 # label 22 # result.2 = phi(incoming_values=[Var(result, sum_array.py:8), Var(result.1, sum_array.py:10)], incoming_blocks=[8, 24]) :: int64 # del result.1 # $22.2 = iternext(value=$phi22.1) :: pair<int64, bool> # $22.3 = pair_first(value=$22.2) :: int64 # $22.4 = pair_second(value=$22.2) :: bool # del $22.2 # $phi24.1 = $22.3 :: int64 # $phi40.1 = $22.3 :: int64 # del $phi40.1 # del $22.3 # $phi40.2 = $phi22.1 :: range_iter_int64 # del $phi40.2 # branch $22.4, 24, 40 # label 24 # del $22.4 # i = $phi24.1 :: int64 # del $phi24.1 for i in range(array.shape[0]): # --- LINE 10 --- # $24.5 = getitem(value=array, index=i, fn=<built-in function getitem>) :: int64 # del i # $24.6 = inplace_binop(fn=<built-in function iadd>, immutable_fn=<built-in function add>, lhs=result.2, rhs=$24.5, static_lhs=Undefined, static_rhs=Undefined) :: int64 # del result.2 # del $24.5 # result.1 = $24.6 :: int64 # del $24.6 # jump 22 # label 40 result += array[i] # --- LINE 11 --- # del result # del array # del $phi24.1 # del $phi22.1 # del $22.4 # jump 42 # label 42 # $42.2 = cast(value=result.2) :: int64 # del result.2 # return $42.2 return result ================================================================================
Zajímavé je taktéž zjistit, jak zhruba vypadá optimalizovaný mezikód LLVM. Zde je patrné, že byly detekovány operace, které je možné provádět s celými vektory:
... ... ... vector.body: ; preds = %vector.body, %vector.ph.new %index = phi i64 [ 0, %vector.ph.new ], [ %index.next.3, %vector.body ] %vec.phi = phi <4 x i64> [ zeroinitializer, %vector.ph.new ], [ %48, %vector.body ] %vec.phi9 = phi <4 x i64> [ zeroinitializer, %vector.ph.new ], [ %49, %vector.body ] %vec.phi10 = phi <4 x i64> [ zeroinitializer, %vector.ph.new ], [ %50, %vector.body ] %vec.phi11 = phi <4 x i64> [ zeroinitializer, %vector.ph.new ], [ %51, %vector.body ] %niter = phi i64 [ %unroll_iter, %vector.ph.new ], [ %niter.nsub.3, %vector.body ] %sunkaddr = mul i64 %index, 8 %4 = bitcast i64* %arg.array.4 to i8* %sunkaddr101 = getelementptr i8, i8* %4, i64 %sunkaddr %5 = bitcast i8* %sunkaddr101 to <4 x i64>* ... ... ... %rdx.shuf = shufflevector <4 x i64> %bin.rdx22, <4 x i64> undef, <4 x i32> <i32 2, i32 3, i32 undef, i32 undef> %bin.rdx23 = add <4 x i64> %bin.rdx22, %rdx.shuf %rdx.shuf24 = shufflevector <4 x i64> %bin.rdx23, <4 x i64> undef, <4 x i32> <i32 1, i32 undef, i32 undef, i32 undef> %bin.rdx25 = add <4 x i64> %bin.rdx23, %rdx.shuf24 %59 = extractelement <4 x i64> %bin.rdx25, i32 0
15. Vygenerovaný kód v assembleru využívající SIMD instrukce
Nejzajímavější bude pochopitelně až výsledný nativní kód, který se spustí namísto interpretace původního pythonního skriptu. A v tomto nativním kódu nalezneme (kromě mnoha dalších nyní nevýznamných informací) i sekvenci instrukcí tvořících programovou smyčku, v níž se používají „vektorové“ registry ymm(x) a navíc jsou operace ve smyčce rozbaleny. Nejprve si ukažme přípravnou fázi smyčky:
_ZN8__main__13sum_array$241E5ArrayIxLi1E1C7mutable7alignedE: movq 16(%rsp), %rax testq %rax, %rax jle .LBB0_1 movq 8(%rsp), %r9 cmpq $16, %rax jae .LBB0_4 xorl %r8d, %r8d xorl %ecx, %ecx jmp .LBB0_13 .LBB0_1: xorl %ecx, %ecx jmp .LBB0_15 .LBB0_4: movq %rax, %r8 andq $-16, %r8 leaq -16(%r8), %rdx movq %rdx, %rsi shrq $4, %rsi incq %rsi movl %esi, %ecx andl $3, %ecx cmpq $48, %rdx jae .LBB0_6 vpxor %xmm0, %xmm0, %xmm0 xorl %edx, %edx vpxor %xmm1, %xmm1, %xmm1 vpxor %xmm2, %xmm2, %xmm2 vpxor %xmm3, %xmm3, %xmm3 jmp .LBB0_8 .LBB0_6: subq %rcx, %rsi vpxor %xmm0, %xmm0, %xmm0 xorl %edx, %edx vpxor %xmm1, %xmm1, %xmm1 vpxor %xmm2, %xmm2, %xmm2 vpxor %xmm3, %xmm3, %xmm3
Následují vlastní instrukce pro částečně rozbalenou smyčku:
.p2align 4, 0x90 .LBB0_7: vpaddq (%r9,%rdx,8), %ymm0, %ymm0 vpaddq 32(%r9,%rdx,8), %ymm1, %ymm1 vpaddq 64(%r9,%rdx,8), %ymm2, %ymm2 vpaddq 96(%r9,%rdx,8), %ymm3, %ymm3 vpaddq 128(%r9,%rdx,8), %ymm0, %ymm0 vpaddq 160(%r9,%rdx,8), %ymm1, %ymm1 vpaddq 192(%r9,%rdx,8), %ymm2, %ymm2 vpaddq 224(%r9,%rdx,8), %ymm3, %ymm3 vpaddq 256(%r9,%rdx,8), %ymm0, %ymm0 vpaddq 288(%r9,%rdx,8), %ymm1, %ymm1 vpaddq 320(%r9,%rdx,8), %ymm2, %ymm2 vpaddq 352(%r9,%rdx,8), %ymm3, %ymm3 vpaddq 384(%r9,%rdx,8), %ymm0, %ymm0 vpaddq 416(%r9,%rdx,8), %ymm1, %ymm1 vpaddq 448(%r9,%rdx,8), %ymm2, %ymm2 vpaddq 480(%r9,%rdx,8), %ymm3, %ymm3
A takto vypadá konec smyčky (podmínka+podmíněný skok) a úklidové operace:
addq $64, %rdx addq $-4, %rsi jne .LBB0_7 .LBB0_8: testq %rcx, %rcx je .LBB0_11 leaq (%r9,%rdx,8), %rdx addq $96, %rdx negq %rcx .p2align 4, 0x90 .LBB0_10: vpaddq -96(%rdx), %ymm0, %ymm0 vpaddq -64(%rdx), %ymm1, %ymm1 vpaddq -32(%rdx), %ymm2, %ymm2 vpaddq (%rdx), %ymm3, %ymm3 subq $-128, %rdx incq %rcx jne .LBB0_10 .LBB0_11: vpaddq %ymm3, %ymm1, %ymm1 vpaddq %ymm2, %ymm0, %ymm0 vpaddq %ymm1, %ymm0, %ymm0 vextracti128 $1, %ymm0, %xmm1 vpaddq %xmm1, %xmm0, %xmm0 vpshufd $78, %xmm0, %xmm1 vpaddq %xmm1, %xmm0, %xmm0 vmovq %xmm0, %rcx cmpq %rax, %r8 je .LBB0_15 andl $15, %eax .LBB0_13: leaq (%r9,%r8,8), %rdx incq %rax .p2align 4, 0x90 .LBB0_14: addq (%rdx), %rcx addq $8, %rdx decq %rax cmpq $1, %rax jg .LBB0_14 .LBB0_15: movq %rcx, (%rdi) xorl %eax, %eax vzeroupper retq .Lfunc_end0:
16. Režim fast math
Výpočty s hodnotami s pohyblivou řádovou čárkou lze realizovat v režimu fast-math, který nezaručí, že všechny mezivýsledky budou správně zaokrouhleny a znormalizovány. Na druhou stranu je tento režim v praxi rychlejší, protože umožňuje matematickému koprocesoru (což je mimochodem termín, který dnes již vlastně postrádá smysl) vynechat některé mezioperace.
Rozdíl mezi „správnou“ aritmetikou a režimem fast-math je patrný z následujících dvou skriptů.
První skript režim fast-math nepoužívá:
from numba import jit import numpy as np @jit def sum_array(array): result = 0. for x in array: result += np.sqrt(x) return result array = np.arange(1, 100000001) x = 0 for _ in range(100): x += sum_array(array) print(sum_array(array))
Druhý testovací skript naopak režim fast-math zapíná:
from numba import jit import numpy as np @jit(nopython=True, fastmath=True) def sum_array(array): result = 0. for x in array: result += np.sqrt(x) return result array = np.arange(1, 100000001) x = 0 for _ in range(100): x += sum_array(array) print(sum_array(array))
Jak se budou lišit vypočtené výsledky?
$ numba sum_sqrts2.py 666666671666.567 $ numba sum_sqrts3.py 666666671666.449
Rozdíl je tedy nepatrný.
Nyní se podívejme na časy výpočtů:
$ time numba sum_sqrts2.py 666666671666.567 real 0m19,524s user 0m19,875s sys 0m1,689s $ time numba sum_sqrts3.py 666666671666.449 real 0m14,829s user 0m15,168s sys 0m1,708s
Rozdíl 15 sekund vs. 19 již je dosti znatelný.
17. Paralelizace výsledného kódu
Další optimalizace, kterou Numba dokáže zajistit, je paralelizace části kódu, typicky programové smyčky nebo častého volání nějaké funkce. Pro tento účel se používá parametr parallel=True předaný dekorátoru @jit a navíc je vhodné při generování indexů atd. použít namísto vestavěného generátoru range jeho paralelní variantu prange. Jak ale takový paralelní výpočet probíhá? Kód, resp. jednotlivé iterace nebo celé volání funkce, je spouštěn ve vláknech, přičemž pro správu vláken se používají různé knihovny, o nichž se zmíním příště.
18. Kdy se paralelizace vyplatí a kdy nikoli?
„Běh ve více vláknech“ je sice mnohdy chápán jako svatý grál moderního IT, ovšem ve skutečnosti tomu tak být nemusí (osobně si myslím, že je výhodnější věnovat čas vektorizaci kódu, než bojovat s vícevláknovým programováním). Ostatně si to můžeme ověřit na příkladu, v němž se pracuje s poli, která mají počet prvků v prvním případě nastaven na 1000 a v případě druhém na 100000. Každý příklad testuje rychlost vykonávání kódu ve třech funkcích, jejichž těla jsou totožná a liší se pouze jejich dekorátory @gil:
import time from numba import jit import numpy as np def regular_sum(a, b): return a+b @jit(nopython=True) def sequential_sum(a, b): return a+b @jit(nopython=True,nogil=True,parallel=True) def parallel_sum(a, b): return a+b N = 1000 x = np.arange(0, N) y = np.zeros(N) print("Let's start") z = regular_sum(x, y) z = sequential_sum(x, y) z = parallel_sum(x, y) MAX = 100000 print("Compiled") t1 = time.time() for _ in range(MAX): z = regular_sum(x, y) t2 = time.time() print(t2-t1) t1 = time.time() for _ in range(MAX): z = sequential_sum(x, y) t2 = time.time() print(t2-t1) t1 = time.time() for _ in range(MAX): z = parallel_sum(x, y) t2 = time.time() print(t2-t1)
import time from numba import jit import numpy as np def regular_sum(a, b): return a+b @jit(nopython=True) def sequential_sum(a, b): return a+b @jit(nopython=True,nogil=True,parallel=True) def parallel_sum(a, b): return a+b N = 100000 x = np.arange(0, N) y = np.zeros(N) print("Let's start") z = regular_sum(x, y) z = sequential_sum(x, y) z = parallel_sum(x, y) MAX = 100000 print("Compiled") t1 = time.time() for _ in range(MAX): z = regular_sum(x, y) t2 = time.time() print(t2-t1) t1 = time.time() for _ in range(MAX): z = sequential_sum(x, y) t2 = time.time() print(t2-t1) t1 = time.time() for _ in range(MAX): z = parallel_sum(x, y) t2 = time.time() print(t2-t1)
Podívejme se nyní na změřené výsledky.
První příklad (krátká pole):
$ numba sum5.py Let's start Compiled 0.28554391860961914 0.1572709083557129 0.5220246315002441
Druhý příklad (delší pole):
$ numba sum6.py Let's start Compiled 14.95469880104065 6.64401388168335 2.430103063583374
19. Repositář s demonstračními příklady
Všechny skripty, které jsme si v dnešním i minulém článku ukázali, naleznete na adrese https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady (pro jejich spuštění je nutné mít nainstalovánu knihovnu Numba a její závislosti):
20. Odkazy na Internetu
- Numba
http://numba.pydata.org/ - numba 0.57.0
https://pypi.org/project/numba/ - Pushing Python toward C speeds with SIMD
https://laurenar.net/posts/python-simd/ - Retrieve generated LLVM from Numba
https://stackoverflow.com/questions/25213137/retrieve-generated-llvm-from-numba - Numba documentation
http://numba.pydata.org/numba-doc/latest/index.html - Numba na GitHubu
https://github.com/numba/numba - First Steps with numba
https://numba.pydata.org/numba-doc/0.12.2/tutorial_firststeps.html - Numba and types
https://numba.pydata.org/numba-doc/0.12.2/tutorial_types.html - Just-in-time compilation
https://en.wikipedia.org/wiki/Just-in-time_compilation - 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 - 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 - 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 - 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/ - Interpretry, překladače, JIT překladače a transpřekladače programovacího jazyka Lua
https://www.root.cz/clanky/interpretry-prekladace-jit-prekladace-a-transprekladace-programovaciho-jazyka-lua/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua
https://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (2)
https://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-2/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (3)
https://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-3/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (4)
https://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-4/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (5 – tabulky a pole)
https://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-5-tabulky-a-pole/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (6 – překlad programových smyček do mezijazyka LuaJITu)
https://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-6-preklad-programovych-smycek-do-mezijazyka-luajitu/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (7 – dokončení popisu mezijazyka LuaJITu)
https://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-7-dokonceni-popisu-mezijazyka-luajitu/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (8 – základní vlastnosti trasovacího JITu)
https://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-8-zakladni-vlastnosti-trasovaciho-jitu/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (9 – další vlastnosti trasovacího JITu)
https://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-9-dalsi-vlastnosti-trasovaciho-jitu/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (10 – JIT překlad do nativního kódu)
https://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-10-jit-preklad-do-nativniho-kodu/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (11 – JIT překlad do nativního kódu procesorů s architekturami x86 a ARM)
https://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-11-jit-preklad-do-nativniho-kodu-procesoru-s-architekturami-x86-a-arm/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (12 – překlad operací s reálnými čísly)
https://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-12-preklad-operaci-s-realnymi-cisly/ - Podpora SIMD (vektorových) instrukcí na RISCových procesorech
https://www.root.cz/clanky/podpora-simd-vektorovych-instrukci-na-riscovych-procesorech/ - Užitečné rozšíření GCC – podpora SIMD (vektorových) instrukcí: nedostatky technologie
https://www.root.cz/clanky/uzitecne-rozsireni-gcc-podpora-simd-vektorovych-instrukci-nedostatky-technologie/ - Podpora SIMD operací v GCC s využitím intrinsic pro nízkoúrovňové optimalizace
https://www.root.cz/clanky/podpora-simd-operaci-v-gcc-s-vyuzitim-intrinsic-pro-nizkourovnove-optimalizace/ - Podpora SIMD operací v GCC s využitím intrinsic: technologie SSE
https://www.root.cz/clanky/podpora-simd-operaci-v-gcc-s-vyuzitim-intrinsic-technologie-sse/ - Rozšíření instrukční sady „Advanced Vector Extensions“ na platformě x86–64
https://www.root.cz/clanky/rozsireni-instrukcni-sady-advanced-vector-extensions-na-platforme-x86–64/ - Rozšíření instrukční sady F16C, FMA a AVX-512 na platformě x86–64
https://www.root.cz/clanky/rozsireni-instrukcni-sady-f16c-fma-a-avx-512-na-platforme-x86–64/ - Použití instrukcí SSE a AVX pro zrychlení bitových operací
https://www.root.cz/clanky/pouziti-instrukci-sse-a-avx-pro-zrychleni-bitovych-operaci/ - Rozšíření instrukční sady AVX-512 na platformě x86–64 (dokončení)
https://www.root.cz/clanky/rozsireni-instrukcni-sady-avx-512-na-platforme-x86–64-dokonceni/ - Nuitka
https://github.com/Nuitka/Nuitka