Obsah
1. Sledování využití paměti Pythonovských aplikací nástrojem Memory profiler
2. Instalace nástroje Memory profiler
3. Sledované demonstrační aplikace
4. Spuštění aplikace se sledováním práce s pamětí
5. Vyhodnocení a vizualizace obsazení paměti sledovanou aplikací
7. Sledování a analýza druhé i třetí aplikace
9. Získání informací o alokované paměti na úrovni programových řádků
11. Úprava aplikace přidávající a ubírající prvky ze seznamu
13. Měření spotřeby paměti u složitější aplikace
15. Spotřeba paměti u aplikací běžících ve větším množství procesů
16. Ukázka jednoduché aplikace, která spouští více souběžně běžících procesů
17. Výsledky analýzy: celková spotřeba operační paměti
18. Výsledky analýzy: spotřeba rozdělená podle jednotlivých procesů
19. Repositář s demonstračními příklady
1. Sledování využití paměti Pythonovských aplikací nástrojem Memory profiler
V dnešním článku si ukážeme, jakým způsobem lze využít nástroj nazvaný Memory profiler pro sledování využití paměti aplikacemi, které jsou naprogramovány v jazyku Python. Navážeme tak na předchozí článek nazvaný Detekce velikosti hodnot uložených v operační paměti a spravovaných interpretrem Pythonu, který byl zaměřen na nástroje (resp. přesněji řečeno balíčky) Pympler a taktéž Guppy.
Připomeňme si, že jak Pympler, tak i Guppy dokážou poměrně přesně zjistit, jaké hodnoty (objekty) a s jakou velikostí jsou v daný okamžik používány interpretrem Pythonu. Dnes popisovaný nástroj Memory profiler je poněkud odlišný, protože navíc umožňuje, aby se informace o obsazené paměti získávala kontinuálně po celou dobu běhu aplikace, s následnou vizualizací obsazení paměti ve formě jednoduchých grafů. Navíc je možné do jednoho grafu vykreslit několik průběhů získaných během několika běhů sledované aplikace (například s nastavením různých parametrů, s přepnutím algoritmů atd.). Navíc je možné získat informace o spotřebě paměti s velmi dobrou přesností (resp. granularitou) – až na úrovni jednotlivých programových řádků (můžeme tedy velmi snadno detekovat ty programové řádky, v nichž se alokují velké paměťové bloky – což budou typicky operace s objemnými objekty).
2. Instalace nástroje Memory profiler
Díky tomu, že je balíček s nástrojem Memory profiler zaregistrovaný na PyPi, je instalace tohoto balíčku triviální a můžeme ji provést (pro právě přihlášeného uživatele) následujícím příkazem:
$ pip3 install --user memory_profiler
Samotná instalace proběhne prakticky okamžitě:
Collecting memory_profiler Downloading memory_profiler-0.61.0-py3-none-any.whl (31 kB) Requirement already satisfied: psutil in /usr/lib/python3/dist-packages (from memory_profiler) (5.5.1) Installing collected packages: memory-profiler Successfully installed memory-profiler-0.61.0
Součástí instalace nástroje Memory Profiler by měl být i spustitelný soubor nazvaný mprof, takže si otestujme, zda existuje a je skutečně spustitelný:
$ whereis mprof mprof: /home/ptisnovs/.local/bin/mprof
Pokud byl soubor nalezen, můžeme ho spustit a vypsat si nápovědu:
$ mprof Usage: mprof <command> <options> <arguments> Available commands: run run a given command or python file attach alias for 'run --attach': attach to an existing process by pid or name rm remove a given file generated by mprof clean clean the current directory from files created by mprof list display existing profiles, with indices plot plot memory consumption generated by mprof run peak print the maximum memory used by an mprof run Type mprof <command> --help for usage help on a specific command. For example, mprof plot --help will list all plotting options.
$ echo $PATH /home/ptisnovs/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/opt/go/bin/:/home/ptisnovs/go/bin/:/home/ptisnovs/.local/bin
3. Sledované demonstrační aplikace
V navazujících kapitolách si základní možnosti nabízené nástrojem Memory profiler ukážeme na několika demonstračních „aplikacích“, které v průběhu své činnosti provádí alokaci paměti, popř. (i když v Pythonu nepřímo) její dealokaci. Mezi jednotlivé operace je vždy vložen příkaz sleep(2), což nám umožní lépe sledovat vliv jednotlivých příkazů na spotřebu operační paměti na grafech, které Memory profiler dokáže vykreslit.
První aplikace nejdříve v programové smyčce vytvoří delší řetězec, na který (po uplynutí dvou sekund) ztratí referenci, protože se do původní proměnné vloží odlišná hodnota (připomeňme si, že v proměnných jsou uloženy pouze reference na hodnoty, nikoli samotné hodnoty). Následně se provede alokace pole bajtů (bytearray) a po uplynutí dvou sekund ztratíme referenci i na toto pole. Úplný zdrojový kód této aplikace lze nalézt na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/memory_profiler/app1.py:
from time import sleep sleep(2) x = "*foo*" sleep(2) y = "" for i in range(200000): y += x print(len(y)) sleep(2) x = 0 y = 0 sleep(2) x = bytearray(1000000) sleep(2) x = 0 sleep(2)
Ve druhé demonstrační aplikaci se taktéž provádí konstrukce řetězce (z kratších částí) i konstrukce pole bajtů, ovšem tentokrát lokálně, v rámci funkcí. To znamená, že reference na řetězec, resp. na pole bajtů existuje pouze pro lokální proměnnou viditelnou uvnitř funkce. V praxi to znamená, že po opuštění funkce je reference ztracena a daná hodnota (objekt) může být z paměti odstraněna. Úplný zdrojový kód této aplikace lze nalézt na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/memory_profiler/app2.py:
from time import sleep def foo_construct(): x = "*foo*" sleep(2) y = "" for i in range(20000000): y += x print(len(y)) def bar_construct(): x = bytearray(100000000) print(len(x)) sleep(2) sleep(2) foo_construct() sleep(2) bar_construct() sleep(2)
Ve třetí demonstrační aplikaci se v rámci funkce (tedy opět lokálně) vytvoří seznam, do kterého se postupně přidávají nové prvky. Posléze začnou být prvky ze seznamu odstraňovány příkazem del. Úplný zdrojový kód této aplikace lze nalézt na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/memory_profiler/app3.py:
from time import sleep def foo_construct(): l = [] for i in range(10000000): l.append(i) if i % 100000 == 0: sleep(0.05) for i in range(10000000, 0, -1): del l[i-1] if i % 100000 == 0: sleep(0.05) sleep(2) sleep(2) foo_construct() sleep(2)
4. Spuštění aplikace se sledováním práce s pamětí
Nástroj Memory profiler je možné využít několika různými způsoby. Ten nejjednodušší způsob spočívá v tom, že se sledovaná aplikace nespustí přímo (tedy přesněji řečeno se nespustí její „vstupní“ Pythonovský skript), ale je spuštěna přes mprof. Memory profiler v takovém případě začne sledovat paměť, kterou virtuální stroj Pythonu alokuje a tuto informaci bude postupně ukládat do automaticky vytvořeného textového souboru:
$ mprof run app1.py mprof: Sampling memory every 0.1s running new process running as a Python program... 1000000
Po doběhnutí aplikace by se měl v adresáři, z něhož bylo sledování spuštěno, objevit soubor, jehož název odpovídá globu „mprofile_*.dat“, kde se za hvězdičku doplňuje časové razítko spuštění aplikace ve formátu YYYYMMDDhhmmss. To v mém konkrétním případě znamená, že soubor s názvem mprofile_20230512085526 byl získán při sledování aplikace, která byla spuštěna 12.5.2023 v 8:55:26. Jedná se o textový soubor, který obsahuje informace o alokované paměti, přičemž každý řádek byl do souboru přidán přibližně každých 100 milisekund.
Tento soubor si pochopitelně můžeme prohlédnout, ovšem nebudeme ho muset zpracovávat přímo, protože pro zpracování nám nástroj Memory profiler poskytuje několik technik:
$ head mprofile_20230512085526.dat CMDLINE /usr/bin/python3 app1.py MEM 3.687500 1683874526.4900 MEM 18.460938 1683874526.5908 MEM 18.460938 1683874526.6918 MEM 18.460938 1683874526.7928 MEM 18.460938 1683874526.8935 MEM 18.460938 1683874526.9946 MEM 18.460938 1683874527.0954 MEM 18.460938 1683874527.1963 MEM 18.460938 1683874527.2973 ... ... ... $ tail mprofile_20230512085526.dat MEM 19.316406 1683874537.5908 MEM 19.316406 1683874537.6915 MEM 19.316406 1683874537.7925 MEM 19.316406 1683874537.8932 MEM 19.316406 1683874537.9942 MEM 19.316406 1683874538.0951 MEM 19.316406 1683874538.1961 MEM 19.316406 1683874538.2971 MEM 19.316406 1683874538.3981 MEM 19.316406 1683874538.4991
5. Vyhodnocení a vizualizace obsazení paměti sledovanou aplikací
První informací, kterou můžeme velmi snadno získat, je nejvyšší obsazená kapacita operační paměti. Tuto informaci nám poskytne příkaz mprof peak, který nalezne všechny soubory vygenerované Memory profilerem a zobrazí informace z posledního souboru:
$ mprof peak Using last profile data. mprofile_20230512085526.dat 19.801 MiB
$ mprof peak *.dat mprofile_20230512085526.dat 19.801 MiB mprofile_20230512090927.dat 114.148 MiB mprofile_20230512092839.dat 405.219 MiB
Vraťme se ovšem zpět k našemu jedinému souboru vytvořenému Memory profilerem. Víme již, že tento soubor obsahuje informace o objemu obsazené paměti s granularitou 0,1 sekundy a přesně tyto informace lze vizualizovat do podoby jednoduchého grafu:
$ mprof plot Using last profile data.
Výsledek by měl vypadat následovně:
Obrázek 1: Výsledek vizualizace obsazení paměti sledovanou aplikací.
6. Analýza grafu
Povšimněte si, jak průběh grafu uvedeného v předchozí kapitole koresponduje se zdrojovým kódem testované aplikace (uvádím zjednodušený výpis bez zbytečných importů atd.):
sleep(2) x = "*foo*" sleep(2) y = "" for i in range(200000): y += x sleep(2) x = 0 y = 0 sleep(2) x = bytearray(1000000) sleep(2) x = 0 sleep(2)
Příkazy sleep, které aplikaci pozastaví na dvě sekundy, nám umožňují zjistit, kdy a v jakém objemu došlo k alokaci paměti, ovšem s tím, že prvotní nárůst na cca 18,46 MiB jde na vrub samotnému interpretru Pythonu a jeho infrastruktuře. Pokusme se tedy jednotlivé části kódu spojit s grafem:
Obrázek 2: Části kódu spuštěné na začátku každé „periody“ viditelné v grafu.
7. Sledování a analýza druhé i třetí aplikace
Podívejme se nyní na analýzu druhé aplikace, v níž se provádí alokace paměti lokálně, v rámci funkcí. Konkrétně je paměť alokována pro objekty, které jsou lokální a můžeme tedy počítat s tím, že po opuštění dané funkce dojde i k uvolnění příslušného paměťového bloku (ovšem tento předpoklad nemusí být splněn, což uvidíme dále):
from time import sleep def foo_construct(): x = "*foo*" sleep(2) y = "" for i in range(20000000): y += x print(len(y)) def bar_construct(): x = bytearray(100000000) print(len(x)) sleep(2) sleep(2) foo_construct() sleep(2) bar_construct() sleep(2)
Aplikaci spustíme společně s jejím sledováním následovně:
$ mprof run app2.py mprof: Sampling memory every 0.1s running new process running as a Python program... 100000000 100000000
Nyní bude nejvyšší obsazená kapacita operační paměti již mnohem vyšší, než tomu bylo u aplikace první:
$ mprof peak Using last profile data. mprofile_20230512090927.dat 114.148 MiB
Mnohem lépe bude tento nárůst viditelný na grafu:
$ mprof plot Using last profile data.
Výsledek bude výsledek vypadat zcela odlišně:
Obrázek 3: Obsazení operační paměti druhou testovanou aplikací.
Podobně lze analyzovat aplikaci, v níž se postupně do seznamu přidávají prvky a poté se opět ubírají (od konce seznamu – jinak se výsledku nedočkáte :-):
from time import sleep def foo_construct(): l = [] for i in range(10000000): l.append(i) if i % 100000 == 0: sleep(0.05) for i in range(10000000, 0, -1): del l[i-1] if i % 100000 == 0: sleep(0.05) sleep(2) sleep(2) foo_construct() sleep(2)
Naměřený výsledek na mém počítači vypadá takto:
Obrázek 4: Obsazení operační paměti třetí testovanou aplikací.
8. Analýza grafu
Analýza grafu zobrazeného v sedmé kapitole na třetím obrázku je nepatrně složitější, a to z toho důvodu, že programová smyčka:
y = "" for i in range(20000000): y += x
nebyla provedena ihned, ale trvala přibližně jednu sekundu. Vliv jednotlivých příkazů na potřebnou kapacitu operační paměti bude následující:
Obrázek 5: Vliv jednotlivých příkazů ve druhé testované aplikaci na kapacitu obsazené operační paměti.
Analýza grafu zobrazeného na čtvrtém obrázku je naproti tomu triviální:
Obrázek 6: Vliv jednotlivých příkazů ve třetí testované aplikaci na kapacitu obsazené operační paměti.
9. Získání informací o alokované paměti na úrovni programových řádků
Nástroj Memory profiler dokáže získat informace o tom, jakým způsobem je paměť obsazována či naopak uvolňována, až na úrovni jednotlivých programových řádků. Výsledky sice nebývají zcela dobře pochopitelné (jak ostatně uvidíme dále), ovšem alespoň upozorní na ty části programového kódu, na něž se může vývojář při optimalizacích zaměřit. Vzhledem k tomu, že se jedná o již velmi detailní informace (jejichž získání je relativně zdlouhavé), je nutné označit celé funkce nebo metody, jejichž analýza nás zajímá, dekorátorem @profile. Můžeme si například upravit první testovací příklad následujícím způsobem, kterým se označí obě funkce ve skriptu:
from time import sleep @profile def foo_construct(): x = "*foo*" y = "" for i in range(200000): y += x print(len(y)) del x del y @profile def bar_construct(): x = bytearray(10000000) print(len(x)) del x foo_construct() bar_construct()
10. Výsledky analýzy
Nyní je nutné aplikaci spustit takovým způsobem, že se jí předá jméno modulu Memory profileru (to proto, aby byla načtena definice dekorátoru @profile). Konkrétně vypadá spuštění naší testovací aplikace následovně:
$ python3 -m memory_profiler app4.py 1000000 10000000
Výsledkem bude po dokončení běhu aplikace následující tabulka, v níž je patrné, jak se měnila obsazená kapacita operační paměti po provedení jednotlivých řádků. Navíc se zobrazí i celkový počet opakování daného řádku:
Filename: app4.py Line # Mem usage Increment Occurrences Line Contents ============================================================= 4 18.332 MiB 18.332 MiB 1 @profile 5 def foo_construct(): 6 18.332 MiB 0.000 MiB 1 x = "*foo*" 7 8 18.332 MiB 0.000 MiB 1 y = "" 9 22.137 MiB -85881.301 MiB 200001 for i in range(200000): 10 22.137 MiB -85877.496 MiB 200000 y += x 11 12 22.004 MiB -0.133 MiB 1 print(len(y)) 13 14 22.004 MiB 0.000 MiB 1 del x 15 19.570 MiB -2.434 MiB 1 del y Filename: app4.py Line # Mem usage Increment Occurrences Line Contents ============================================================= 18 19.570 MiB 19.570 MiB 1 @profile 19 def bar_construct(): 20 29.105 MiB 9.535 MiB 1 x = bytearray(10000000) 21 29.105 MiB 0.000 MiB 1 print(len(x)) 22 19.570 MiB -9.535 MiB 1 del x
11. Úprava aplikace přidávající a ubírající prvky ze seznamu
Naprosto stejnou úpravu provedeme i u naší třetí testovací aplikace, která postupně přidává prvky do seznamu a posléze je zase odebírá. Úprava je ve skutečnosti triviální – postačuje totiž pouze přidat dekorátor @profile před testovanou funkcí:
from time import sleep @profile def foo_construct(): l = [] for i in range(100000): l.append(i) for i in range(100000, 0, -1): del l[i-1] foo_construct()
12. Výsledky analýzy
Výsledky analýzy provedené Memory profilerem opět mohou být poněkud matoucí (zejména sloupec Increment), ovšem i tak dobře naznačují, na kterých řádcích zdrojového kódu se provádí alokace paměti (přidávání prvků do seznamu) a její dealokace (odstraňování prvků z konce seznamu):
$ python3 -m memory_profiler app5.py Filename: app5.py Line # Mem usage Increment Occurrences Line Contents ============================================================= 4 18.312 MiB 18.312 MiB 1 @profile 5 def foo_construct(): 6 18.312 MiB 0.000 MiB 1 l = [] 7 22.664 MiB 0.008 MiB 100001 for i in range(100000): 8 22.664 MiB 4.344 MiB 100000 l.append(i) 9 10 22.664 MiB -147352.746 MiB 100001 for i in range(100000, 0, -1): 11 22.664 MiB -147352.746 MiB 100000 del l[i-1]
$ python3 -m memory_profiler app5.py Filename: app5.py Line # Mem usage Increment Occurrences Line Contents ============================================================= 4 18.211 MiB 18.211 MiB 1 @profile 5 def foo_construct(): 6 18.211 MiB 0.000 MiB 1 l = [] 7 57.199 MiB 0.117 MiB 1000001 for i in range(1000000): 8 57.199 MiB 38.871 MiB 1000000 l.append(i) 9 10 57.199 MiB -17654430.246 MiB 1000001 for i in range(1000000, 0, -1): 11 57.199 MiB -17654430.246 MiB 1000000 del l[i-1]
13. Měření spotřeby paměti u složitější aplikace
Měření spotřeby operační paměti samozřejmě můžeme provádět i u složitějších aplikací. Jako příklad jsem vybral (nepatrně) rozsáhlejší aplikaci použitou v článcích o populární knihovně Pygame. V této aplikaci se vytváří několik ploch (surface) a teoreticky by mělo být možné detekovat alokaci paměti pro tyto potenciálně velké bloky dat. Vyzkoušejme si tedy, zda a jak je výsledek získaný Memory Profilerem čitelný a užitečný:
#!/usr/bin/python # vim: set fileencoding=utf-8 import pygame import sys # Nutno importovat kvůli konstantám QUIT atd. from pygame.locals import * # Velikost okna aplikace WIDTH = 320 HEIGHT = 240 # Třída představující sprite zobrazený jako jednobarevný čtverec. class BlockySprite(pygame.sprite.Sprite): # Konstruktor def __init__(self, color, size, x, y): # Nejprve je nutné zavolat konstruktor předka, # tj. konstruktor třídy pygame.sprite.Sprite: pygame.sprite.Sprite.__init__(self) # Vytvoření obrázku představujícího vizuální obraz spritu: self.image = pygame.Surface([size, size]) self.image.fill(color) # Vytvoření obalového obdélníku # (velikost se získá z rozměru obrázku) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y # Počáteční rychlost spritu self.speed_x = 0 self.speed_y = 0 # Nastavení barvy spritu, který kolidoval s hráčem def yellowColor(self): self.image.fill(YELLOW) # Nastavení barvy spritu, který nekolidoval s hráčem def grayColor(self): self.image.fill(GRAY) # Inicializace knihovny Pygame pygame.init() clock = pygame.time.Clock() # Vytvoření okna pro vykreslování display = pygame.display.set_mode([WIDTH, HEIGHT]) # Nastavení titulku okna pygame.display.set_caption("Pygame test #22") # Konstanty s n-ticemi představujícími základní barvy BLACK = (0, 0, 0) RED = (255, 0, 0) GRAY = (128, 128, 128) YELLOW = (255, 255, 0) # Objekt sdružující všechny sprity all_sprites = pygame.sprite.Group() # Objekt sdružující všechny sprity kromě hráče all_sprites_but_player = pygame.sprite.Group() # Vytvoření několika typů spritů # barva x y velikost wall1 = BlockySprite(GRAY, 50, 10, 10) wall2 = BlockySprite(GRAY, 15, 100, 100) wall3 = BlockySprite(GRAY, 15, 100, 150) wall4 = BlockySprite(GRAY, 15, 200, 100) wall5 = BlockySprite(GRAY, 15, 200, 150) wall6 = BlockySprite(GRAY, 15, 150, 100) wall7 = BlockySprite(GRAY, 15, 150, 150) player = BlockySprite(RED, 40, WIDTH / 2 - 20, HEIGHT / 2 - 20) # Přidání několika dalších spritů do seznamu # (jen jeden sprite - ten poslední - bude ve skutečnosti pohyblivý) all_sprites.add(wall1) all_sprites.add(wall2) all_sprites.add(wall3) all_sprites.add(wall4) all_sprites.add(wall5) all_sprites.add(wall6) all_sprites.add(wall7) all_sprites.add(player) # Seznam všech nepohyblivých spritů all_sprites_but_player.add(wall1) all_sprites_but_player.add(wall2) all_sprites_but_player.add(wall3) all_sprites_but_player.add(wall4) all_sprites_but_player.add(wall5) all_sprites_but_player.add(wall6) all_sprites_but_player.add(wall7) # Posun všech spritů ve skupině na základě jejich rychlosti @profile def move_sprites(sprite_group, playground_width, playground_height): for sprite in sprite_group: # Posun spritu sprite.rect.x = sprite.rect.x + sprite.speed_x sprite.rect.y = sprite.rect.y + sprite.speed_y # Kontrola, zda sprite nenarazil do okrajů okna if sprite.rect.x < 0: sprite.rect.x = 0 sprite.speed_x = 0 if sprite.rect.x + sprite.rect.width > playground_width: sprite.rect.x = playground_width - sprite.rect.width sprite.speed_x = 0 if sprite.rect.y < 0: sprite.rect.y = 0 sprite.speed_y = 0 if sprite.rect.y + sprite.rect.height > playground_height: sprite.rect.y = playground_height - sprite.rect.height sprite.speed_y = 0 # Vykreslení celé scény na obrazovku @profile def draw_scene(display, background_color, sprite_group): # Vyplnění plochy okna černou barvou display.fill(background_color) # Vykreslení celé skupiny spritů do bufferu sprite_group.draw(display) # Obnovení obsahu obrazovky (překlopení zadního a předního bufferu) pygame.display.update() # Změna barvy spritu na základě kolize s hráčem @profile def change_colors(sprite_group, hit_list): # Projít všemi sprity ze skupiny, kterou detekovala kolizní funkce for sprite in sprite_group: if sprite in hit_list: sprite.yellowColor() else: sprite.grayColor() # Zjistí kolize spritu se "stěnami" (nepohyblivými sprity) @profile def check_collisions(player, sprite_group): # Vytvoření seznamu spritů, které kolidují s hráčem hit_list = pygame.sprite.spritecollide(player, sprite_group, False) # Změna barev kolidujících spritů change_colors(sprite_group, hit_list) collisions = len(hit_list) # Přenastavení titulku okna caption = "Pygame test #22: collisions " + str(collisions) pygame.display.set_caption(caption) @profile def mainLoop(): while True: # Načtení a zpracování všech událostí z fronty for event in pygame.event.get(): if event.type == QUIT: pygame.quit() sys.exit() if event.type == KEYDOWN: if event.key == K_ESCAPE: pygame.quit() sys.exit() # Stiskem kurzorových kláves je možné měnit směr pohybu spritu elif event.key == pygame.K_LEFT: player.speed_x = -3 elif event.key == pygame.K_RIGHT: player.speed_x = +3 elif event.key == pygame.K_UP: player.speed_y = -3 elif event.key == pygame.K_DOWN: player.speed_y = +3 if event.type == KEYUP: # Puštění kurzorových kláves vede k zastavení pohybu spritu if event.key == pygame.K_LEFT: player.speed_x = 0 elif event.key == pygame.K_RIGHT: player.speed_x = 0 elif event.key == pygame.K_UP: player.speed_y = 0 elif event.key == pygame.K_DOWN: player.speed_y = 0 move_sprites(all_sprites, display.get_width(), display.get_height()) check_collisions(player, all_sprites_but_player) draw_scene(display, BLACK, all_sprites) clock.tick(20) mainLoop() # finito
14. Výsledky měření
Podívejme se nyní na výsledky měření získané po spuštění aplikace z předchozí kapitoly a několika posunech spritu po obrazovce s využitím kurzorových kláves.
Hned v první sledované funkci vidíme alokaci paměti u řádku s dekorátorem @profile, což souvisí s inicializací virtuálního stroje Pythonu i knihovny Pygame (a zobrazenému incrementu vůbec nevěřte :-). Na dalších řádcích funkce move_sprites nedochází k měřitelným změnám v obsazení operační paměti:
Filename: sprites.py Line # Mem usage Increment Occurrences Line Contents ============================================================= 105 87.551 MiB 10934.941 MiB 125 @profile 106 def move_sprites(sprite_group, playground_width, playground_height): 107 87.551 MiB 0.000 MiB 1125 for sprite in sprite_group: 108 # Posun spritu 109 87.551 MiB 0.000 MiB 1000 sprite.rect.x = sprite.rect.x + sprite.speed_x 110 87.551 MiB 0.000 MiB 1000 sprite.rect.y = sprite.rect.y + sprite.speed_y 111 # Kontrola, zda sprite nenarazil do okrajů okna 112 87.551 MiB 0.000 MiB 1000 if sprite.rect.x < 0: 113 sprite.rect.x = 0 114 sprite.speed_x = 0 115 87.551 MiB 0.000 MiB 1000 if sprite.rect.x + sprite.rect.width > playground_width: 116 sprite.rect.x = playground_width - sprite.rect.width 117 sprite.speed_x = 0 118 87.551 MiB 0.000 MiB 1000 if sprite.rect.y < 0: 119 sprite.rect.y = 0 120 sprite.speed_y = 0 121 87.551 MiB 0.000 MiB 1000 if sprite.rect.y + sprite.rect.height > playground_height: 122 sprite.rect.y = playground_height - sprite.rect.height 123 sprite.speed_y = 0
Prakticky totéž platí i u další sledované funkce draw_scene, ovšem s tím rozdílem, že můžeme vidět alokaci paměti u volání metody Surface.update() (což bychom neočekávali):
Filename: sprites.py Line # Mem usage Increment Occurrences Line Contents ============================================================= 127 87.551 MiB 10934.941 MiB 125 @profile 128 def draw_scene(display, background_color, sprite_group): 129 # Vyplnění plochy okna černou barvou 130 87.551 MiB 0.000 MiB 125 display.fill(background_color) 131 # Vykreslení celé skupiny spritů do bufferu 132 87.551 MiB 0.000 MiB 125 sprite_group.draw(display) 133 # Obnovení obsahu obrazovky (překlopení zadního a předního bufferu) 134 87.551 MiB 0.355 MiB 125 pygame.display.update()
I třetí měřená funkce change_colors neprovádí žádnou měřitelnou, resp. detekovatelnou alokaci paměti, což je jen dobře:
Filename: sprites.py Line # Mem usage Increment Occurrences Line Contents ============================================================= 138 87.551 MiB 10934.941 MiB 125 @profile 139 def change_colors(sprite_group, hit_list): 140 # Projít všemi sprity ze skupiny, kterou detekovala kolizní funkce 141 87.551 MiB 0.000 MiB 1000 for sprite in sprite_group: 142 87.551 MiB 0.000 MiB 875 if sprite in hit_list: 143 87.551 MiB 0.000 MiB 111 sprite.yellowColor() 144 else: 145 87.551 MiB 0.000 MiB 764 sprite.grayColor()
I další výsledek je matoucí, protože na řádku 154 se volá již změřená funkce change_colors, ve které se zcela jistě žádná alokace paměti neprovádí:
Filename: sprites.py Line # Mem usage Increment Occurrences Line Contents ============================================================= 149 87.551 MiB 10934.941 MiB 125 @profile 150 def check_collisions(player, sprite_group): 151 # Vytvoření seznamu spritů, které kolidují s hráčem 152 87.551 MiB 0.000 MiB 125 hit_list = pygame.sprite.spritecollide(player, sprite_group, False) 153 # Změna barev kolidujících spritů 154 87.551 MiB 10934.941 MiB 125 change_colors(sprite_group, hit_list) 155 87.551 MiB 0.000 MiB 125 collisions = len(hit_list) 156 # Přenastavení titulku okna 157 87.551 MiB 0.000 MiB 125 caption = "Pygame test #22: collisions " + str(collisions) 158 87.551 MiB 0.000 MiB 125 pygame.display.set_caption(caption)
Povšimněte si, že ve funkci představující smyčku pro zpracování událostí, se dealokuje určitá část operační paměti při ukončování aplikace, konkrétně po zavolání funkce pygame.quit (ostatní hodnoty ve sloupci Increment jsou opět zcela matoucí):
Filename: sprites.py Line # Mem usage Increment Occurrences Line Contents ============================================================= 161 87.195 MiB 87.195 MiB 1 @profile 162 def mainLoop(): 163 while True: 164 # Načtení a zpracování všech událostí z fronty 165 87.551 MiB 0.000 MiB 149 for event in pygame.event.get(): 166 87.551 MiB 0.000 MiB 24 if event.type == QUIT: 167 pygame.quit() 168 sys.exit() 169 87.551 MiB 0.000 MiB 24 if event.type == KEYDOWN: 170 87.551 MiB 0.000 MiB 7 if event.key == K_ESCAPE: 171 86.102 MiB -1.449 MiB 1 pygame.quit() 172 86.102 MiB 0.000 MiB 1 sys.exit() 173 # Stiskem kurzorových kláves je možné měnit směr pohybu spritu 174 87.551 MiB 0.000 MiB 6 elif event.key == pygame.K_LEFT: 175 87.551 MiB 0.000 MiB 1 player.speed_x = -3 176 87.551 MiB 0.000 MiB 5 elif event.key == pygame.K_RIGHT: 177 87.551 MiB 0.000 MiB 2 player.speed_x = +3 178 87.551 MiB 0.000 MiB 3 elif event.key == pygame.K_UP: 179 87.551 MiB 0.000 MiB 1 player.speed_y = -3 180 87.551 MiB 0.000 MiB 2 elif event.key == pygame.K_DOWN: 181 87.551 MiB 0.000 MiB 2 player.speed_y = +3 182 87.551 MiB 0.000 MiB 23 if event.type == KEYUP: 183 # Puštění kurzorových kláves vede k zastavení pohybu spritu 184 87.551 MiB 0.000 MiB 6 if event.key == pygame.K_LEFT: 185 87.551 MiB 0.000 MiB 1 player.speed_x = 0 186 87.551 MiB 0.000 MiB 5 elif event.key == pygame.K_RIGHT: 187 87.551 MiB 0.000 MiB 2 player.speed_x = 0 188 87.551 MiB 0.000 MiB 3 elif event.key == pygame.K_UP: 189 87.551 MiB 0.000 MiB 1 player.speed_y = 0 190 87.551 MiB 0.000 MiB 2 elif event.key == pygame.K_DOWN: 191 87.551 MiB 0.000 MiB 2 player.speed_y = 0 192 193 87.551 MiB 10934.941 MiB 125 move_sprites(all_sprites, display.get_width(), display.get_height()) 194 87.551 MiB 10934.941 MiB 125 check_collisions(player, all_sprites_but_player) 195 87.551 MiB 10935.297 MiB 125 draw_scene(display, BLACK, all_sprites) 196 87.551 MiB 0.000 MiB 125 clock.tick(20)
15. Spotřeba paměti u aplikací běžících ve větším množství procesů
V závěrečné části dnešního článku si ukážeme, jakým způsobem je možné nástrojem Memory Profiler sledovat spotřebu operační paměti v případě, že Pythonovská aplikace (skript) spouští nějaké další procesy. V takovém případě se sleduje buď celková spotřeba operační paměti v daném okamžiku nebo individuální spotřeba jednotlivých procesů. Memory Profiler podporuje, jak ostatně uvidíme v dalších třech kapitolách, obě zmíněné možnosti.
16. Ukázka jednoduché aplikace, která spouští více souběžně běžících procesů
V článku Souběžné a paralelně běžící úlohy naprogramované v Pythonu jsme si mj. popsali i standardní modul pojmenovaný multiprocesses. Tento modul vývojáře do značné míry odstiňuje od nízkoúrovňových operací, tedy od samotného rozvětvení procesu (fork), spuštění nového interpretru a specifikace, jaký kód má tento interpret použít. Z pohledu vývojáře je totiž použití modulu multiprocessing velmi přímočaré – pouze se zvolí, jaká funkce se má zavolat v novém procesu a jaké mají být této funkci předány argumenty. Navíc modul multiprocessing programátorům nabízí mechanismy umožňující komunikaci mezi procesy a v neposlední řadě taktéž čekání na dokončení spuštěného synovského procesu.
A právě tento modul využijeme v dalším demonstračním příkladu, který v samostatných souběžně běžících synovských procesech spustí úlohy, které již známe – postupnou konstrukci řetězce z kratších částí, což je z hlediska spotřeby paměti velmi náročná operace. Navíc se (metoda join) čeká na dokončení těchto procesů:
from multiprocessing import Process from time import sleep def worker(r): x = "*foo*" y = "" for i in range(r): y += x print(len(y)) sleep(1) del y sleep(1) def main(): ps = [] for r in (10000000, 20000000, 30000000, 10000000): p = Process(target=worker, args=(r,)) p.start() ps.append(p) sleep(0.2) for p in ps: p.join() sleep(2) if __name__ == "__main__": print("Running main") main()
17. Výsledky analýzy: celková spotřeba operační paměti
Výše uvedený program po svém spuštění využil poměrně velké množství operační paměti, ovšem tato paměť nebyla alokována původním procesem, ale procesy synovskými (původní program vyžaduje přibližně 18 MiB pro virtuální stroj Pythonu, ovšem mnohem více paměti vyžadují pro svůj běh synovské procesy). Celkovou spotřebu paměti, což je důležitý údaj zejména pro administrátory, zjistíme Memory Profilerem při použití přepínače –include-children, který zajistí, že se spotřeba paměti pro celý strom procesů v dané okamžiky měření sečte:
$ mprof run --include-children multiprocesses.py mprof: Sampling memory every 0.1s running new process running as a Python program... Running main 50000000 50000000 100000000 150000000
Výsledky si opět necháme vykreslit do grafu:
$ mprof plot mprofile_20230513190727.dat
Výsledkem bude tento graf, z něhož je patrné, že celková spotřeba operační paměti dosahuje 400 MiB. Daný bod největší spotřeby paměti – peak – je na grafu zvýrazněn:
Obrázek 7: Celková spotřeba operační paměti zobrazená na grafu.
18. Výsledky analýzy: spotřeba rozdělená podle jednotlivých procesů
Graf s celkovou spotřebou operační paměti lze použít ve chvíli, kdy je zapotřebí změřit paměťové požadavky nasazované aplikace. Ovšem pro vývojáře může být mnohem užitečnější zjištění, jakou spotřebu paměti mají jednotlivé synovské procesy (a navíc v jakém čase). I tuto informaci lze s využitím Memory Profileru změřit, pouze je zapotřebí použít přepínač –multiprocess:
$ mprof run --multiprocess multiprocesses.py mprof: Sampling memory every 0.1s running new process Running main 50000000 50000000 100000000 150000000
Výsledkem nyní bude datový soubor obsahující odlišně zapsané informace (povšimněte si nových řádků začínajících na „CHLD“):
CMDLINE /usr/bin/python3 multiprocesses.py MEM 1.683594 1683997647.0138 MEM 10.714844 1683997647.1216 CHLD 21871 12.796875 1683997647.1277 MEM 10.714844 1683997647.2279 CHLD 21871 19.632812 1683997647.2337 MEM 10.718750 1683997647.3340 CHLD 21871 26.343750 1683997647.3401 CHLD 21872 13.832031 1683997647.3401 MEM 10.718750 1683997647.4405 CHLD 21871 32.804688 1683997647.4464 CHLD 21872 20.675781 1683997647.4464 ... ... ...
Z grafu, který Memory Profiler vytvoří, je nyní patrné, že každý synovský proces dosáhne nějaké maximální spotřeby paměti, v tomto stavu počká sekundu, dále je naalokovaný řetězec smazán (a paměť se uvolní), počká se další sekundu a až poté je proces následně je ukončen:
Obrázek 8: Spotřeba paměti rodičovského procesu i všech synovských procesů.
19. Repositář s demonstračními příklady
Zdrojové kódy všech prozatím popsaných demonstračních příkladů určených pro programovací jazyk Python 3 byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem stále velmi malý, dnes má velikost zhruba několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
20. Odkazy na Internetu
- Top 5 Python Memory Profilers
https://stackify.com/top-5-python-memory-profilers/ - Pympler na GitHubu
https://github.com/pympler/pympler - Pympler na PyPI
https://pypi.org/project/Pympler/ - Dokumentace k balíčku Pympler
https://pympler.readthedocs.io/en/latest/ - Guppy 3 na GitHubu
https://github.com/zhuyifei1999/guppy3/ - Guppy 3 na PyPI
https://pypi.org/project/guppy3/ - Memory Profiler na GitHubu
https://github.com/pythonprofilers/memory_profiler - Memory Profiler na PyPI
https://pypi.org/project/memory-profiler/ - How to use guppy/heapy for tracking down memory usage
https://smira.ru/wp-content/uploads/2011/08/heapy.html - Identifying memory leaks
https://pympler.readthedocs.io/en/latest/muppy.html#muppy - How do I determine the size of an object in Python?
https://stackoverflow.com/questions/449560/how-do-i-determine-the-size-of-an-object-in-python - Why is bool a subclass of int?
https://stackoverflow.com/questions/8169001/why-is-bool-a-subclass-of-int - Memory Management in Python
https://realpython.com/python-memory-management/ - Why do ints require three times as much memory in Python?
https://stackoverflow.com/questions/23016610/why-do-ints-require-three-times-as-much-memory-in-python - cpython/Include/cpython/longintrepr.h
https://github.com/python/cpython/blob/main/Include/cpython/longintrepr.h#L64 - sys — System-specific parameters and functions
https://docs.python.org/3/library/sys.html - Python 3.3 s flexibilní reprezentací řetězců
https://www.root.cz/clanky/interni-reprezentace-retezcu-v-ruznych-jazycich-od-pocitacoveho-praveku-po-soucasnost/#k17