Pythonメモリ使用量

画像

100万個の整数はどのくらいのメモリを必要としますか?


他のプログラミング言語と比較して、Pythonがメモリをどれだけ効率的に使用しているかという考えにしばしば戸惑いました。 たとえば、100万個の整数を処理するにはどのくらいのメモリが必要ですか? そして、任意の長さの同じ行数で?
判明したように、Pythonには、Cのソースコードに頼らずに対話型コンソールから必要な情報を直接取得する機会があります(ただし、忠実性のために、私たちはまだそこを見ています)。
好奇心を満たすことで、データ型の内部に入り、メモリが正確に何に費やされているかを調べます。

すべての例は、32ビットマシンのCPythonバージョン2.7.4で作成されました。 最後に、64ビットマシンのメモリ要件の表があります。

必要なツール


sys.getsizeofおよび__sizeof __()メソッド

最初に必要なツールは、sys標準ライブラリです。 公式文書を引用します。

sys.getsizeof(オブジェクト[、default_value])

オブジェクトのサイズをバイト単位で返します。
デフォルト値が指定されている場合、オブジェクトがサイズを取得する方法を提供しない場合に戻ります。 そうでない場合、TypeError例外がスローされます。
Getsizeof()は__sizeof__オブジェクトメソッドを呼び出し、ガベージコレクター用に格納されている追加情報のサイズを追加します(使用されている場合)。


Pythonで書き直されたgetsizeof()のアルゴリズムは次のようになります。
Py_TPFLAGS_HAVE_GC = 1 << 14 # .     0b100000000000000 def sys_getsizeof(obj, default = None)     if obj.hasattr('__sizeof__'):         size = obj.__sizeof__()     elif default is not None:         return default     else:         raise TypeError('    __sizeof__')     #       HAVE_GC     if type(obj).__flags__ & Py_TPFLAGS_HAVE_GC:         size = size +  PyGC_Head     return size 


ここで、PyGC_Headは、循環リンクを検出するためにガベージコレクターによって使用される二重リンクリストアイテムです。 ソースコードでは、次の構造で表されます。
 typedef union _gc_head { struct { union _gc_head *gc_next; union _gc_head *gc_sourcev; Py_ssize_t gc_refs; } gc; long double dummy; } PyGC_Head; 


PyGC_Headのサイズは、32ビットマシンでは12バイト、64ビットマシンでは24バイトになります。

コンソールでgetsizeof()を試して、何が起こるか見てみましょう。
 >>> import sys >>> GC_FLAG = 1 << 14 >>> sys.getsizeof(1) 12 >>> (1).__sizeof__() 12 >>> bool(type(1).__flags__ & GC_FLAG) False >>> sys.getsizeof(1.1) 16 >>> (1.1).__sizeof__() 16 >>> bool(type(1.1).__flags__ & GC_FLAG) False >>> sys.getsizeof('') 21 >>> ''.__sizeof__() 21 >>> bool(type('').__flags__ & GC_FLAG) False >>> sys.getsizeof('hello') 26 >>> sys.getsizeof(tuple()) 24 >>> tuple().__sizeof__() 12 >>> bool(type(tuple()).__flags__ & GC_FLAG) True >>> sys.getsizeof(tuple((1, 2, 3))) 36 


フラグをチェックする魔法を除いて、すべてが非常に簡単です。
例からわかるように、intとfloatはそれぞれ12バイトと16バイトを占有します。 Strは、コンテンツ内の各文字に対して21バイトと別のバイトを使用します。 空のタプルは12バイトを占有し、要素ごとに4バイトを追加します。 単純なデータ型(他のオブジェクトへの参照を含まないため、ガベージコレクターによって追跡されない)の場合、sys.getsizeofの値は__sizeof __()メソッドによって返される値と等しくなります。

id()およびctypes.string_at

ここで、メモリが正確に何に使われているかを調べましょう。
このためには、2つのことが必要です。1つ目は、オブジェクトの保存場所を見つけること、2つ目は、メモリからの読み取りに直接アクセスすることです。 Pythonがメモリへの直接アクセスから慎重に保護しているという事実にもかかわらず、これを行うことはまだ可能です。 この場合、セグメンテーションエラーが発生する可能性があるため、注意が必要です。

組み込みのid()関数は、オブジェクトの先頭が格納されているメモリアドレスを返します(オブジェクト自体はC構造体です)
 >>> obj = 1 >>> id(obj) 158020320 


メモリアドレスのデータを読み取るには、ctypesモジュールのstring_at関数を使用する必要があります。 彼女の公式説明はあまり詳しくありません:
ctypes.string_at(アドレス[、長さ])
この関数は、メモリセルのアドレスの先頭を含む文字列を返します。 「長さ」が指定されていない場合、文字列はゼロで終了しているとみなされ、


次に、id()が返したアドレスのデータを読み取ってみましょう。
 >>> import ctypes >>> obj = 1 >>> sys.getsizeof(obj) 12 >>> ctypes.string_at(id(obj), 12) 'u\x01\x00\x00 \xf2&\x08\x01\x00\x00\x003\x01\x00\x00 \xf2&\x08\x00\x00\x00\x001\x00\x00\x00' 


16進コードの外観はあまり印象的ではありませんが、真実に近いです。

モデル構造

知覚に便利な値で結論を提示するために、もう1つのモジュールを使用します。 ここでは、structモジュールのunpack()関数が役立ちます。

構造
このモジュールは、文字列として表されるPython値とC構造体の間の変換を行います。

struct.unpack(形式、文字列)
指定された形式に従って文字列を解析します。 文字列に要素が1つしか含まれていない場合でも、常にタプルを返します。 文字列には、形式で記述されているとおりの正確な情報量が含まれている必要があります。


必要なデータ形式。
シンボルC値Python値32ビットマシンの長さ
cチャー単一の文字列1
私はintint4
l長いint4
L符号なしロングint4
dダブル浮く8


次に、すべてをまとめて収集し、いくつかのタイプのデータの内部構造を調べます。

Int

 >>> obj = 1 >>> sys.getsizeof(obj), obj.__sizeof__() (12, 12) >>> struct.unpack('LLl', ctypes.string_at(id(obj), 12)) (373, 136770080, 1) 


値の形式は簡単に推測できます。

最初の数(373)は、オブジェクトへのポインターの数です。
 >>> obj2 = obj >>> struct.unpack('LLl', ctypes.string_at(id(obj), 12)) (374, 136770080, 1) 

ご覧のとおり、オブジェクトへの別のリンクを作成した後、数が1つ増えました。

2番目の番号(136770080)は、オブジェクトのタイプへのポインター(id)です。
 >>> type(obj) <type 'int'> >>> id(type(obj) ) 136770080 


3番目の数値(1)は、オブジェクトのコンテンツです。
 >>> obj = 1234567 >>> struct.unpack('LLl', ctypes.string_at(id(obj), 12)) (1, 136770080, 1234567) 

推測は、CPythonのソースコードを見ると確認できます。

 typedef struct { PyObject_HEAD long ob_ival; } PyIntObject; 

ここで、PyObject_HEADはすべての組み込みオブジェクトに共通のマクロであり、ob_ivalはlong型の値です。 PyObject_HEADマクロは、オブジェクトへのポインターの数のカウントと、オブジェクトの親タイプへのポインターを追加します—まさに見たとおりです。

フロート

浮動小数点数はintに非常に似ていますが、Cメモリではdouble型の値で表されます。
 typedef struct { PyObject_HEAD double ob_fval; } PyFloatObject; 


これは簡単に確認できます。
 >>> obj = 1.1 >>> sys.getsizeof(obj), obj.__sizeof__() (16, 16) >>> struct.unpack('LLd', ctypes.string_at(id(obj), 16) (1, 136763968, 1.1) 


文字列(Str)

文字列は、nullバイトで終わる文字の配列として表されます。 また、文字列の長さ、その内容からのハッシュ、および内部キャッシュに格納されているかどうかを決定するフラグは、インターンされた内部キャッシュに格納されます。

 typedef struct { PyObject_VAR_HEAD long ob_shash; #    int ob_sstate; #    ? char ob_sval[1]; #   +   } PyStringObject; 

PyObject_VAR_HEADマクロにはPyObject_HEADが含まれ、文字列の長さを格納する長いob_ival値が追加されます。

 >>> obj = 'hello world' >>> sys.getsizeof(obj), obj.__sizeof__() (32, 32) >>> struct.unpack('LLLli' + 'c' * (len(obj) + 1), ctypes.string_at(id(obj), 4*5 + len(obj) + 1)) (1, 136790112, 11, -1500746465, 0, 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\x00') 


4番目の値は、ストリングからのハッシュに対応します。これは確認が簡単です。
 >>> hash(obj) -1500746465 


ご覧のとおり、sstate値は0であるため、現在、ラインはキャッシュされていません。 キャッシュに追加してみましょう。
 >>> intern(obj) 'hello world' >>> struct.unpack('LLLli' + 'c' * (len(obj) + 1), ctypes.string_at(id(obj), 4*5 + len(obj) + 1)) (2, 136790112, 11, -1500746465, 1, 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\x00') 


タプル

タプルは、ポインターの配列として表されます。 その使用はリング参照につながる可能性があるため、ガベージコレクターによって追跡され、追加のメモリを消費します(これはsys.getsizeof()呼び出しによって通知されます)。

タプル構造は文字列に似ていますが、長さ以外の特別なフィールドはありません。
 typedef struct { PyObject_VAR_HEAD PyObject *ob_item[1]; } PyTupleObject; 


 >>> obj = (1,2,3) >>> sys.getsizeof(obj), obj.__sizeof__() (36, 24) >>> struct.unpack('LLL'+'L'*len(obj), ctypes.string_at(id(obj), 12+4*len(obj))) (1, 136532800, 3, 146763112, 146763100, 146763088) >>> for i in obj: print i, id(i) 1 146763112 2 146763100 3 146763088 

例からわかるように、タプルの最後の3つの要素は、その内容へのポインターです。

残りの基本データ型(unicode、list、dict、set、frozenset)も同様の方法で調べることができます。

結果は何ですか?


種類CPythonの名前書式ネストされたオブジェクトの形式32ビット長64ビット長GC用メモリ*
IntPyIntObjectLLl1224
浮くPyFloatObjectLLd1624
strPyStringObjectLLLli + c *(長さ+ 1)21 +長さ37 +長さ
ユニコードPyUnicodeObjectLllllllL *(長さ+ 1)28 + 4 *長さ52 + 4 *長さ
タプルPyTupleObjectLLL + L *長さ12 + 4 *長さ24 + 8 *長さあります
リストPyListObjectL * 5L *長さ20 + 4 *長さ40 + 8 *長さあります
セット/
冷凍セット
PySetObjectL * 7 +(lL)* 8 + lLLL *長さ(<= 5アイテム)100
(> 5アイテム)100 + 8 *長さ
(<= 5アイテム)200
(> 5アイテム)200 + 16 *長さ
あります
口述PyDictObjectL * 7 +(lLL)* 8lLL *長さ(<= 5アイテム)124
(> 5アイテム)124 + 12 *長さ
(<= 5アイテム)248
(> 5アイテム)248 + 24 *長さ
あります
* 32ビットマシンで12バイト、64ビットマシンで32バイトを追加

Pythonの単純なデータ型は、Cのプロトタイプよりも2〜3倍大きいことがわかります。違いは、オブジェクトへの参照の数とその型(PyObject_HEADマクロの内容)へのポインターを格納する必要があるためです。 これは、以前に作成されたオブジェクトの再利用を可能にする内部キャッシングによって部分的に相殺されます(これは不変の型でのみ可能です)。

文字列とタプルの場合、違いはそれほど重要ではなく、一定の値が追加されます。

また、リスト、辞書、およびセットは、原則として、必要以上に1/3を占有します。 これは、プロセッサ時間を節約するためにメモリを犠牲にする新しい要素を追加するアルゴリズムの実装によるものです。

したがって、記事の冒頭の質問に答えます:100万の整数を保存するには、数値自体に11.4メガバイト(12 * 10 ^ 6バイト)が必要であり、タプルごとにさらに3.8メガバイト(12 + 4 + 4 * 10 ^ 6バイト)が必要ですそれらへのリンクを保存します。

UPD:タイプミス。
UPD:サブタイトルでは、「100万素数」ではなく「100万整数」

Source: https://habr.com/ru/post/J193890/


All Articles