Hlavní navigace

Detekce velikosti hodnot uložených v operační paměti a spravovaných interpretrem Pythonu

30. 3. 2023
Doba čtení: 37 minut

Sdílet

 Autor: Python
V ekosystému jazyka Python je mnohdy užitečné zjistit, jaký objem operační paměti zabírají hodnoty (objekty), s nimiž se v aplikacích psaných v Pythonu pracuje. Kupodivu se nejedná o zcela triviální úlohu.

Obsah

1. Detekce velikosti hodnot uložených v operační paměti a spravovaných interpretrem Pythonu

2. Standardní funkce getsizeof z balíčku sys

3. Zjištění velikosti hodnot standardních skalárních datových typů Pythonu

4. Velikosti objektů představujících řetězce

5. Zjištění velikosti hodnot uložených do standardních kolekcí Pythonu

6. Velikosti skalárních hodnot i kolekcí

7. Velikosti funkcí v operační paměti zjišťované pomocí getsizeof

8. Velikosti tříd a objektů zjišťované pomocí getsizeof

9. Balíček Pympler

10. Funkce poskytované balíčkem pympler.asizeof

11. Funkce asizeof.asizeof pro zjištění velikosti hodnot v operační paměti

12. Zjištění a výpis velikosti skalárních hodnot i kolekcí

13. Zobrazení podrobnější statistiky o velikostech hodnot

14. Získání skutečné velikosti funkcí, tříd a objektů uložených v operační paměti

15. Balíček guppy3

16. Přístup k informacím o objektech uložených na haldě (heap)

17. Proč se tedy liší velikosti hodnot True a False?

18. Způsob uložení celočíselných hodnot Pythonu

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Detekce velikosti hodnot uložených v operační paměti a spravovaných interpretrem Pythonu

Programovací jazyk Python se v prvních letech své existence používal především pro tvorbu pomocných systémových a jiných skriptů; jednalo se tedy o (čitelnější) náhradu za Perl, BASH (či jiný v té době existující shell) a AWK. Ovšem v současnosti se Python používá i v mnoha dalších oblastech informatiky, mnohdy i pro tvorbu rozsáhlých aplikací a služeb. A právě ve chvíli, kdy se Python nasazuje i ve velkých aplikacích popř. pokud se používá pro zpracování dat s velkým objemem, se často objevují požadavky na snížení paměťové náročnosti těchto systémů.

Z tohoto důvodu je vhodné alespoň zhruba vědět, jak velké oblasti paměti jsou obsazeny jednotlivými hodnotami, které se v aplikacích psaných v Pythonu používají. Jak však zjistit velikost objektů (resp. obecně řečeno velikost hodnot) uložených v RAM? K tomuto účelu slouží jedna funkce ze standardní knihovny a taktéž další pomocné balíčky, s nimiž se postupně seznámíme.

2. Standardní funkce getsizeof z balíčku sys

V případě, že postačuje zjistit pouze přibližné nároky jednotlivých objektů (resp. přesněji řečeno jejich hodnot) na dostupnou kapacitu operační paměti, není nutné instalovat žádné dodatečné balíčky, protože podobnou funkci nabízí i funkce getsizeof ze standardního balíčku sys:

from sys import getsizeof
 
help(getsizeof)

Nápověda k této funkci je velmi stručná a neobsahuje například informaci, zda vrácená velikost (v bajtech) je skutečnou velikostí objektu nebo se vrací velikost bloku paměti alokované pro zkoumaný objekt. To totiž není totéž – už jen kvůli zarovnání paměťových bloků:

Help on built-in function getsizeof in module sys:
 
getsizeof(...)
    getsizeof(object [, default]) -> int
 
    Return the size of object in bytes.

3. Zjištění velikosti hodnot standardních skalárních datových typů Pythonu

Způsob použití výše zmíněné funkce sys si můžeme otestovat na skriptu, v němž prozkoumáme velikost hodnot základních skalárních datových typů programovacího jazyka Python. Bude se konkrétně jednat o celá čísla (tedy long v Pythonu 3), numerické hodnoty s plovoucí řádovou čárkou, pravdivostní hodnoty, hodnotu None a o řetězce (které jsou kupodivu taktéž považovány za skalární hodnoty). Pro zajímavost jsme do skriptu přidali ještě hodnoty posledního skalárního typu – komplexních čísel:

from sys import getsizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(getsizeof(value), "\t", typename, "\t", value)
 
 
print_sizeof(0)
print_sizeof(1)
print_sizeof(42)
print_sizeof(2<<30)
print_sizeof(2<<60)
print()
 
print_sizeof(1.0)
print_sizeof(3.1415)
print()
 
print_sizeof(1+2j)
print_sizeof(1.2+3.4j)
print()
 
print_sizeof(True)
print_sizeof(False)
print_sizeof(None)
print()
 
print_sizeof("")
print_sizeof("f")
print_sizeof("fo")
print_sizeof("foo")
print_sizeof("foo bar")
print_sizeof("foo bar baz")
print_sizeof("foo bar baz xyz")
print_sizeof("foo bar baz xyzzy")

Zajímavé bude zjistit, jaké velikosti se vlastně vrací (viz první sloupec):

24       int             0
28       int             1
28       int             42
32       int             2147483648
36       int             2305843009213693952
 
24       float           1.0
24       float           3.1415
 
32       complex         (1+2j)
32       complex         (1.2+3.4j)
 
28       bool            True
24       bool            False
16       NoneType        None
 
49       str
50       str             f
51       str             fo
52       str             foo
56       str             foo bar
60       str             foo bar baz
64       str             foo bar baz xyz
66       str             foo bar baz xyzzy
Poznámka: povšimněte si například rozdílu mezi True a False. Proč se vrací odlišné hodnoty bude vysvětleno v sedmnácté a osmnácté kapitole. Taktéž je zajímavé, že celočíselné hodnoty mají (obecně) rozdílnou velikost, což si opět vysvětlíme v navazujícím textu.

4. Velikosti objektů představujících řetězce

Nejjednodušší je situace u řetězců, protože základní velikost objektu typu řetězec (přesněji prázdný řetězec) je 49 bajtů a každý další přidaný znak znamená, že se velikost objektu zvýší o jeden až čtyři bajty, v závislosti na konkrétních znacích uložených v řetězci.

V programovacím jazyku Python verze totiž 3.3 došlo k poměrně významné změně, která se týká způsobu interního uložení řetězců. Autoři Pythonu si totiž uvědomili, že na jednu stranu je sice důležité a velmi žádoucí podporovat Unicode (a to celé Unicode, tedy žádný subset typu BMP, vzhledem k normě GB 18030) ovšem v mnoha případech to vede k tomu, že se do operační paměti ukládá až čtyřikrát větší množství dat, než je skutečně nutné, protože mnoho řetězců používaných v každodenní praxi obsahuje pouze ASCII znaky (příkladem mohou být URL). Navíc větší množství dat uložených v paměti znamená, že se při manipulaci s nimi bude hůře využívat procesorová cache. Proto došlo v rámci PEP 393 k takové úpravě, která zajistí možnost uložení řetězců ve třech formátech, což je naznačeno v tabulce:

Šířka znaku Kódování Prefix při zápisu kódu
1 bajt Latin-1 \x
2 bajty UCS-2 \u
4 bajty UCS-4 \U

Tyto změny by měly být pro programátory i uživatele zcela transparentní, takže by se nemělo stát, že by například do řetězce původně uloženého s kódováním Latin-1 (nadmnožina ASCII) nešel uložit například znak v azbuce – ostatně řetězce jsou v Pythonu neměnitelné, takže se konverze provede v rámci prováděné operace automaticky.

Podívejme se nyní, jak je výběr formátu pro uložení řetězce prováděn při interpretaci řetězcového literálu. Nejprve importujeme modul sys, který nabízí funkci getsizeof():

>>> import sys

Zjistíme velikost objektu reprezentujícího prázdný řetězec. Tato velikost se může lišit podle verze Pythonu a použité architektury, nás však budou zajímat rozdíly oproti této hodnotě:

>>> sys.getsizeof("")
49

Zjistíme velikost objektu řetězce s jedním ASCII znakem (měla by být o jedničku vyšší, než hodnota předchozí) a taktéž velikost objektu řetězce s jedenácti znaky (bude se lišit o deset bajtů oproti hodnotě předchozí). Výsledek je zřejmý – každý znak je v tomto případě reprezentován jediným bajtem:

>>> sys.getsizeof("e")
50
>>> sys.getsizeof("e 123456789")
60

Nyní vytvoříme řetězec s ne-ASCII znakem. Velikost příslušného objektu se zvětší (opět nás však bude zajímat rozdíl oproti této velikosti, ne její absolutní hodnota):

>>> sys.getsizeof("ě")
76

Dále vypočteme velikost řetězce s ne-ASCII znakem, po němž následuje deset ASCII znaků. Vidíme, že každý znak je uložen ve dvou bajtech – prvním znakem byl určen interní formát řetězce:

>>> sys.getsizeof("ě 123456789")
96

Zkusme si nyní vytvořit řetězec s jediným znakem, který nepatří do BPM, tedy ho nelze reprezentovat v UCS-2. Posléze k tomuto znaku přidáme dalších deset znaků a snadno zjistíme, že v tomto případě je každý znak reprezentován čtyřmi bajty ((120–80)/10):

>>> sys.getsizeof("\U0001ffff")
80
>>> sys.getsizeof("\U0001ffff 123456789")
120

Jen pro zajímavost se můžeme podívat, jak celý objekt s řetězcem vypadá. U ASCII řetězců:

>>> bytearray((ctypes.c_byte*sys.getsizeof("Hello world!")).from_address(id("Hello world!")))
bytearray(b'\x02\x00\x00\x00\x00\x00\x00\x00\x00\x89\x96\x00
\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x17L\xc6c\x01
\xc0\x08a\xe4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00Hello world!\x00')

U řetězců reprezentovaných v UCS-2:

>>> bytearray((ctypes.c_byte*sys.getsizeof("ěščřžýáíéúů")).from_address(id("ěščřžýáíéúů")))
bytearray(b'\x02\x00\x00\x00\x00\x00\x00\x00\x00\x89\x96\x00
\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x98\xd8J\xd9
\xd5\xb7\xd0\x9d\xa8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b
\x01a\x01\r\x01Y\x01~\x01\xfd\x00\xe1\x00\xed\x00\xe9\x00\xfa
\x00o\x01\x00\x00')

5. Zjištění velikosti hodnot uložených do standardních kolekcí Pythonu

V dalším kroku použijeme funkci getsizeof pro zjištění velikosti kolekcí, tedy n-tic, seznamů a asociativních polí (můžet si do příkladu přidat i množiny):

from sys import getsizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(getsizeof(value), "\t", typename, "\t", value)
 
 
print_sizeof(())
print_sizeof((1,))
print_sizeof((1, 2))
print_sizeof((1, 2, 3))
print_sizeof((1, 2, 3, 4))
print_sizeof((1, 2, 3, 4, 5))
print_sizeof((1, 2, 3, 4, 5, 6))
print()
 
print_sizeof([])
print_sizeof([1])
print_sizeof([1, 2])
print_sizeof([1, 2, 3])
print_sizeof([1, 2, 3, 4])
print_sizeof([1, 2, 3, 4, 5])
print_sizeof([1, 2, 3, 4, 5, 6])
print()
 
print_sizeof({})
print_sizeof({1:1})
print_sizeof({1:1, 2:2})
print_sizeof({1:1, 2:2, 3:3})
print_sizeof({1:1, 2:2, 3:3, 4:4})
print_sizeof({1:1, 2:2, 3:3, 4:4, 5:5})
print_sizeof({1:1, 2:2, 3:3, 4:4, 5:5, 6:6})

Výsledky ukazují, jak se velikosti kolekcí postupně zvětšují společně se zvyšujícím se počtem prvků:

40       tuple           ()
48       tuple           (1,)
56       tuple           (1, 2)
64       tuple           (1, 2, 3)
72       tuple           (1, 2, 3, 4)
80       tuple           (1, 2, 3, 4, 5)
88       tuple           (1, 2, 3, 4, 5, 6)
 
56       list            []
64       list            [1]
72       list            [1, 2]
80       list            [1, 2, 3]
88       list            [1, 2, 3, 4]
96       list            [1, 2, 3, 4, 5]
104      list            [1, 2, 3, 4, 5, 7]
 
64       dict            {}
232      dict            {1: 1}
232      dict            {1: 1, 2: 2}
232      dict            {1: 1, 2: 2, 3: 3}
232      dict            {1: 1, 2: 2, 3: 3, 4: 4}
232      dict            {1: 1, 2: 2, 3: 3, 4: 4, 5: 5}
360      dict            {1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6}

Ukazují se však skutečně velikosti všech hodnot, nebo pouze velikosti referencí uložených v kolekcích (v Pythonu je vše reference)? To zjistíme snadno – zkusíme do kolekce uložit řetězce s milionem znaků:

from sys import getsizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(getsizeof(value), "\t", typename)
 
 
print_sizeof((1, 2))
print_sizeof((1, "?" * 1000000))
print()
 
print_sizeof([1, 2])
print_sizeof([1, "?" * 1000000])
print()
 
print_sizeof({1:1, 2:2})
print_sizeof({1:1, 2:"?" * 1000000})

Z výsledků je jasně patrné, že se velikosti samotných hodnot do výsledku nezapočítávají:

56       tuple
56       tuple
 
72       list
72       list
 
232      dict
232      dict

6. Velikosti skalárních hodnot i kolekcí

Hodnoty získané výše uvedenými skripty nám pomohly získat alespoň základní představu o tom, jaký objem RAM je potřeba pro uložení skalárních hodnot i prvků do základních kolekcí. Shrnutí platné pro současné verze Pythonu 3 (budoucí verze Pythonu mohou mít odlišné požadavky) je uvedeno v následující tabulce:

Datový typ Význam Minimální velikost Další růst
long celé číslo 28 +4 bajty pro každou mocninu 230
float číslo s plovoucí řádovou čárkou 24 ×
complex komplexní číslo s plovoucí řádovou čárkou 32 ×
bool True nebo False 24/28 ×
NoneType hodnota none 16 ×
       
string řetězec 49 +1 až 4 bajty pro každý další znak (již známe)
tuple n-tice 40 +8 bajtů pro každý další prvek n-tice
list seznam 56 +8 bajtů pro každý další prvek seznamu
       
set množina 216 5 prvků: 728, 19 prvků: 2264, …, 77 prvků: 8408, …
dict slovník 64 1 prvek 232, 6 prvků: 360; 22 prvků: 1184; …
Poznámka: z výše uvedené tabulky je patrné, že hodnoty můžeme zhruba rozdělit do tří skupin. V první skupině jsou skalární hodnoty s buď pevnou velikostí, nebo velikostí, která roste pro vyšší hodnoty (celá čísla). Ve druhé skupině nalezneme „lineární“ datové typy, jejichž velikosti rostou lineárně s počtem uložených prvků (nebo s počtem znaků pro řetězce). A konečně ve třetí skupině jsou množiny a slovníky, které pochopitelně musí taktéž růst s rostoucím počtem uložených prvků, ale růst není striktně lineární – paměť se totiž alokuje po větších blocích, což je v IT velmi častá forma optimalizace.

7. Velikosti funkcí v operační paměti zjišťované pomocí getsizeof

Velmi důležitým datovým typem je v programovacím jazyku Python funkce. Podobně jako v případě hodnot dalších typů může být v operační paměti uloženo (obecně) libovolné množství hodnot typu funkce. Vyzkoušejme si tedy, jakou velikost (chápáno jaké množství bajtů) funkce vrací standardní volání sys.getsizeof, a to pro různě definované funkce:

from sys import getsizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(getsizeof(value), "\t", typename, "\t", value)
 
 
def foo():
    pass
 
 
def bar(x, y):
    return x+y
 
 
def baz(x=0, y=1):
    print(x)
    print(y)
    return x+y
 
 
print_sizeof(print)
print_sizeof(foo)
print_sizeof(bar)
print_sizeof(baz)

Získané výsledky vypadají následovně:

72       builtin_function_or_method      <built-in function print>
136      function        <function foo at 0x7fcea02f8160>
136      function        <function bar at 0x7fcea02f81f0>
136      function        <function baz at 0x7fcea02f8280>
Poznámka: později uvidíme, že tyto velikosti nejsou vypočteny přesně, protože se vrací pouze velikost „slotu“ pro funkci.

8. Velikosti tříd a objektů zjišťované pomocí getsizeof

Standardní funkce sys.getsizeof může být použita i pro zjištění velikosti tříd (což jsou taktéž hodnoty) a instancí tříd neboli objektů. Opět se podívejme na jednoduchý příklad, v němž tyto hodnoty zjišťujeme:

from sys import getsizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(getsizeof(value), "\t", typename, "\t", value)
 
 
class C1:
    pass
 
 
class C2:
    def __init__(self):
        pass
 
class C3:
    def __init__(self):
        pass
 
    def foo(self, x):
        self.x=x
 
    def bar(self, y):
        self.y=y
 
 
o1 = C1()
o2 = C2()
o3 = C3()
 
print_sizeof(C1)
print_sizeof(o1)
 
print_sizeof(C2)
print_sizeof(o2)
 
print_sizeof(C3)
print_sizeof(o3)
 
o3.foo(42)
 
print_sizeof(C3)
print_sizeof(o3)
 
o3.bar(0)
 
print_sizeof(C3)
print_sizeof(o3)

Podívejme se na zjištěné a zobrazené velikosti objektů i tříd:

1064     type            <class '__main__.C1'>
48       C1              <__main__.C1 object at 0x7ff7f2e20430>
1064     type            <class '__main__.C2'>
48       C2              <__main__.C2 object at 0x7ff7f2e20040>
1064     type            <class '__main__.C3'>
48       C3              <__main__.C3 object at 0x7ff7f2e20190>
1064     type            <class '__main__.C3'>
48       C3              <__main__.C3 object at 0x7ff7f2e20190>
1064     type            <class '__main__.C3'>
48       C3              <__main__.C3 object at 0x7ff7f2e20190>

Popř. lze příklad nepatrně upravit tak, že naplníme atribut objektu tím samým objektem (resp. referencí na ten samý objekt):

from sys import getsizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(getsizeof(value), "\t", typename, "\t", value)
 
 
class C4:
    def __init__(self):
        pass
 
    def foo(self, x):
        self.x=x
 
 
o4 = C4()
 
print_sizeof(C4)
print_sizeof(o4)
 
o4.foo(o4)
 
print_sizeof(C4)
print_sizeof(o4)

Výsledky nyní budou vypadat následovně:

1064     type            <class '__main__.C4'>
48       C4              <__main__.C4 object at 0x7f7f2f0d6430>
1064     type            <class '__main__.C4'>
48       C4              <__main__.C4 object at 0x7f7f2f0d6430>

Naše znalosti o velikosti hodnot tedy můžeme doplnit takto:

Datový typ Význam Minimální velikost Další růst
func funkce 136  
class definice třídy 1064  
inst instance třídy 48 jako u slovníků pro každý další atribut
Poznámka: dále opět uvidíme, jak je možné zjistit velikost těchto hodnot přesnějším způsobem – výsledky získané přes sys.getsizeof totiž v tomto případě v žádném případě nejsou přesné.

9. Balíček Pympler

Největší předností výše popsané funkce sys.getsizeof je fakt, že se jedná o funkci ze standardní knihovny sys. To mj. znamená, že tuto funkci je možné zavolat prakticky kdykoli a kdekoli, a to bez nutnosti instalace nějakých dalších balíčků. Ovšem tato funkce není příliš dokonalá, protože například neumožňuje zjistit velikost hodnot v operační paměti po zarovnání, protože alokátor paměti alokuje nové bloky vždy na adresách dělitelných nějakou konstantou 4, 8 či 16 (podle systému a architektury) a tudíž vlastně nebudou využity bajty umístěné těsně za koncem bloku v případě, že velikost bloku není dělitelná výše uvedenou konstantou. A navíc má tato standardní funkce problémy se zjištěním velikosti tříd a objektů, protože automaticky nezapočítává velikosti jednotlivých atributů atd.

Existují však i další (ovšem nutno dodat, že již nestandardní) balíčky, které „opravují“ chování funkce sys.getsizeof a navíc nabízí uživatelům i další zajímavou a potenciálně užitečnou funkcionalitu. Tyto balíčky je však pochopitelně nutné nejdříve nainstalovat. Prvním z balíčků, o nichž se dnes alespoň ve stručnosti zmíníme, je balíček nazvaný Pympler. Vzhledem k tomu, že je 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) tímto příkazem:

$ pip3 install --user pympler

Samotná instalace proběhne prakticky okamžitě:

Collecting pympler
  Downloading Pympler-1.0.1-py3-none-any.whl (164 kB)
     |████████████████████████████████| 164 kB 1.5 MB/s
Installing collected packages: pympler
Successfully installed pympler-1.0.1
Poznámka: samozřejmě můžete provést instalaci i tohoto balíčku v rámci virtuálního prostředí Pythonu atd.

10. Funkce poskytované balíčkem pympler.asizeof

Samotný Pympler obsahuje několik podbalíčků, z nichž nás v dnešním článku bude zajímat především podbalíček nazvaný pympler.asizeof:

>>> from pympler import asizeof

Podbalíček pympler.asizeof v současné verzi obsahuje devět užitečných funkcí:

>>> help(asizeof)
Help on module pympler.asizeof in pympler:
 
NAME
    pympler.asizeof
 
DESCRIPTION
    This module exposes 9 functions and 2 classes to obtain lengths and
    sizes of Python objects (for Python 3.5 or later).
 
    Earlier versions of this module supported Python versions down to
    Python 2.2.  If you are using Python 3.5 or older, please consider
    downgrading Pympler.
 
    **Public Functions** [#unsafe]_
 
       Function **asizeof** calculates the combined (approximate) size
       in bytes of one or several Python objects.
 
       Function **asizesof** returns a tuple containing the (approximate)
       size in bytes for each given Python object separately.
 
       Function **asized** returns for each object an instance of class
       **Asized** containing all the size information of the object and
       a tuple with the referents [#refs]_.
 
       Functions **basicsize** and **itemsize** return the *basic-*
       respectively *itemsize* of the given object, both in bytes.  For
       objects as ``array.array``, ``numpy.array``, ``numpy.matrix``,
       etc. where the item size varies depending on the instance-specific
       data type, function **itemsize** returns that item size.
 
       Function **flatsize** returns the *flat size* of a Python object
       in bytes defined as the *basic size* plus the *item size* times
       the *length* of the given object.
 
       Function **leng** returns the *length* of an object, like standard
       function ``len`` but extended for several types.  E.g. the **leng**
       of a multi-precision int (or long) is the number of ``digits``
       [#digit]_.  The length of most *mutable* sequence objects includes
       an estimate of the over-allocation and therefore, the **leng** value
       may differ from the standard ``len`` result.  For objects like

V podbalíčku však nalezneme i další více či méně zajímavé hodnoty, které lze vypsat například takto:

from pympler import asizeof
 
for item in dir(asizeof):
    if item[0] != "_":
        print(item)

Výsledek:

ABCMeta
Asized
Asizer
Callable
Dict
List
Optional
Struct
Tuple
Types
Union
Weakref
adict
array
asized
asizeof
asizesof
basicsize
calcsize
curdir
finditer
flatsize
isbuiltin
isclass
iscode
isframe
isfunction
ismethod
ismodule
itemsize
leng
linesep
log
named_refs
numpy
refs
stack
stat
statvfs
sys
warnings

11. Funkce asizeof.asizeof pro zjištění velikosti hodnot v operační paměti

Nejužitečnější funkce z podbalíčku pympler.asizeof se jmenuje taktéž asizeof. Jedná se o v mnoha ohledech vylepšenou standardní funkci sys.getsizeof, která umožňuje vypočítat skutečné velikosti objektů uložených v operační paměti:

from pympler import asizeof
 
help(asizeof.asizeof)

Tato funkce má několik zajímavých přepínačů, z nichž některé si popíšeme v dalším textu:

Help on function asizeof in module pympler.asizeof:
 
asizeof(*objs, **opts)
    Return the combined size (in bytes) of all objects passed
    as positional arguments.
 
    The available options and defaults are:
 
         *above=0*      -- threshold for largest objects stats
 
         *align=8*      -- size alignment
 
         *clip=80*      -- clip ``repr()`` strings
 
         *code=False*   -- incl. (byte)code size
 
         *cutoff=10*    -- limit large objects or profiles stats
 
         *derive=False* -- derive from super type
 
         *frames=False* -- ignore stack frame objects
 
         *ignored=True* -- ignore certain types
 
         *infer=False*  -- try to infer types
 
         *limit=100*    -- recursion limit
 
         *stats=0*      -- print statistics

12. Zjištění a výpis velikosti skalárních hodnot i kolekcí

Nyní se pokusme upravit skripty pro zjišťování velikostí různých hodnot uložených v operační paměti takovým způsobem, aby se namísto standardní funkce sys-getsizeof volala funkce asizeof z podbalíčku pympler.asizeof. Úprava skriptu je ve skutečnosti minimální, ovšem umožní nám další úpravy (a taktéž výsledky se mohou lišit).

Začneme skriptem, v němž se zjišťují velikosti skalárních hodnot, a to včetně řetězců. Upravená varianta tohoto skriptu bude vypadat následovně:

from pympler import asizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(asizeof.asizeof(value), "\t", typename, "\t", value)
 
 
print_sizeof(0)
print_sizeof(1)
print_sizeof(42)
print_sizeof(2<<30)
print_sizeof(2<<60)
print()
 
print_sizeof(1.0)
print_sizeof(3.1415)
print()
 
print_sizeof(1+2j)
print_sizeof(1.2+3.4j)
print()
 
print_sizeof(True)
print_sizeof(False)
print_sizeof(None)
print()
 
print_sizeof("")
print_sizeof("f")
print_sizeof("fo")
print_sizeof("foo")
print_sizeof("foo bar")
print_sizeof("foo bar baz")
print_sizeof("foo bar baz xyz")
print_sizeof("foo bar baz xyzzy")

Podívejme se nyní na zjištěné a vypsané výsledky:

24       int             0
32       int             1
32       int             42
32       int             2147483648
40       int             2305843009213693952
 
24       float           1.0
24       float           3.1415
 
32       complex         (1+2j)
32       complex         (1.2+3.4j)
 
32       bool            True
24       bool            False
16       NoneType        None
 
56       str
56       str             f
56       str             fo
56       str             foo
56       str             foo bar
64       str             foo bar baz
64       str             foo bar baz xyz
72       str             foo bar baz xyzzy
Poznámka: povšimněte si, že velikosti jsou vždy dělitelné osmi, protože asizeof počítá se zarovnáním paměťových bloků.

V dalším kroku provedeme úpravu skriptu pro zjištění velikosti kolekcí:

from pympler import asizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(asizeof.asizeof(value), "\t", typename, "\t", value)
 
 
print_sizeof(())
print_sizeof((1,))
print_sizeof((1, 2))
print_sizeof((1, 2, 3))
print_sizeof((1, 2, 3, 4))
print_sizeof((1, 2, 3, 4, 5))
print_sizeof((1, 2, 3, 4, 5, 6))
print()
 
print_sizeof([])
print_sizeof([1])
print_sizeof([1, 2])
print_sizeof([1, 2, 3])
print_sizeof([1, 2, 3, 4])
print_sizeof([1, 2, 3, 4, 5])
print_sizeof([1, 2, 3, 4, 5, 6])
print()
 
print_sizeof({})
print_sizeof({1:1})
print_sizeof({1:1, 2:2})
print_sizeof({1:1, 2:2, 3:3})
print_sizeof({1:1, 2:2, 3:3, 4:4})
print_sizeof({1:1, 2:2, 3:3, 4:4, 5:5})
print_sizeof({1:1, 2:2, 3:3, 4:4, 5:5, 6:6})

Opět je zajímavé se podívat na výsledky, které tento skript vypočítal a zobrazil:

40       tuple           ()
80       tuple           (1,)
120      tuple           (1, 2)
160      tuple           (1, 2, 3)
200      tuple           (1, 2, 3, 4)
240      tuple           (1, 2, 3, 4, 5)
280      tuple           (1, 2, 3, 4, 5, 6)
 
56       list            []
96       list            [1]
136      list            [1, 2]
176      list            [1, 2, 3]
216      list            [1, 2, 3, 4]
256      list            [1, 2, 3, 4, 5]
296      list            [1, 2, 3, 4, 5, 6]
 
64       dict            {}
264      dict            {1: 1}
296      dict            {1: 1, 2: 2}
328      dict            {1: 1, 2: 2, 3: 3}
360      dict            {1: 1, 2: 2, 3: 3, 4: 4}
392      dict            {1: 1, 2: 2, 3: 3, 4: 4, 5: 5}
552      dict            {1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6}

Opět se podívejme na příklad s kolekcemi, které obsahují řetězce o délce milionu znaků (a v tomto případě i bajtů):

from pympler import asizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(asizeof.asizeof(value, code=True), "\t", typename)
 
 
print_sizeof((1, 2))
print_sizeof((1, "?" * 1000000))
print()
 
print_sizeof([1, 2])
print_sizeof([1, "?" * 1000000])
print()
 
print_sizeof({1:1, 2:2})
print_sizeof({1:1, 2:"?" * 1000000})

Nyní již výsledky, na rozdíl od použití sys.getsizeof, mají praktický význam:

120       tuple
1000144   tuple
 
136       list
1000160   list
 
296       dict
1000352   dict

13. Zobrazení podrobnější statistiky o velikostech hodnot

O hodnotách uložených v operační paměti lze získat i další podrobnější informace. To provedeme takovým způsobem, že při volání funkce asizeof kromě reference na hodnotu (objekt) použijeme i pojmenovaný parametr stats nastavený na hodnotu odlišnou od False. Vyzkoušejme si to nejprve pro skalární hodnoty:

from pympler import asizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(asizeof.asizeof(value, stats=1), "\n", typename, "\t", value)
 
 
print_sizeof(0)
print_sizeof(1)
print_sizeof(42)
print_sizeof(2<<30)
print_sizeof(2<<60)
print()
 
print_sizeof(1.0)
print_sizeof(3.1415)
print()
 
print_sizeof(1+2j)
print_sizeof(1.2+3.4j)
print()
 
print_sizeof(True)
print_sizeof(False)
print_sizeof(None)
print()
 
print_sizeof("")
print_sizeof("f")
print_sizeof("fo")
print_sizeof("foo")
print_sizeof("foo bar")
print_sizeof("foo bar baz")
print_sizeof("foo bar baz xyz")
print_sizeof("foo bar baz xyzzy")

Z výsledků je patrné, že se skutečně zobrazí mnoho doplňujících informací o každé hodnotě, a to včetně informace o zarovnání atd.:

asizeof((0,), stats=1) ...
 24 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
24
 int             0
 
asizeof((1,), stats=1) ...
 32 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
32
 int             1
 
asizeof((42,), stats=1) ...
 32 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
32
 int             42
 
asizeof((2147483648,), stats=1) ...
 32 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
32
 int             2147483648
 
asizeof((2305843009213693952,), stats=1) ...
 40 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
40
 int             2305843009213693952
 
 
asizeof((1.0,), stats=1) ...
 24 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
24
 float           1.0
 
asizeof((3.1415,), stats=1) ...
 24 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
24
 float           3.1415
 
 
asizeof(((1+2j),), stats=1) ...
 32 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
32
 complex         (1+2j)
 
asizeof(((1.2+3.4j),), stats=1) ...
 32 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
32
 complex         (1.2+3.4j)
 
 
asizeof((True,), stats=1) ...
 32 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
32
 bool            True
 
asizeof((False,), stats=1) ...
 24 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
24
 bool            False
 
asizeof((None,), stats=1) ...
 16 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
16
 NoneType        None
 
 
asizeof(('',), stats=1) ...
 56 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
56
 str
 
asizeof(('f',), stats=1) ...
 56 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
56
 str             f
 
asizeof(('fo',), stats=1) ...
 56 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
56
 str             fo
...
...
...
asizeof(('foo bar baz xyzzy',), stats=1) ...
 72 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
72
 str             foo bar baz xyzzy

Podobně můžeme získat informace o kolekcích:

from pympler import asizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(asizeof.asizeof(value, stats=1), "\t", typename, "\t", value)
 
 
print_sizeof(())
print_sizeof((1,))
print_sizeof((1, 2))
print_sizeof((1, 2, 3))
print_sizeof((1, 2, 3, 4))
print_sizeof((1, 2, 3, 4, 5))
print_sizeof((1, 2, 3, 4, 5, 6))
print()
 
print_sizeof([])
print_sizeof([1])
print_sizeof([1, 2])
print_sizeof([1, 2, 3])
print_sizeof([1, 2, 3, 4])
print_sizeof([1, 2, 3, 4, 5])
print_sizeof([1, 2, 3, 4, 5, 6])
print()
 
print_sizeof({})
print_sizeof({1:1})
print_sizeof({1:1, 2:2})
print_sizeof({1:1, 2:2, 3:3})
print_sizeof({1:1, 2:2, 3:3, 4:4})
print_sizeof({1:1, 2:2, 3:3, 4:4, 5:5})
print_sizeof({1:1, 2:2, 3:3, 4:4, 5:5, 6:6})

Výsledky (zkráceno, ovšem povšimněte si, že se detekují prvky uložené v kolekcích):

asizeof(((),), stats=1) ...
 40 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
  1 deepest recursion
40       tuple           ()
...
...
...
asizeof(((1, 2, 3, 4, 5, 6),), stats=1) ...
 280 bytes
   8 byte aligned
   8 byte sizeof(void*)
   1 object given
   7 objects sized
   7 objects seen
   0 objects missed
   0 duplicates
   1 deepest recursion
280      tuple           (1, 2, 3, 4, 5, 6)
 
 
asizeof(([],), stats=1) ...
 56 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
  1 deepest recursion
56       list            []
...
...
...
asizeof(([1, 2, 3, 4, 5, 6],), stats=1) ...
 296 bytes
   8 byte aligned
   8 byte sizeof(void*)
   1 object given
   7 objects sized
   7 objects seen
   0 objects missed
   0 duplicates
   1 deepest recursion
296      list            [1, 2, 3, 4, 5, 6]
 
 
asizeof(({},), stats=1) ...
 64 bytes
  8 byte aligned
  8 byte sizeof(void*)
  1 object given
  1 object sized
  1 object seen
  0 objects missed
  0 duplicates
  1 deepest recursion
64       dict            {}
...
...
...
asizeof(({1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6},), stats=1) ...
 552 bytes
   8 byte aligned
   8 byte sizeof(void*)
   1 object given
   7 objects sized
  13 objects seen
   0 objects missed
   6 duplicates
   1 deepest recursion
552      dict            {1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6}

14. Získání skutečné velikosti funkcí, tříd a objektů uložených v operační paměti

Jak jsme si již řekli v úvodní části tohoto článku, vrací standardní funkce sys.getsizeof pro všechny třídy stejnou velikost a totéž do jisté míry platí i pro všechny objekty (tam ovšem již záleží na počtu atributů). Bude tedy zajímavé zjistit, jaké hodnoty vypíše funkce asizeof, která je interně mnohem komplikovanější, než sys.getsizeof.

Začneme výpisem velikosti funkcí:

from pympler import asizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(asizeof.asizeof(value), "\t", typename, "\t", value)
 
 
def foo():
    pass
 
 
def bar(x, y):
    return x+y
 
 
def baz(x=0, y=1):
    print(x)
    print(y)
    return x+y
 
 
print_sizeof(print)
print_sizeof(foo)
print_sizeof(bar)
print_sizeof(baz)

Výsledky jsou poněkud překvapivé:

0        builtin_function_or_method      <built-in function print>
0        function        <function foo at 0x7f3828c4d790>
0        function        <function bar at 0x7f3828c4d820>
0        function        <function baz at 0x7f3828c4d8b0>

Pro získání skutečné velikosti funkcí je nutné funkci asizeof předat pojmenovaný parametr code nastavený na hodnotu odlišnou od False. Příklad tedy upravíme do následující podoby:

from pympler import asizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(asizeof.asizeof(value, code=True), "\t", typename, "\t", value)
 
 
def foo():
    pass
 
 
def bar(x, y):
    return x+y
 
 
def baz(x=0, y=1):
    print(x)
    print(y)
    return x+y
 
 
print_sizeof(print)
print_sizeof(foo)
print_sizeof(bar)
print_sizeof(baz)

S výsledky:

0        builtin_function_or_method      <built-in function print>
736      function        <function foo at 0x7f3ced833790>
912      function        <function bar at 0x7f3ced833820>
1032     function        <function baz at 0x7f3ced8338b0>
Poznámka: povšimněte si, že pro interní funkce interpretru se nevrací korektní hodnota.

Podobně můžeme postupovat i při získávání velikostí tříd a objektů – opět je více než vhodné použít parametr code v případě, že nás zajímá skutečná velikost objektu jak s kódem, tak i s atributy:

from pympler import asizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(asizeof.asizeof(value, code=True), "\t", typename, "\t", value)
 
 
class C1:
    pass
 
 
class C2:
    def __init__(self):
        pass
 
class C3:
    def __init__(self):
        pass
 
    def foo(self, x):
        self.x=x
 
    def bar(self, y):
        self.y=y
 
 
o1 = C1()
o2 = C2()
o3 = C3()
 
print_sizeof(C1)
print_sizeof(o1)
 
print_sizeof(C2)
print_sizeof(o2)
 
print_sizeof(C3)
print_sizeof(o3)
 
o3.foo(42)
 
print_sizeof(C3)
print_sizeof(o3)
 
o3.bar(0)
 
print_sizeof(C3)
print_sizeof(o3)

Výsledky:

1672     type            <class '__main__.C1'>
1824     C1              <__main__.C1 object at 0x7fb416e05430>
2504     type            <class '__main__.C2'>
2656     C2              <__main__.C2 object at 0x7fb416e05040>
3832     type            <class '__main__.C3'>
3984     C3              <__main__.C3 object at 0x7fb416e05190>
3832     type            <class '__main__.C3'>
4016     C3              <__main__.C3 object at 0x7fb416e05190>
3832     type            <class '__main__.C3'>
4016     C3              <__main__.C3 object at 0x7fb416e05190>

A konečně si ukažme, se není velkým problémem ani situace, kdy objekt obsahuje referenci na sebe sama:

from pympler import asizeof
 
 
def print_sizeof(value):
    typename = "{:8}".format(type(value).__name__)
    print(asizeof.asizeof(value, code=True), "\t", typename, "\t", value)
 
 
class C4:
    def __init__(self):
        pass
 
    def foo(self, x):
        self.x=x
 
 
o4 = C4()
 
print_sizeof(C4)
print_sizeof(o4)
 
o4.foo(o4)
 
print_sizeof(C4)
print_sizeof(o4)

Výsledek:

3184     type            <class '__main__.C4'>
3336     C4              <__main__.C4 object at 0x7fc7f42b9430>
3184     type            <class '__main__.C4'>
3336     C4              <__main__.C4 object at 0x7fc7f42b9430>

15. Balíček guppy3

Kromě balíčku Pympler, jehož (prozatím velmi malou část) jsme si dnes popsali, ovšem existují i další balíčky, které se používají ve chvílích, kdy je nutné zjistit chování aplikace z hlediska spotřeby operační paměti. Mezi velmi užitečný balíček z této oblasti patří balíček nazvaný guppy, který je již možné využít pro analýzu chování celé aplikace, tedy nejenom pro zjištění velikosti jednotlivých hodnot (navíc typicky nemá programátor přehled o všech hodnotách, které jsou v paměti uloženy, protože některé z těchto hodnot využívá samotný interpret atd.). I tento balíček je dostupný na PyPi, takže je jeho instalace triviální:

$ pip3 install --user guppy3
Collecting guppy3
  Downloading guppy3-3.1.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (649 kB)
     |████████████████████████████████| 649 kB 1.6 MB/s
Installing collected packages: guppy3
Successfully installed guppy3-3.1.2
Poznámka: trojka v názvu tohoto balíčku značí, že se jedná o port původního balíčku určeného pro Python 2 do Pythonu 3.

Po instalaci si otestujeme, zda je nově nainstalovaný balíček skutečně dostupný i z interpretru Pythonu (konkrétně se zaměříme na balíček hpy s informacemi o objektech uložených na haldě):

>>> from guppy import hpy
 
>>> h=hpy()
 
>>> h.doc
 
Top level interface to Heapy. Available attributes:
Anything            Prod                Via                 iso
Clodo               Rcs                 doc                 load
Id                  Root                findex              monitor
Idset               Size                heap                pb
Module              Type                heapu               setref
Nothing             Unity               idset               test

a:

>>> help(hpy)
 
 
Help on function hpy in module guppy:
 
hpy(ht=None)
    Main entry point to the Heapy system.
    Returns an object that provides a session context and will import
    required modules on demand. Some commononly used methods are:
 
    .heap()                 get a view of the current reachable heap
    .iso(obj..)     get information about specific objects
 
    The optional argument, useful for debugging heapy itself, is:
 
        ht     an alternative hiding tag

16. Přístup k informacím o objektech uložených na haldě (heap)

Pravděpodobně zcela nejužitečnější funkcí nabízenou celým balíčkem guppy3 je metoda heap dostupná přes třídy z podbalíčku heapy (hpy). Tato metoda umožňuje získat informace o hodnotách (objektech) uložených na haldě a vracet tyto informace v podobě, která je snadno programově zpracovatelná. Nalezneme zde i různé další varianty této metody, například heapu atd.:

Help on method heap in module guppy.heapy.Use:
 
heap() method of guppy.heapy.Use._GLUECLAMP_ instance
    heap() -> IdentitySet[1]
 
    Traverse the heap from a root to find all reachable and visible
    objects. The objects that belong to a heapy instance are normally not
    included. Return an IdentitySet with the objects found, which is
    presented as a table partitioned according to a default equivalence
    relation (Clodo [3]).
 
    See also: setref[2]
 
    References
        [0] heapy_Use.html#heapykinds.Use.heap
        [1] heapy_UniSet.html#heapykinds.IdentitySet
        [2] heapy_Use.html#heapykinds.Use.setref
        [3] heapy_Use.html#heapykinds.Use.Clodo
 
 
heapu(rma=1, abs=0, stat=1) method of guppy.heapy.Use._GLUECLAMP_ instance
    heapu() -> Stat
 
    Finds the objects in the heap that remain after garbage collection but
    are _not_ reachable from the root.  This can be used to find objects
    in extension modules that remain in memory even though they are
    gc-collectable and not reachable.
 
    Returns an object containing a statistical summary of the objects
    found - not the objects themselves. This is to avoid making the
    objects reachable.
 
    See also: setref[1]
 
    References
        [0] heapy_Use.html#heapykinds.Use.heapu
        [1] heapy_Use.html#heapykinds.Use.setref

Podívejme se nyní na některé způsoby použití metody heap. Získáme souhrnné informace o všech objektech na haldě a necháme si zobrazit jejich statistiku. K tomu nám postačují pouhé tři řádky kódu:

from guppy import hpy
 
h=hpy()
print(h.heap())

Výsledkem bude tabulka, která obsahuje jak informace o celkovém počtu objektů (120000 objektů pro interpretaci programu se třemi řádky!!!), tak i celkové obsazení haldy. To však není vše, protože objekty jsou rozděleny do skupin podle jejich typu (str, tuple atd.) a tabulka obsahuje jak celkový počet objektů daného typu, tak i obsazení paměti:

Partition of a set of 121759 objects. Total size = 12892713 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0  31829  26  2973098  23   2973098  23 str
     1  21959  18  2072008  16   5045106  39 tuple
     2   6716   6  1186311   9   6231417  48 types.CodeType
     3   1158   1  1052208   8   7283625  56 type
     4  13621  11  1047842   8   8331467  65 bytes
     5   6319   5   859384   7   9190851  71 function
     6  25507  21   719732   6   9910583  77 int
     7   1158   1   568640   4  10479223  81 dict of type
     8    327   0   508584   4  10987807  85 dict of module
     9    760   1   481272   4  11469079  89 dict (no owner)
<270 more rows. Type e.g. '_.more' to view.>

Mimochodem: na starší verzi Pythonu (3.6.6) dostaneme zcela odlišné hodnoty (i proto si ji pro jeden projekt udržuji :-):

Partition of a set of 41461 objects. Total size = 4814075 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0  11614  28  1041238  22   1041238  22 str
     1  10110  24   733424  15   1774662  37 tuple
     2    563   1   462216  10   2236878  46 type
     3   2525   6   365032   8   2601910  54 types.CodeType
     4   5024  12   346353   7   2948263  61 bytes
     5   2455   6   333880   7   3282143  68 function
     6    563   1   271736   6   3553879  74 dict of type
     7    106   0   187896   4   3741775  78 dict of module
     8    261   1   183416   4   3925191  82 dict (no owner)
     9    402   1   148928   3   4074119  85 set
<118 more rows. Type e.g. '_.more' to view.>

Pokusme se skript upravit tak, že v něm vytvoříme velký řetězec a poté opět zavoláme metodu heap:

from guppy import hpy
 
x="?"*100000000
 
h=hpy()
print(h.heap())

Nyní bude výsledek vypadat odlišně, přesně podle očekávání (viz první řádek tabulky s hodnotami typu str):

Partition of a set of 41464 objects. Total size = 104814244 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0  11616  28 101041345  96 101041345  96 str
     1  10110  24   733448   1 101774793  97 tuple
     2    563   1   462216   0 102237009  98 type
     3   2525   6   365032   0 102602041  98 types.CodeType
     4   5024  12   346363   0 102948404  98 bytes
     5   2455   6   333880   0 103282284  99 function
     6    563   1   271736   0 103554020  99 dict of type
     7    106   0   187896   0 103741916  99 dict of module
     8    261   1   183416   0 103925332  99 dict (no owner)
     9    402   1   148928   0 104074260  99 set
<118 more rows. Type e.g. '_.more' to view.>
Poznámka: počet objektů i celková obsazenost haldy se může lišit v případě, že skript spustíme několikrát. Vše záleží na (asynchronním) běhu automatického správce paměti.

Ovšem metoda heap nezobrazí pouze tabulku, ale umožňuje nám (například interaktivně nebo i programově) objekty procházet a zkoumat je. Například si můžeme nechat vypsat statistiku o první řádku tabulky, tedy o všech objektem typu str (všechny další příklady jsou prováděny z REPLu Pythonu):

>>> h.heap()[0]
Partition of a set of 34181 objects. Total size = 3177461 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0  34181 100  3177461 100   3177461 100 str

Necháme si vypsat nejdelší řetězce (resp. jejich prvních několik znaků):

>>> h.heap()[0].byid
Set of 34202  objects. Total size = 3179683 bytes.
 Index     Size   %   Cumulative  %   Representation (limited)
     0     7423   0.2      7423   0.2 'The class Bi... copy of S.\n'
     1     6512   0.2     13935   0.4 '\nThe ``code...hentication``'
     2     6327   0.2     20262   0.6 'Configuratio... by spaces.\n'
     3     6150   0.2     26412   0.8 "Support for ... 'error'.\n\n"
     4     5573   0.2     31985   1.0 'Controls for...ger`.\n\n    '
     5     4791   0.2     36776   1.2 'Heap queues\...at Art! :-)\n'
     6     4791   0.2     41567   1.3 'Heap queues\...at Art! :-)\n'
     7     4708   0.1     46275   1.5 ' Retry confi...equest.\n    '
     8     4252   0.1     50527   1.6 'Serialize ``...ible.\n\n    '
     9     4114   0.1     54641   1.7 '\n        Ge...ib`\n        '

Nyní naalokujeme delší řetězec (10000 znaků) a zopakujeme předchozí operaci. Nový řetězec se dostane na samotný vrchol tabulky (a jeho velikost přesně odpovídá popisu, který jsme si uvedli v předchozích kapitolách – 49 bajtů + počet znaků pro ASCII řetězce):

>>> x="*"*10000
 
>>> h.heap()[0].byid
Set of 34204  objects. Total size = 3189794 bytes.
 Index     Size   %   Cumulative  %   Representation (limited)
     0    10049   0.3     10049   0.3 '************...*************'
     1     7423   0.2     17472   0.5 'The class Bi... copy of S.\n'
     2     6512   0.2     23984   0.8 '\nThe ``code...hentication``'
     3     6327   0.2     30311   1.0 'Configuratio... by spaces.\n'
     4     6150   0.2     36461   1.1 "Support for ... 'error'.\n\n"
     5     5573   0.2     42034   1.3 'Controls for...ger`.\n\n    '
     6     4791   0.2     46825   1.5 'Heap queues\...at Art! :-)\n'
     7     4791   0.2     51616   1.6 'Heap queues\...at Art! :-)\n'
     8     4708   0.1     56324   1.8 ' Retry confi...equest.\n    '
     9     4252   0.1     60576   1.9 'Serialize ``...ible.\n\n    '
<34194 more rows. Type e.g. '_.more' to view.>

Řetězec necháme odstranit správcem paměti a opět se podíváme na výsledky:

>>> x=None
 
>>> h.heap()[0].byid
Set of 34203  objects. Total size = 3179745 bytes.
 Index     Size   %   Cumulative  %   Representation (limited)
     0     7423   0.2      7423   0.2 'The class Bi... copy of S.\n'
     1     6512   0.2     13935   0.4 '\nThe ``code...hentication``'
     2     6327   0.2     20262   0.6 'Configuratio... by spaces.\n'
     3     6150   0.2     26412   0.8 "Support for ... 'error'.\n\n"
     4     5573   0.2     31985   1.0 'Controls for...ger`.\n\n    '
     5     4791   0.2     36776   1.2 'Heap queues\...at Art! :-)\n'
     6     4791   0.2     41567   1.3 'Heap queues\...at Art! :-)\n'
     7     4708   0.1     46275   1.5 ' Retry confi...equest.\n    '
     8     4252   0.1     50527   1.6 'Serialize ``...ible.\n\n    '
     9     4114   0.1     54641   1.7 '\n        Ge...ib`\n        '
<34193 more rows. Type e.g. '_.more' to view.>
Poznámka: projekt Guppy3 nabízí i mnohé další zajímavé a potenciálně užitečné vlastnosti, o nichž se podrobněji zmíníme příště.

17. Proč se tedy liší velikosti hodnot True a False?

Zbývá nám ještě odpovědět na otázku z titulku tohoto článku, tedy proč se velikosti hodnot True a False odlišují. Nejprve si musíme uvědomit, o jakých hodnotách se vlastně bavíme. Z historických důvodů totiž programovací jazyk Python reprezentuje hodnotu False jako nulu a hodnotu True jako jedničku. Nejedná se však o pouhou (řekněme) pseudoekvivalenci získanou na základě nějakých konverzních pravidel, ale o (pro naprostou většinu operací) skutečnou ekvivalenci:

>>> 0 == False
True
 
>>> 1 == True
True
 
>>> 2 == True
False

Z tohoto pohledu jsou tedy hodnoty True a False instancemi třídy Number! Vyzkoušejme si to:

>>> import numbers
 
>>> isinstance(0, numbers.Number)
True
 
>>> isinstance(1, numbers.Number)
True
 
>>> isinstance(True, numbers.Number)
True
 
>>> isinstance(False, numbers.Number)
True

Nyní tedy víme, že nám bude postačovat zjistit, z jakého důvodu je celočíselná hodnota 0 uložena v operační paměti s jinou velikostí než celočíselná hodnota 1, protože naprosto stejná pravidla budou platit pro hodnotu False a hodnotu True.

Poznámka: o obou pravdivostních hodnotách můžeme mluvit v singuláru, protože typicky mohou existovat v rámci celého procesu jen v jediné instanci, na níž se odkazují jednotlivé proměnné, atributy nebo parametry funkcí/metod.

18. Způsob uložení celočíselných hodnot Pythonu

V Pythonu 3 jsou celočíselné hodnoty reprezentovány datovým typem interně nazvaným long (a externě se jedná o objekty typu int). Ovšem nejedná se o stejný long, jaký známe například z programovacích jazyků C, C++ či Javy, protože typ long v podání Pythonu znamená, že uložená celočíselná hodnota může mít prakticky jakýkoli rozsah (teoreticky jsme omezeni jen kapacitou operační paměti). To mj. znamená, že nejsme omezeni pouze například na „klasický“ 32bitový či 64bitový rozsah, tedy na hodnoty –231..231-1 či –263..263-1 (popř. na rozsah 64bitový).

Způsob uložení hodnot typu long tedy musí být do značné míry adaptivní, což znamená, že malé hodnoty budou uloženy v kratším paměťovém bloku, než hodnoty větší (resp. přesněji řečeno hodnoty více vzdálené od nuly). To pochopitelně komplikuje a prodlužuje všechny výpočty, takže obecně platí, že v této oblasti bude Python vždy pomalejší, než nativní kód popř. než skripty napsané v interpretovaných jazycích, které podporují standardní formát celočíselných hodnot implementovaný přímo na mikroprocesoru.

Konkrétně vypadá paměťová struktura s celými čísly typu long následovně:

struct _longobject {
    PyObject_HEAD
    _PyLongValue long_value;
};

Samotná hlavička objektu není nyní příliš zajímavá (bylo by to téma na samostatný článek), takže se podívejme na druhý prvek celé struktury, což je opět datová struktura:

typedef struct _PyLongValue {
    uintptr_t lv_tag; /* Number of digits, sign and flags */
    digit ob_digit[1];
} _PyLongValue;

Tady se skrývají zajímavější informace. V prvním prvku struktury jsou uloženy informace o počtu cifer, znaménko a další příznaky. A druhý prvek je polem, do něhož jsou uloženy bajty, z nichž se celočíselná hodnota skládá. Výpočet uložené hodnoty lze zapsat takto:

value = SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i)

kde ob_size je jeden z atributů každé hodnoty a SHIFT může být buď 15 nebo 30. V prvním případě, kdy je SHIFT==15, se pracuje se 16bitovými hodnotami, ve druhém případě s hodnotami 32bitovými (tato volba je provedena při překladu interpretru Pythonu; poté ji již není možné měnit).

root_podpora

Speciálním případem je nula, která má ob_size nulový a tedy se nealokuje žádné pole ob_digit. Ale již pro jedničku je nutné pole alokovat, i když pochopitelně jen s jedním prvkem. A právě zde je tedy odpověď na původní otázku, proč je velikost False menší než velikost True. False je hodnota odpovídající celočíselné nule a tudíž ji lze v paměti uložit bez pole ob_digits. Naproti tomu True je již plnohodnotný celočíselný objekt, jehož velikost je stejná, jako velikost objektů/hodnot 1 až 230 (na současných platformách).

Poznámka: pro jistotu dodejme, že zatímco typ long (z pohledu Pythonistů int) zajišťuje uložení hodnot s prakticky neomezeným rozsahem, typ float již tuto vlastnost nemá, protože jde o typ definovaný v normě IEEE 754, kterou jsme se již na stránkách Roota zabývali několikrát (a proto budou výpočtu s floaty v Pythonu poněkud paradoxně rychlejší, než celočíselné výpočty).

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:

# Demonstrační příklad Stručný popis příkladu Cesta
1 getsizeof1.py získání nápovědy k funkci getsizeof z balíčku sys https://github.com/tisnik/most-popular-python-libs/blob/master/sys/getsizeof1.py
2 getsizeof2.py získání a tisk velikostí vybraných skalárních hodnot Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/sys/getsizeof2.py
3 getsizeof3.py získání a tisk velikosti kontejnerů Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/sys/getsizeof3.py
4 getsizeof4.py získání a tisk velikosti funkcí https://github.com/tisnik/most-popular-python-libs/blob/master/sys/getsizeof4.py
5 getsizeof5.py získání a tisk velikosti tříd a objektů https://github.com/tisnik/most-popular-python-libs/blob/master/sys/getsizeof5.py
6 getsizeof6.py získání a tisk velikosti tříd a objektů https://github.com/tisnik/most-popular-python-libs/blob/master/sys/getsizeof6.py
7 getsizeof7.py velikost kolekcí obsahujících velké prvky https://github.com/tisnik/most-popular-python-libs/blob/master/sys/getsizeof7.py
       
8 asizeof01.py získání nápovědy k balíčku pympler.asizeof https://github.com/tisnik/most-popular-python-libs/blob/master/asizeof/asizeof01.py
9 asizeof02.py všechny veřejné atributy balíčku pympler.asizeof https://github.com/tisnik/most-popular-python-libs/blob/master/asizeof/asizeof02.py
10 asizeof03.py získání nápovědy k funkci asizeof z balíčku pympler.asizeof https://github.com/tisnik/most-popular-python-libs/blob/master/asizeof/asizeof03.py
11 asizeof04.py získání a tisk velikostí vybraných skalárních hodnot Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/asizeof/asizeof04.py
12 asizeof05.py získání a tisk velikosti kontejnerů Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/asizeof/asizeof05.py
13 asizeof06.py získání a tisk velikostí vybraných skalárních hodnot Pythonu se statistikou https://github.com/tisnik/most-popular-python-libs/blob/master/asizeof/asizeof06.py
14 asizeof07.py získání a tisk velikosti kontejnerů Pythonu se statistikou https://github.com/tisnik/most-popular-python-libs/blob/master/asizeof/asizeof07.py
15 asizeof08.py získání a tisk velikosti funkcí bez parametru code https://github.com/tisnik/most-popular-python-libs/blob/master/asizeof/asizeof08.py
16 asizeof09.py získání a tisk velikosti funkcí s parametrem code https://github.com/tisnik/most-popular-python-libs/blob/master/asizeof/asizeof09.py
17 asizeof10.py získání a tisk velikosti tříd a objektů bez parametru code https://github.com/tisnik/most-popular-python-libs/blob/master/asizeof/asizeof10.py
18 asizeof11.py získání a tisk velikosti tříd a objektů s parametrem code https://github.com/tisnik/most-popular-python-libs/blob/master/asizeof/asizeof11.py
19 asizeof12.py velikost kolekcí obsahujících velké prvky https://github.com/tisnik/most-popular-python-libs/blob/master/asizeof/asizeof11.py

20. Odkazy na Internetu

  1. Top 5 Python Memory Profilers
    https://stackify.com/top-5-python-memory-profilers/
  2. Pympler na GitHubu
    https://github.com/pympler/pympler
  3. Pympler na PyPI
    https://pypi.org/project/Pympler/
  4. Dokumentace k balíčku Pympler
    https://pympler.readthedoc­s.io/en/latest/
  5. Guppy 3 na GitHubu
    https://github.com/zhuyife­i1999/guppy3/
  6. Guppy 3 na PyPI
    https://pypi.org/project/guppy3/
  7. Memory Profiler na GitHubu
    https://github.com/python­profilers/memory_profiler
  8. Memory Profiler na PyPI
    https://pypi.org/project/memory-profiler/
  9. How to use guppy/heapy for tracking down memory usage
    https://smira.ru/wp-content/uploads/2011/08/heapy.html
  10. Identifying memory leaks
    https://pympler.readthedoc­s.io/en/latest/muppy.html#mup­py
  11. How do I determine the size of an object in Python?
    https://stackoverflow.com/qu­estions/449560/how-do-i-determine-the-size-of-an-object-in-python
  12. Why is bool a subclass of int?
    https://stackoverflow.com/qu­estions/8169001/why-is-bool-a-subclass-of-int
  13. Memory Management in Python
    https://realpython.com/python-memory-management/
  14. Why do ints require three times as much memory in Python?
    https://stackoverflow.com/qu­estions/23016610/why-do-ints-require-three-times-as-much-memory-in-python
  15. cpython/Include/cpython/longintrepr.h
    https://github.com/python/cpyt­hon/blob/main/Include/cpyt­hon/longintrepr.h#L64
  16. sys — System-specific parameters and functions
    https://docs.python.org/3/li­brary/sys.html
  17. 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

Byl pro vás článek přínosný?

Autor článku

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