Obsah
1. Rychlost CPythonu 3.11 a 3.12 v porovnání s JIT a AOT překladači Pythonu
2. JIT a AOT překladače Pythonu
4. Instalace a první spuštění překladače mypyc
5. Příklad použití překladače mypyc
7. Zjištění, které části kódu bylo možné přeložit optimálním způsobem
8. Doplnění všech typových informací
9. Poznámka k hodnotě __name__
11. (Mikro)benchmark: výpočty s využitím FP hodnot
12. Nativní varianta benchmarku přepsaná do ANSI C
13. Varianty upravené pro Mypyc
14. Varianty upravené pro Numbu
15. Výsledky benchmarků: rychlost výpočtů
16. Výsledky benchmarků: spotřeba operační paměti
17. Zdrojové kódy benchmarků použitých v dnešním článku
18. Repositář s demonstračními příklady pro nástroj Numba
19. Repositář s demonstračními příklady pro nástroj Mypy
1. Rychlost Pythonu 3.11 a 3.12 v porovnání s JIT a AOT překladači Pythonu
Programovací jazyk Python zajisté není zapotřebí čtenářům Roota podrobně představovat. V současnosti se jedná o jeden z nejpoužívanějších a současně i nejpopulárnějších (což ovšem ani zdaleka není totéž) programovacích jazyků a v praxi se používá jak pro psaní jednorázových skriptů, tak i mnohdy velmi rozsáhlých aplikací. Jednou z nevýhod Pythonu je, resp. byl relativně pomalý běh aplikací psaných v tomto jazyku. Řekněme si to na rovinu: z mainstreamových jazyků vycházel klasický CPython většinou jako nejpomalejší technologie. Ovšem v současnosti již není situace vůbec špatná, protože existují JIT (just in time) i AOT (ahead of time) překladače Pythonu. A navíc i klasický CPython je neustále vylepšován, přičemž poměrně velký výkonnostní skok se odehrál u Python verze 3.11 (a některá vylepšení nalezneme i u verze 3.12).
V dnešním článku se přesvědčíme, jak rychlý či naopak pomalý je klasický CPython (verze 3.8 až 3.12) při provádění FP operací v porovnání s JIT překladačem Numba a taktéž v porovnání s AOT překladačem mypyc (což je nástroj, který je součástí Mypy). JIT překladačem Numba jsme se již na stránkách Roota zabývali, a to konkrétně v článcích Projekt Numba aneb další přístup k překladu Pythonu do nativního kódu a Just in time překlad programů psaných v Pythonu nástrojem Numba. Nástroj mypy prozatím nebyl popsán, takže se v navazujících kapitolách alespoň ve stručnosti zmíníme o jeho základních vlastnostech. Navážeme přitom na následující články o Mypy:
- 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/
2. JIT a AOT překladače Pythonu
Klasický CPython byl původně navržen jako interpret bajtkódu virtuálního stroje Pythonu. Zdrojové kódy se před spuštěním přeložily do bajtkódu a ten byl interpretován, což je (podle očekávání) poměrně pomalé. Proto postupně vznikly překladače zajišťující překlad zdrojových kódu Pythonu do nativního kódu. Takový překlad lze provést v jediném kroku nebo ve více fázích (frontend a backend překladače). Navíc se od sebe překladače liší podle toho, zda je překlad proveden ještě před spuštěním aplikace (AOT – Ahead of Time) nebo ve chvíli, kdy se načítají jednotlivé balíčky nebo když se detekuje „horká“ část kódu (JIT – Just in Time). Každý ze zmíněných způsobů, tedy interpretace, JIT překlad a AOT překlad, má své výhody a zápory a obecně asi není možné říci, který z nich je obecně výhodnější (záleží na konkrétním způsobu použití).
Bez dalších podrobností si můžeme jednotlivé AOT a JIT překladače vypsat:
| Překladač | Typ | Poznámka |
|---|---|---|
| Cython | AOT | nadmnožina Pythonu |
| Numba | JIT (+AOT) | viz další text |
| PyPy | JIT | |
| mypyc | AOT | nepovinné typové informace |
| Psyco | JIT | již neudržován, zmíněn pouze pro úplnost |
| Unladen Swallow | JIT pro CPython | již nevyvíjen, některé vlastnosti jsou přímo v CPythonu |
3. AOT překladač mypyc
Příkladem AOT překladače Pythonu je nástroj nazvaný mypyc. Ten je součástí instalace Mypy. Nástroj mypyc pracuje následujícím způsobem:
- Je provedena analýza původního Pythonovského kódu s jeho překladem do AST
- Na základě AST (s typy) je vygenerován zdrojový kód v jazyku C
- Tento kód je přeložen překladačem céčka (typicky gcc)
- Výsledkem je sdílená knihovna strukturovaná takovým způsobem, že ji lze využít jako C-extension jazyka Python
Výslednou sdílenou knihovnu (so – shared object) lze naimportovat do Pythonovského skriptu (odpovídá totiž specifikaci C-extension). Na rozdíl od dalších AOT překladačů tedy není výsledkem přímo spustitelný soubor, což opět přináší některé přednosti, ale i zápory.
4. Instalace a první spuštění překladače mypyc
Překladač mypyc je součástí balíčku Mypy (podpora a kontrola typových informací v Pythonu). Samotná instalace je snadná (předpokladem ovšem je, že je již nainstalován CPython verze 3.5 či vyšší):
$ pip3 install --user mypy
Collecting mypy
Downloading mypy-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (12.2 MB)
|████████████████████████████████| 12.2 MB 779 kB/s
Requirement already satisfied: typing-extensions>=3.10 in ./.local/lib/python3.8/site-packages (from mypy) (4.4.0)
Collecting tomli>=1.1.0; python_version < "3.11"
Downloading tomli-2.0.1-py3-none-any.whl (12 kB)
Collecting mypy-extensions>=1.0.0
Downloading mypy_extensions-1.0.0-py3-none-any.whl (4.7 kB)
Installing collected packages: tomli, mypy-extensions, mypy
Successfully installed mypy-1.3.0 mypy-extensions-1.0.0 tomli-2.0.1
Většinou je ovšem ještě nutné provést upgrade balíčku nazvaného typing_extensions:
$ pip3 install --upgrade --user typing_extensions
Collecting typing_extensions
Downloading typing_extensions-4.6.3-py3-none-any.whl (31 kB)
Installing collected packages: typing-extensions
Attempting uninstall: typing-extensions
Found existing installation: typing-extensions 4.4.0
Uninstalling typing-extensions-4.4.0:
Successfully uninstalled typing-extensions-4.4.0
Successfully installed typing-extensions-4.6.3
Otestování, zda je Mypy spustitelný:
$ mypy --version mypy 1.3.0 (compiled: yes)
Nás ovšem dnes bude zajímat především mypyc. I ten by měl být dostupný:
$ mypyc --help
usage: mypy [-h] [-v] [-V] [more options; see below]
[-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...]
Mypy is a program that will type check your Python code.
Pass in any files or folders you want to type check. Mypy will
recursively traverse any provided folders to find .py files:
$ mypy my_program.py my_src_folder
For more information on getting started, see:
- https://mypy.readthedocs.io/en/stable/getting_started.html
For more details on both running mypy and using the flags below, see:
...
...
...
5. Příklad použití překladače mypyc
Podívejme se nyní na způsob použití AOT překladače mypyc. Necháme si přeložit výpočet Mandelbrotovy množiny (podrobnosti budou uvedeny níže). Výpočet přitom není nijak optimalizován a prozatím AOT překladači ani nijak „nepomůžeme“ specifikací datových typů. Už dopředu je možné naznačit, že kvůli chybějícím typovým informacím nebude výsledek ideální, ovšem úpravy (přidání typových informací) kupodivu v tomto případě nebudou nijak složité:
import palette_mandmap
from sys import argv
def calc_mandelbrot(width, height, maxiter, palette):
print("P3")
print("{w} {h}".format(w=width, h=height))
print("255")
cy = -1.5
for y in range(0, height):
cx = -2.0
for x in range(0, width):
zx = 0.0
zy = 0.0
i = 0
while i < maxiter:
zx2 = zx * zx
zy2 = zy * zy
if zx2 + zy2 > 4.0:
break
zy = 2.0 * zx * zy + cy
zx = zx2 - zy2 + cx
i += 1
r = palette[i][0]
g = palette[i][1]
b = palette[i][2]
print("{r} {g} {b}".format(r=r, g=g, b=b))
cx += 3.0/width
cy += 3.0/height
if len(argv) < 4:
width = 512
height = 512
maxiter = 255
else:
width = int(argv[1])
height = int(argv[2])
maxiter = int(argv[3])
calc_mandelbrot(width, height, maxiter, palette_mandmap.palette)
AOT překlad se provede takto:
$ mypyc mandelbrot_5.py
Samotný překlad je relativně rychlý:
running build_ext building 'mandelbrot_5' extension creating build/temp.linux-x86_64-cpython-311 creating build/temp.linux-x86_64-cpython-311/build gcc -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -fPIC -I/usr/local/lib64/python3.11/site-packages/mypyc/lib-rt -I/usr/include/python3.11 -c build/__native.c -o build/temp.linux-x86_64-cpython-311/build/__native.o -O3 -g1 -Werror -Wno-unused-function -Wno-unused-label -Wno-unreachable-code -Wno-unused-variable -Wno-unused-command-line-argument -Wno-unknown-warning-option -Wno-unused-but-set-variable -Wno-ignored-optimization-argument -Wno-cpp creating build/lib.linux-x86_64-cpython-311 gcc -shared -Wl,-z,relro -Wl,--as-needed -Wl,-z,now -Wl,--build-id=sha1 -Wl,-z,relro -Wl,--as-needed -Wl,-z,now -Wl,--build-id=sha1 build/temp.linux-x86_64-cpython-311/build/__native.o -L/usr/lib64 -o build/lib.linux-x86_64-cpython-311/mandelbrot_5.cpython-311-x86_64-linux-gnu.so copying build/lib.linux-x86_64-cpython-311/mandelbrot_5.cpython-311-x86_64-linux-gnu.so ->
Výsledkem by měl být tento soubor (koncovka napovídá, že se jedná o sdílenou knihovnu):
-rwxrwxr-x 1 ptisnovs ptisnovs 366664 Nov 18 10:22 mandelbrot_5.cpython-38-x86_64-linux-gnu.so
Současně se v průběhu překladu vytvořil i podadresář build, který však nebudeme dále potřebovat. Použít ho lze ve chvíli, kdy je nutné výše zmíněnou knihovnu nainstalovat (což však nebudeme potřebovat):
build
├── lib.linux-x86_64-cpython-311
│ └── mandelbrot_5.cpython-311-x86_64-linux-gnu.so
├── __native.c
├── __native.h
├── __native_internal.h
├── ops.txt
├── setup.py
└── temp.linux-x86_64-cpython-311
└── build
└── __native.o
4 directories, 8 files
6. Spuštění kódu
Samotné spuštění kódu, který by měl vykreslit Mandelbrotou množinu, se provede takto:
$ python3 -c "import mandelbrot_5" 400 400 255 > mandelbrot.ppm
Výsledkem by měl být tento obrázek:
Obrázek 1: Výsledek běhu programu, jenž vznikl AOT překladem Pythonovského kódu.
O tom, že výše uvedený příkaz nebude importovat soubor mandelbrot5.py, ale nativní sdílenou knihovnu, se můžeme přesvědčit například použitím nástroje strace:
$ strace python3 -c "import mandelbrot_5" 400 400 255 > mandelbrot.ppm
V zobrazených zprávách lze najít mj. i tyto informace ukazující, že se skutečně načetla sdílená knihovna:
execve("/usr/bin/python3", ["python3", "-c", "import mandelbrot_5"], 0x7ffd0f6a5670 /* 60 vars */) = 0
...
...
...
stat("/tmp/ramdisk/mandelbrot_5.cpython-38-x86_64-linux-gnu.so", {st_mode=S_IFREG|0775, st_size=366664, ...}) = 0
futex(0x7f4d65b320c8, FUTEX_WAKE_PRIVATE, 2147483647) = 0
openat(AT_FDCWD, "/tmp/ramdisk/mandelbrot_5.cpython-38-x86_64-linux-gnu.so", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0\205\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0775, st_size=366664, ...}) = 0
mmap(NULL, 248488, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f4d64fbf000
mmap(0x7f4d64fc6000, 192512, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x7000) = 0x7f4d64fc6000
mmap(0x7f4d64ff5000, 20480, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x36000) = 0x7f4d64ff5000
mmap(0x7f4d64ffa000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3a000) = 0x7f4d64ffa000
close(3) = 0
7. Zjištění, které části kódu bylo možné přeložit optimálním způsobem
Překladač mypyc se snaží o překlad veškerého zdrojového kódu, který je mu předán. Ovšem pokud nemá k dispozici typové informace, nebude výsledek optimální. Proto je vhodné zjistit, které části kódu nejsou přeloženy optimálně kvůli tomu, že jsme AOT překladači nedodali všechny důležité typové informace. Toto zjištění je snadné a můžeme ho získat v několika formátech (textový, HTML, formát kompatibilní s Coberturou atd.).
Celkový přehled lze získat příkaze:
$ mypyc --txt-report report mandelbrot_5.py
Výsledkem bude tato tabulka:
Mypy Type Check Coverage Summary ================================ Script: index +--------------+-------------------+--------+ | Module | Imprecision | Lines | +--------------+-------------------+--------+ | mandelbrot_5 | 63.41% imprecise | 41 LOC | +--------------+-------------------+--------+ | Total | 63.41% imprecise | 41 LOC | +--------------+-------------------+--------+
Podrobnější čitelný výsledek získáme příkazem:
$ mypyc --html-report html mandelbrot_5.py
Výsledkem bude HTML stránka, kde jsou zvýrazněny řádky přeložené optimálně i ty řádky, které nebylo možné optimálně přeložit:
Obrázek 2: Optimálně a neoptimálně přeložené řádky původního skriptu.
8. Doplnění všech typových informací
Ve skutečnosti je doplnění typových informací v našem konkrétním případě relativně snadné a změny se týkají pouze hlavičky funkce s výpočtem fraktálu. Náš kód bude po úpravách vypadat takto (změna se týká jen zvýrazněného řádku):
import palette_mandmap
from sys import argv
from typing import Tuple
def calc_mandelbrot(width: int, height: int, maxiter: int, palette: Tuple[Tuple[int, int, int], ...]) -> None:
print("P3")
print("{w} {h}".format(w=width, h=height))
print("255")
cy = -1.5
for y in range(0, height):
cx = -2.0
for x in range(0, width):
zx = 0.0
zy = 0.0
i = 0
while i < maxiter:
zx2 = zx * zx
zy2 = zy * zy
if zx2 + zy2 > 4.0:
break
zy = 2.0 * zx * zy + cy
zx = zx2 - zy2 + cx
i += 1
r = palette[i][0]
g = palette[i][1]
b = palette[i][2]
print("{r} {g} {b}".format(r=r, g=g, b=b))
cx += 3.0/width
cy += 3.0/height
if len(argv) < 4:
width = 512
height = 512
maxiter = 255
else:
width = int(argv[1])
height = int(argv[2])
maxiter = int(argv[3])
calc_mandelbrot(width, height, maxiter, palette_mandmap.palette)
Snadno lze zjistit, že je nyní optimálně přeložen veškerý kód:
Mypy Type Check Coverage Summary ================================ Script: index +--------------+-------------------+--------+ | Module | Imprecision | Lines | +--------------+-------------------+--------+ | mandelbrot_6 | 0.00% imprecise | 44 LOC | +--------------+-------------------+--------+ | Total | 0.00% imprecise | 44 LOC | +--------------+-------------------+--------+
Obrázek 3: Všechny řádky jsou nyní přeloženy optimálním způsobem.
9. Poznámka k hodnotě __name__
Při AOT překladu již existujících skriptů můžeme narazit na jeden problém, který je ovšem snadno řešitelný (a vlastně se nejedná o problém, ale o potenciálně výhodnou vlastnost). Týká se to hodnoty __name__, která má při běžném spuštění Pythonovského skriptu hodnotu __main__, o čemž se můžeme snadno přesvědčit spuštěním následujícího jednořádkového skriptu:
print(__name__)
Při přímém spuštění (interpretaci) tohoto skriptu dostaneme očekávaný výstup:
$ python3 print_name.py __main__
Při importu se ovšem vypíše odlišné jméno:
$ python3 -c "import print_name" print_name
Totéž ovšem platí po AOT překladu, protože výslednou sdílenou knihovnu nelze „spustit“ ale importovat:
$ mypyc print_name.py running build_ext copying build/lib.linux-x86_64-3.8/print_name.cpython-38-x86_64-linux-gnu.so -> $ rm print_name.py $ python3 -c "import print_name" print_name
Co to znamená v praxi? Skripty, jejichž kód, který se má spustit, je „schovaný“ před importem, budou pouze importovány, ale takový kód se nespustí:
def main():
...
...
...
if __name__ == "__main__":
main() # nespustí se
10. JIT a AOT překladač Numba
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í – vliv JIT překladu ostatně uvidíme na výsledku benchmarků.
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.
Pro účely benchmarků si nainstalujeme i nástroj Numba:
$ 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
11. (Mikro)benchmark: výpočty s využitím FP hodnot
Vzhledem k tomu, že se v dnešním benchmarku budeme do značné míry snažit vyhnout měření rychlosti knihovních funkcí, bude celý benchmark ve skutečnosti provádět prakticky jen výpočty s výpisem výsledku výpočtů na standardní výstup. Ten bude přesměrován do souboru, protože výsledkem výpočtů budou bitmapy ve formátu Portable Pixel Map (viz [1]). Samozřejmě, že i výpis hodnot na standardní výstup znamená nutnost volání knihovních funkcí, ovšem oproti počtu numerických operací se bude jednat o minimální čas, což je možné zjistit například profilerem, popř. úplným zákazem výstupu (to však nechceme – musíme i optimalizující překladač donutit, aby volání funkcí z kódu zcela neodstranil).
Celý benchmark spočívá ve výpočtu barev pixelů Mandelbrotovy množiny, přičemž rozlišení výsledného rastrového obrázku i maximální počet iterací bude možné zvolit z příkazového řádku. Budeme testy měřit kód s vnořenými smyčkami, podmínkou ve smyčce, FP výpočty atd. a nikoli například rychlost implementace přidávání prvků do seznamu či zjišťování, zda řetězec odpovídá regulárnímu výrazu (k tomu se můžeme dostat později). Následuje výpis zdrojového kódu benchmarku. Kód je přitom napsán tak, aby byl kompatibilní s Pythonem 3.x, Jythonem, mypyc i s Numbou:
#!/usr/bin/env python
# vim: set fileencoding=utf-8
import palette_mandmap
from sys import argv, exit
def calc_mandelbrot(width, height, maxiter, palette):
print("P3")
print("{w} {h}".format(w=width, h=height))
print("255")
cy = -1.5
for y in range(0, height):
cx = -2.0
for x in range(0, width):
zx = 0.0
zy = 0.0
i = 0
while i < maxiter:
zx2 = zx * zx
zy2 = zy * zy
if zx2 + zy2 > 4.0:
break
zy = 2.0 * zx * zy + cy
zx = zx2 - zy2 + cx
i += 1
r = palette[i][0]
g = palette[i][1]
b = palette[i][2]
print("{r} {g} {b}".format(r=r, g=g, b=b))
cx += 3.0/width
cy += 3.0/height
if __name__ == "__main__":
if len(argv) < 4:
width = 512
height = 512
maxiter = 255
else:
width = int(argv[1])
height = int(argv[2])
maxiter = int(argv[3])
calc_mandelbrot(width, height, maxiter, palette_mandmap.palette)
V benchmarku se používá i další modul nazvaný palette_mandmap.py, který obsahuje barvovou paletu (palette, color map). Paleta byla získána ze známého (a dnes již vlastně historického) programu Fractint a obsahuje 256 trojic hodnot R, G, B. Samotná paleta nemá prakticky žádný vliv na naměřené hodnoty, ale výsledné obrázky jsou díky ní hezčí.
12. Nativní varianta benchmarku přepsaná do ANSI C
Jen pro zajímavost (a taktéž kvůli benchmarkům) se podívejme na to, jak je možné totožný algoritmus implementovat v ANSI C. Jedná se z velké části o přímý přepis původního algoritmu bez dalších optimalizací, které céčko umožňuje:
#include <stdlib.h>
#include <stdio.h>
#include "palette_mandmap.h"
void calc_mandelbrot(unsigned int width, unsigned int height, unsigned int maxiter, unsigned char palette[][3])
{
puts("P3");
printf("%d %d\n", width, height);
puts("255");
double cy = -1.5;
int y;
for (y=0; y<height; y++) {
double cx = -2.0;
int x;
for (x=0; x<width; x++) {
double zx = 0.0;
double zy = 0.0;
unsigned int i = 0;
while (i < maxiter) {
double zx2 = zx * zx;
double zy2 = zy * zy;
if (zx2 + zy2 > 4.0) {
break;
}
zy = 2.0 * zx * zy + cy;
zx = zx2 - zy2 + cx;
i++;
}
unsigned char *color = palette[i];
unsigned char r = *color++;
unsigned char g = *color++;
unsigned char b = *color;
printf("%d %d %d\n", r, g, b);
cx += 3.0/width;
}
cy += 3.0/height;
}
}
int main(int argc, char **argv)
{
if (argc < 4) {
puts("usage: ./mandelbrot width height maxiter");
return 1;
}
int width = atoi(argv[1]);
int height = atoi(argv[2]);
int maxiter = atoi(argv[3]);
calc_mandelbrot(width, height, maxiter, palette);
return 0;
}
13. Varianty upravené pro Mypyc
Výše uvedený pythonovský skript byl pro potřeby mypyc upraven jen minimálně. Pouze došlo k tomu, že se výpočet přímo spustí i v případě importu skriptu (nebo jeho varianty přeložené do nativního kódu). Takto upravený zdrojový kód byl vypsán v páté kapitole. Druhá úprava spočívá v přidání typových informací. Takto upravený zdrojový kód byl vypsán v osmé kapitole a opět ho tedy nebudeme znovu uvádět. Žádné další úpravy specifické pro mypyc nebyly provedeny.
Obrázek 4: Detail Mandelbrotovy množiny vypočtené skriptem přeloženým nástrojem Mypyc do nativního kódu.
14. Varianty upravené pro Numbu
Nepatrné úpravy zdrojového kódu benchmarku si vyžaduje i nástroj Numba (opět platí – v IT nedostaneme prakticky nic zadarmo).
Prvním krokem při praktickém použití nástroje Numba je zápis anotace @jit před funkcí, u které potřebujeme, aby ji překladač optimalizoval v čase běhu. Nejdříve musíme do příslušného modulu anotaci naimportovat, což je snadné:
from numba import jit
Následně tuto anotaci použijeme – žádné další kroky není zapotřebí provést:
@jit
def calc_mandelbrot(width, height, maxiter, palette):
...
...
...
Jedním z potenciálně problematických prvků našeho benchmarku je použití standardní pythonovské funkce print. Při JIT překladu totiž může Numba použít dvě varianty této funkce – původní „univerzální“ pythonovskou variantu s téměř nepřebernými možnostmi formátování a volitelnými parametry nebo zjednodušenou variantu umožňující tisk číselných hodnot nebo řetězců (více info o nativních funkcích Numby je uvedeno na stránce https://numba.pydata.org/numba-doc/dev/reference/pysupported.html). Obecně platí, že pokud použijeme zjednodušenou variantu funkce print, bude JIT schopen přeložit celou JITovanou funkci do strojového kódu.
Náš kód tedy na dvou místech nepatrně upravíme a využijeme tak velké flexibility formátu PNM, v němž je možné použít jako oddělovač buď konec řádku nebo libovolný bílý znak (jednou z nepříjemných vlastností nativní varianty printu je to, že se vždy tiskne konec řádku).
Původní kód:
print("P3")
print("{w} {h}".format(w=width, h=height))
print("255")
Nový kód:
print("P3")
print(width)
print(height)
print("255")
Původní kód:
r = palette[i][0]
g = palette[i][1]
b = palette[i][2]
print("{r} {g} {b}".format(r=r, g=g, b=b))
Nový kód:
r = palette[i][0] g = palette[i][1] b = palette[i][2] print(r) print(g) print(b)
Nová podoba benchmarku tedy bude následující:
#!/usr/bin/env python
# vim: set fileencoding=utf-8
import palette_mandmap
from sys import argv, exit
from numba import jit
@jit
def calc_mandelbrot(width, height, maxiter, palette):
print("P3")
print(width)
print(height)
print("255")
cy = -1.5
for y in range(0, height):
cx = -2.0
for x in range(0, width):
zx = 0.0
zy = 0.0
i = 0
while i < maxiter:
zx2 = zx * zx
zy2 = zy * zy
if zx2 + zy2 > 4.0:
break
zy = 2.0 * zx * zy + cy
zx = zx2 - zy2 + cx
i += 1
r = palette[i][0]
g = palette[i][1]
b = palette[i][2]
print(r)
print(g)
print(b)
cx += 3.0/width
cy += 3.0/height
if __name__ == "__main__":
if len(argv) < 4:
width = 512
height = 512
maxiter = 255
else:
width = int(argv[1])
height = int(argv[2])
maxiter = int(argv[3])
calc_mandelbrot(width, height, maxiter, palette_mandmap.palette)
JIT ve skutečnosti může pracovat ve dvou režimech, které se nazývají object mode a nopython mode. V prvním režimu je kód vytvářený JITem schopný zpracovat libovolné objekty (resp. reference na ně) a v případě potřeby se v kódu volá C API Pythonu pro zpracování těchto objektů. Pokud je tento režim použit, nebude se rychlost výsledného programu příliš odlišovat od běhu interpretru. Z tohoto důvodu se většinou budeme chtít tomuto režimu vyhnout – pokud to půjde. Naproti tomu druhý režim (nopython mode) generuje kód, v němž se C API nevolá a všechny proměnné a argumenty nesou hodnoty nativních typů (int, double atd.). Tento režim si můžeme vynutit anotací @jit(nopython=True), ovšem s několika omezeními, které se týkají například výše zmíněné funkce print (ostatně si zkuste sami vyzkoušet, co se stane, pokud tuto anotaci přidáme do prvního příkladu).
Benchmark upravíme následujícím způsobem:
@jit(nopython=True) def calc_mandelbrot(width, height, maxiter, palette):
Pro jistotu si uveďme celý kód, jak s novou anotací, tak i s použitím zjednodušené nativní funkce print:
#!/usr/bin/env python
# vim: set fileencoding=utf-8
import palette_mandmap
from sys import argv, exit
from numba import jit
@jit(nopython=True)
def calc_mandelbrot(width, height, maxiter, palette):
print("P3")
print(width)
print(height)
print("255")
cy = -1.5
for y in range(0, height):
cx = -2.0
for x in range(0, width):
zx = 0.0
zy = 0.0
i = 0
while i < maxiter:
zx2 = zx * zx
zy2 = zy * zy
if zx2 + zy2 > 4.0:
break
zy = 2.0 * zx * zy + cy
zx = zx2 - zy2 + cx
i += 1
r = palette[i][0]
g = palette[i][1]
b = palette[i][2]
print(r)
print(g)
print(b)
cx += 3.0/width
cy += 3.0/height
if __name__ == "__main__":
if len(argv) < 4:
width = 512
height = 512
maxiter = 255
else:
width = int(argv[1])
height = int(argv[2])
maxiter = int(argv[3])
calc_mandelbrot(width, height, maxiter, palette_mandmap.palette)
15. Výsledky benchmarků: rychlost výpočtů
Nyní již máme hned několik variant zdrojových kódů benchmarků:
- Původní zdrojový kód pro klasický interpret Pythonu
- Kód přepsaný do ANSI C
- Kód určený pro AOT překlad nástrojem mypyc bez typových informací (liší se způsobem spuštění)
- Kód určený pro AOT překlad nástrojem mypyc s přidanými typovými informacemi
- Numba: kód, do něhož byla pouze přidána anotace @jit
- Numba: varianta s jednodušší (nativní) funkcí print
- Numba: varianta s jednodušší (nativní) funkcí print a anotací @jit(nopython=True)
Tyto zdrojové kódy použijeme pro spuštění celkem dvanácti benchmarků, jejichž označení a význam je zapsán v tabulce:
| Označení | Stručný popis benchmarku |
|---|---|
| native | benchmark přepsaný do ANSI C, překlad bez optimalizací |
| native optim | benchmark přepsaný do ANSI C, překlad s optimalizacemi |
| python 3.8 | benchmark spuštěný standardním CPythonem verze 3.8 |
| python 3.9 | benchmark spuštěný standardním CPythonem verze 3.9 |
| python 3.10 | benchmark spuštěný standardním CPythonem verze 3.10 |
| python 3.11 | benchmark spuštěný standardním CPythonem verze 3.11 |
| python 3.12 | benchmark spuštěný standardním CPythonem verze 3.12 |
| mypyc no type hints | kód určený pro AOT překlad nástrojem mypyc bez typových informací |
| mypyc with type hints | kód určený pro AOT překlad nástrojem mypyc s přidanými typovými informacemi |
| numba 2 | Numba: kód, do něhož byla pouze přidána anotace @jit |
| numba 3 | Numba: varianta s jednodušší (nativní) funkcí print |
| numba 4 | Numba: varianta s jednodušší (nativní) funkcí print a anotací @jit(nopython=True) |
První graf ukazuje časy běhu všech benchmarků. Zvolil jsem liniový graf, který naznačuje, jak dobré či špatné jsou jednotlivé implementace nejenom pro dlouhé výpočty, ale i při započítání času inicializace procesu. Na horizontální osu jsou vyneseny velikosti bitmap (x×y pixelů), na vertikální osu pak doby běhu v sekundách:
Obrázek 5: Časy běhu všech benchmarků v závislosti na požadovaném rozlišení výsledné bitmapy.
Na druhém grafu jsou zobrazeny časy běhu benchmarků pro malé bitmapy. Právě zde nehrají výpočty prakticky žádnou významnou roli, mnohem více se zde projeví inicializace procesu. To v případě nástroje Numba znamená JIT překlad. Jak je patrné, ten trvá přibližně 4 sekundy:
Obrázek 6: Časy běhu benchmarků pro malé bitmapy. Zde se nejvíce projeví čas inicializace procesu.
Pro větší velikosti bitmap se již kromě času JITování projeví i časy výpočtů. Stále se však pohybujeme v celkovém času dosahujícím jednotky sekund, takže JITování zde hraje spíše negativní roli:
Obrázek 7: Časy běhu benchmarků pro středně velké bitmapy.
Pro ještě větší bitmapy však dostaneme zcela odlišné výsledky, protože JITovaný kód zde překonává jak klasické interpretry (což se dalo čekat), tak i Mypy!:
Obrázek 8: Časy běhu benchmarků pro velké bitmapy.
Ještě se podívejme na pohled na „sweet spot“, kde rychlost JITovaného kódu překročí rychlost kódu interpretovaného:
Obrázek 9: Oblast, v níž se JITovaný kód stává rychlejším, než kód interpretovaný.
V úvodní části článku jsme se taktéž zmínili o tom, že Python 3.11 (a 3.12) je obecně rychlejší, než předchozí interpretry Pythonu – tedy alespoň podle tvrzení tvůrců. Podívejme se, zda je tomu skutečně tak:
Obrázek 10: Porovnání rychlostí interpretrů Pythonu (CPython 3.x).
16. Výsledky benchmarků: spotřeba operační paměti
V dnešním článku jsme se sice primárně zaměřili na měření rychlosti FP výpočtů (v jediném vláknu), ale možná stojí zato se podívat i na spotřebu operační paměti pro jednotlivé skupiny benchmarků. Zde je situace snadná, protože všechny benchmarky lze zhruba rozdělit do dvou kategorií – nativní kód, kód běžící v libovolné verzi CPythonu a nativní kód přeložený pomocí mypyc na straně jedné a benchmarky JITované nástrojem Numba na straně druhé:
Obrázek 11: Spotřeba operační paměti.
Víme již, že Python 3.11 a Python 3.12 je rychlejší, než jeho předchůdci. Na druhou stranu mají tyto dvě nejnovější verze nepatrně větší spotřebu operační paměti. Nejedná se ovšem o nic podstatného, jak je to ostatně patrné z dalšího grafu (ovšem pro porovnání Numbou porovnejte s grafem předchozím):
Obrázek 12: Spotřeba operační paměti, zaměřeno na klasické interpretry Pythonu.
17. Zdrojové kódy benchmarků použitých v dnešním článku
Pro změření výkonnosti různých variant spuštění projektů naprogramovaných v Pythonu bylo použito celkem sedm verzí zdrojových kódů benchmarku. První verze je určena pro klasický CPython (my jsme využili verze 3.8 až 3.12), další tři verze jsou určeny pro použití společně s JIT překladačem Numba. Následují dvě verze určené pro AOT překladač Mypy a konečně poslední verze byla přepsána do ANSI C, abychom mohli porovnat, jak může být sémanticky totožný kód rychlejší při použití odlišného ekosystému:
18. Repositář s demonstračními příklady pro nástroj Numba
Všechny demonstrační příklady ukazující vlastnosti nástroje Numba naleznete v repositáři https://github.com/tisnik/most-popular-python-libs:
19. Repositář s demonstračními příklady pro nástroj Mypy
Všechny Pythonovské skripty, které jsme si popsali v článcích o Mypy, naleznete opět na adrese https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady (pro jejich spuštění je pochopitelně nutné mít nainstalován balíček mypy společně s Pythonem alespoň 3.7):
20. Odkazy na Internetu
- Python 3.12: More Faster and More Efficient Python
https://medium.com/@HeCanThink/python-3–12-more-faster-and-more-efficient-python-b636f00b047 - 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/ - mypy homepage
https://www.mypy-lang.org/ - mypy documentation
https://mypy.readthedocs.io/en/stable/ - Mypy na PyPi Optional static typing for Python
https://pypi.org/project/mypy/ - 5 Reasons Why You Should Use Type Hints In Python
https://www.youtube.com/watch?v=dgBCEB2jVU0 - Python Typing – Type Hints & Annotations
https://www.youtube.com/watch?v=QORvB-_mbZ0 - What Problems Can TypeScript Solve?
https://www.typescriptlang.org/why-create-typescript - How to find code that is missing type annotations?
https://stackoverflow.com/questions/59898490/how-to-find-code-that-is-missing-type-annotations - Do type annotations in Python enforce static type checking?
https://stackoverflow.com/questions/54734029/do-type-annotations-in-python-enforce-static-type-checking - Understanding type annotation in Python
https://blog.logrocket.com/understanding-type-annotation-python/ - Static type checking with Mypy — Perfect Python
https://www.youtube.com/watch?v=9gNnhNxra3E - Static Type Checker for Python
https://github.com/microsoft/pyright - Differences Between Pyright and Mypy
https://github.com/microsoft/pyright/blob/main/docs/mypy-comparison.md - 4 Python type checkers to keep your code clean
https://www.infoworld.com/article/3575079/4-python-type-checkers-to-keep-your-code-clean.html - Pyre: A performant type-checker for Python 3
https://pyre-check.org/ - „Typing the Untyped: Soundness in Gradual Type Systems“ by Ben Weissmann
https://www.youtube.com/watch?v=uJHD2×yv7×o - Covariance and contravariance (computer science)
https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science) - Functional Programming: Type Systems
https://www.youtube.com/watch?v=hy1wjkcIBCU - A Type System From Scratch – Robert Widmann
https://www.youtube.com/watch?v=IbjoA5×VUq0 - „Type Systems – The Good, Bad and Ugly“ by Paul Snively and Amanda Laucher
https://www.youtube.com/watch?v=SWTWkYbcWU0 - Type Systems: Covariance, Contravariance, Bivariance, and Invariance explained
https://medium.com/@thejameskyle/type-systems-covariance-contravariance-bivariance-and-invariance-explained-35f43d1110f8 - Statická vs. dynamická typová kontrola
https://www.root.cz/clanky/staticka-dynamicka-typova-kontrola/ - Typový systém
https://cs.wikipedia.org/wiki/Typov%C3%BD_syst%C3%A9m - Comparison of programming languages by type system
https://en.wikipedia.org/wiki/Comparison_of_programming_languages_by_type_system - Flow
https://flow.org/ - TypeScript
https://www.typescriptlang.org/ - Sorbet
https://sorbet.org/ - Pyright
https://github.com/microsoft/pyright - Mypy: Type hints cheat sheet
https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html - PEP 484 – Type Hints
https://peps.python.org/pep-0484/ - 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/ - Nuitka
https://github.com/Nuitka/Nuitka




