高校国語788336 views
教育149510 views
小学算数1200427 views
りんご209490 views
高校日本史190571 views
LaTeX962153 views
中学英語811710 views
小学社会310485 views
中学理科1630641 views
高校生物551766 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 の set のメモリレイアウトとオブジェクト構造(PySetObject の解剖)

CPython の set は Objects/setobject.c で実装されている。内部構造を理解することで、パフォーマンス特性や制約の理由が明確になる。

PySetObject の構造

CPython 3.x における set オブジェクトは以下の構造体で定義されている。

typedef struct {
    PyObject_HEAD
    Py_ssize_t fill;            /* アクティブ + ダミーエントリ数 */
    Py_ssize_t used;            /* アクティブエントリ数 */
    Py_ssize_t mask;            /* テーブルサイズ - 1(2^n - 1)*/
    setentry *table;            /* ハッシュテーブル */
    Py_hash_t hash;             /* frozenset のみ使用 */
    Py_ssize_t finger;          /* pop() 用のインデックス */
    setentry smalltable[PySet_MINSIZE];  /* 小さいセット用の埋め込みテーブル */
    PyObject *weakreflist;      /* 弱参照リスト */
} PySetObject;

重要なフィールドを順に見ていく。

fill と used の違い

fillused の区別は、削除されたエントリの扱いに関係する。

used

実際に格納されている要素数。len(s) で返る値。

fill

アクティブなエントリ + ダミーエントリ(削除済みマーカー)の合計。

要素を削除しても fill は減らない。これはオープンアドレス法の探索を正しく動作させるためだ。

s = {1, 2, 3, 4, 5}
# used=5, fill=5

s.remove(3)
# used=4, fill=5(3 の位置にダミーマーカー)

fill が閾値を超えるとリサイズが発生し、ダミーエントリも整理される。

setentry の構造

各エントリは以下の構造を持つ。

typedef struct {
    PyObject *key;      /* 格納されている要素(またはNULL/ダミー)*/
    Py_hash_t hash;     /* 要素のハッシュ値(キャッシュ)*/
} setentry;

ハッシュ値をキャッシュしているのは、探索時に毎回 __hash__ を呼ばないためだ。リサイズ時にも再計算せずに済む。

smalltable の最適化

小さい set のために、オブジェクト内に固定サイズのテーブルを持っている。

#define PySet_MINSIZE 8

setentry smalltable[PySet_MINSIZE];

8 要素以下の set は、追加のメモリ確保なしで動作する。これにより、小さい set の作成が高速になる。

import sys

# 空の set でもメモリは確保済み
s = set()
print(sys.getsizeof(s))  # 216 bytes(smalltable を含む)

要素数が 8 を超えると、table ポインタが別のメモリ領域を指すようになる。

mask とインデックス計算

mask はテーブルサイズから 1 を引いた値で、常に 2^n - 1 の形になる。

# インデックス計算
index = hash_value & mask

# これは以下と同等だが高速
# index = hash_value % (mask + 1)

ビット AND 演算は除算より高速なため、この最適化は重要だ。テーブルサイズが 2 の累乗でなければならない理由がここにある。

テーブルサイズの進行

テーブルは 2 の累乗サイズで拡張される。

# 実際のサイズ変化を観察
import sys

s = set()
prev_size = sys.getsizeof(s)
sizes = [(0, prev_size)]

for i in range(100):
    s.add(i)
    size = sys.getsizeof(s)
    if size != prev_size:
        sizes.append((len(s), size))
        prev_size = size

for count, size in sizes:
    print(f"{count} elements: {size} bytes")

出力例として、要素数 0→5→21→85 あたりでサイズが増加する。これはテーブルサイズが 8→32→128→512 と 4 倍ずつ成長していることを示す。

finger フィールド

fingerpop() の最適化に使われる。

Py_ssize_t finger;  /* pop() 用のインデックス */

pop() はランダムな要素を返すが、毎回テーブルの先頭から探索すると効率が悪い。finger は前回の位置を記憶し、そこから探索を再開する。

s = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
while s:
    print(s.pop(), end=' ')
# 出力順序はハッシュテーブルの配置に依存

hash フィールド(frozenset 専用)

frozenset はイミュータブルなので、ハッシュ値を一度計算したらキャッシュできる。

Py_hash_t hash;  /* frozenset のみ使用 */

set はミュータブルなのでこのフィールドは使われない(ハッシュ不可能なため)。

fs = frozenset([1, 2, 3])
print(hash(fs))  # 計算済みの値を返す(2回目以降は即座に返る)

メモリレイアウトの可視化

set オブジェクトのメモリ配置を概念的に示す。

# 概念的なメモリ配置
#
# PySetObject:
# +------------------+
# | PyObject_HEAD    |  (ob_refcnt, ob_type)
# +------------------+
# | fill             |  5
# +------------------+
# | used             |  4
# +------------------+
# | mask             |  7 (テーブルサイズ8の場合)
# +------------------+
# | table            |  → smalltable または外部テーブルへのポインタ
# +------------------+
# | hash             |  (frozenset のみ)
# +------------------+
# | finger           |  pop() 用
# +------------------+
# | smalltable[0]    |  (key, hash)
# | smalltable[1]    |  (key, hash)
# | ...              |
# | smalltable[7]    |  (key, hash)
# +------------------+
# | weakreflist      |
# +------------------+

空スロットとダミーの判別

テーブルのスロットには 3 つの状態がある。

状態key の値意味
NULL一度も使われていない
アクティブ有効なポインタ要素が格納されている
ダミー_PySet_Dummy削除された(探索は継続)

ダミーマーカーがあることで、衝突チェーンが途切れない。

#define EMPTY_TO_MINSIZE(so) do {                       \
    memset((so)->smalltable, 0, sizeof((so)->smalltable)); \
    (so)->used = (so)->fill = 0;                        \
    (so)->table = (so)->smalltable;                     \
    (so)->mask = PySet_MINSIZE - 1;                     \
} while(0)

GC との連携

set は他のオブジェクトを参照するため、ガベージコレクタの追跡対象になる。

PyObject_GC_Track(so);  /* GC に登録 */

循環参照を検出するために、GC は set 内の全要素を走査できる必要がある。tp_traverse コールバックがこれを実現している。

import gc

# 循環参照の例
a = set()
b = [a]
a.add(tuple(b))  # 間接的な循環

# GC が検出して解放できる
gc.collect()

この内部構造を理解することで、set の挙動やパフォーマンス特性の理由が明確になる。