高校物理160203 views
高校倫理1440106 views
中学理科1630641 views
英語613639 views
小学算数1200427 views
ヒストリア290598 views
中学英語811710 views
Computer368106 views
高校日本史190571 views
世界の国564520 views
Help
Tools
NewsSpreadsheetCalendarSlidesTier ListPen ToolIllustrationCrayonPixel ArtASCII ArtPerspectiveEndless StairsGraphMind MapER DiagramFamily TreeMemeCurved TextImage EditorMosaicRetro FilterPencil SketchOCR/HighlighterMakeup EditorFaviconVideo TrimmerScrolling VideoVideo TitleColor PickerColor ExtractorBonfireFireworksWater RippleWater SplashBreaking GlassWood GrainMarble TextureCSS ButtonIcon MakerBar ChartGrouped Bar ChartStacked Bar ChartPie ChartLine ChartArea ChartStacked Area ChartScatter Plot3D Bar Chart3D Pie ChartBar Chart RaceBubble ChartPopulation PyramidPictogramEarningsCandlestick ChartInvestment RiskMortgage SimulatorCalculatorMatrix CalculatorFunction GraphPolynomial ExpansionVenn DiagramField VisualizerRubik's Cube Group TheoryTraveling SalesmanVoronoi and DelaunayFractalColumn ArithmeticDraw Math FiguresArithmetic AnimationArithmetic Word ProblemsCounting with Tree DiagramsCube NetsCross SectionsMotion PathMechanicsWavesLight and LensesThermodynamicsHow Semiconductors WorkMolecular StructuresAtomic OrbitalsElectrochemical CellsChemical EquilibriumCrystal LatticesBuffer pHComplex IonsDNA Double HelixCell DivisionMembrane ChannelsNerve ImpulseRock ClassificationWeatherConstellationsSolar and Lunar Eclipses3D ModelingFloor PlanSeismic StructuresIntersection TurnMaglevCooking AnimationOrigamiLive Viewer CountGeoJSON MapRailway MapPopulation MapCrime MapLand Price MapSchool MapShrine and Castle MapHouse of Representatives MapWord MapSolitaireReversiHakoiri MusumeChessHamburgerRippleSlide Puzzle MakerNeon PinballNovel MakerJapanese Typing PracticePiano Score EditorMusic Theory

English

Python 辞書のメモリ使用量を実測する:sys.getsizeof の罠と pympler による再帰計測

Python の辞書はどれくらいのメモリを消費しているのか。sys.getsizeof() で調べようとして、想定外の結果に困惑した経験はないだろうか? この関数には「罠」がある。

sys.getsizeof() が返す値の正体

sys.getsizeof() はオブジェクト自体のメモリ使用量を返す。ただし、オブジェクトが参照している他のオブジェクトは含まれない。

import sys

d = {"name": "Alice", "age": 30}
print(sys.getsizeof(d))  # 184(環境により異なる)

この 184 バイトは辞書のハッシュテーブル構造だけを指している。キーの文字列 "name""age"、値の "Alice"30 は別のオブジェクトとしてメモリ上に存在する。つまり、辞書が「本当に」消費しているメモリはもっと大きい。

import sys

d = {"name": "Alice", "age": 30}

# 辞書本体
print(sys.getsizeof(d))           # 184

# キーと値を個別に計測
print(sys.getsizeof("name"))      # 53
print(sys.getsizeof("Alice"))     # 54
print(sys.getsizeof("age"))       # 52
print(sys.getsizeof(30))          # 28

すべてを足し合わせると 371 バイト。しかしこれでも正確ではない。ネストした辞書やリストがあれば、その中身も再帰的に計測しなければならない。

pympler による再帰的計測

pympler ライブラリの asizeof() は、オブジェクトが参照するすべてのオブジェクトを再帰的にたどり、合計サイズを返します。

from pympler import asizeof

d = {"name": "Alice", "age": 30}
print(asizeof.asizeof(d))  # 448(環境により異なる)

sys.getsizeof() の 184 バイトに対し、asizeof() は 448 バイト。2倍以上の差がある。ネストが深くなるほど、この差は広がっていく。

from pympler import asizeof

nested = {
    "user": {
        "profile": {
            "name": "Alice",
            "tags": ["python", "data", "ml"]
        }
    }
}

print(asizeof.asizeof(nested))  # 1000 前後

空の辞書はなぜ大きいのか

空の辞書でも一定のメモリを消費する。

import sys

empty = {}
print(sys.getsizeof(empty))  # 64

64 バイトは辞書オブジェクトのヘッダと、最小サイズのハッシュテーブルを確保するため。CPython は辞書を作成した時点で 8 スロット分の領域を用意している。

リサイズのタイミング

辞書に要素を追加していくと、ある時点でメモリ使用量が急増する。これはハッシュテーブルのリサイズが発生したタイミング。

import sys

d = {}
prev_size = sys.getsizeof(d)

for i in range(20):
    d[i] = i
    current_size = sys.getsizeof(d)
    if current_size != prev_size:
        print(f"要素数 {len(d)}: {prev_size} -> {current_size} バイト")
        prev_size = current_size

実行結果の例:

要素数 1: 64 -> 184 バイト
要素数 6: 184 -> 344 バイト
要素数 11: 344 -> 656 バイト

CPython は負荷率(使用スロット数 ÷ 全スロット数)が約 2/3 を超えるとリサイズを行う。リサイズ時にはテーブルサイズがおよそ 2 倍になり、すべてのエントリが再配置される。

リサイズ前

スロットの 2/3 が埋まっている。次の挿入でリサイズが発生する。

リサイズ後

テーブルサイズが 2 倍に拡張される。負荷率は約 1/3 に下がり、余裕ができる。

大量の小さな辞書を作るときの注意

辞書を大量に生成するとき、オーバーヘッドが無視できなくなる。

from pympler import asizeof

# 辞書 10000 個
dicts = [{"x": i, "y": i * 2} for i in range(10000)]
print(asizeof.asizeof(dicts) / 1024 / 1024, "MB")  # 約 4.5 MB

# 名前付きタプル 10000 個
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
tuples = [Point(i, i * 2) for i in range(10000)]
print(asizeof.asizeof(tuples) / 1024 / 1024, "MB")  # 約 1.2 MB

同じデータでも、辞書と名前付きタプルでは 4 倍近い差が出る。構造が固定されているなら、namedtupledataclass(slots=True) を検討する価値がある。

dict のメモリ効率が改善された歴史

Python 3.6 で辞書の内部実装が変わり、メモリ効率が大幅に改善された。

Python 3.5 以前
従来の実装

ハッシュテーブルに直接キー・値・ハッシュ値を格納。スロットが空でもフルサイズの領域を確保していた。

Python 3.6
Compact Dict 導入

インデックス配列とエントリ配列を分離。空スロットは小さなインデックスのみで済むようになり、メモリ使用量が 20〜25% 削減された。

Python 3.6 以降では挿入順序も保持されるようになった。これは副次的な効果で、Compact Dict の設計がたまたま順序を保つ構造だったため。

pympler のその他の機能

pympler には asizeof() 以外にも便利な機能がある。

from pympler import asizeof

d = {"a": [1, 2, 3], "b": {"nested": "value"}}

# 詳細な内訳を表示
asizeof.asized(d, detail=1).format()

asized() を使うと、どの部分がどれだけメモリを消費しているか内訳を確認できる。メモリ最適化の際に、ボトルネックを特定するのに役立ちます。

まとめ

sys.getsizeof() は参照先を含まない「浅い」計測しかできない。辞書の本当のメモリ使用量を知りたいなら pympler.asizeof() を使う。リサイズは負荷率 2/3 で発生し、テーブルサイズが倍増する。大量の辞書を扱うなら、namedtupledataclass への置き換えも検討してみてください。