Objective-CランタイムにおけるARCと参照カウントの内部実装:SideTableとweakテーブルの解剖

オブジェクトのメモリレイアウトと参照カウント

64ビットシステムにおけるオブジェクト構造

Objective-Cのオブジェクトは、内部的には以下のような構造体で表現されます。

struct objc_object {
    isa_union_t isa;
};

ここでの isa は単なるクラスへのポインタではなく、ビットフィールド(位域)を利用した最適化された構造体です。これにより、クラス情報のみならずオブジェクトのステータスや参照カウントの一部を直接保持することが可能になっています。

isaビットフィールドの分解

ビット領域 役割と詳細
is_optimized 非ポインタisa(最適化されたランタイム)であるかのフラグ
has_associated_objects 関連オブジェクト(Associated Objects)が紐づいているか
requires_cxx_destruction C++のデストラクタ呼び出しが必要か
class_memory_address クラスオブジェクトのメモリポインタ
runtime_validation ランタイムの整合性を検証するためのマジックナンバー
has_weak_references weakポインタから参照されているか
is_deallocating 現在解放処理中であるかのフラグ
overflow_to_sidetable 参照カウントがisaの容量を超え、SideTableに溢れているか
inline_reference_count isa内に直接埋め込まれた参照カウント(arm64では通常19ビット)

設計思想:

  • 高速パス: 参照カウントの大部分は inline_reference_count に保持し、ロック不要で高速にアクセスします。
  • 低速パス: カウントがオーバーフローした場合や、weak/関連オブジェクトが存在する場合は、グローバルな SideTable を利用します。

グローバルハッシュテーブル「SideTable」の仕組み

SideTableのデータ構造

SideTableはランタイムによって管理されるグローバルな構造体であり、以下のようなコンポーネントで構成されています。

struct RuntimeSideTable {
    os_unfair_lock table_lock;       // スレッドセーフを担保するロック
    RefcountMap overflow_counts;     // isaから溢れた参照カウントを格納
    WeakReferenceMap weak_refs;      // weakポインタのアドレスを管理
    AssociatedObjectMap associated;  // 関連オブジェクトの辞書
};
  • RefcountMap: オブジェクトのポインタをキーとし、溢れた参照カウントを値とするハッシュマップ。
  • WeakReferenceMap: オブジェクトをキーとし、そのオブジェクトを指すすべてのweakポインタのアドレスリストを値とします。
  • AssociatedObjectMap: 関連オブジェクトを格納する辞書。

SideTableが利用されるケース

トリガー 理由
参照カウントのオーバーフロー isaのビット数(19ビット)では表現しきれないため
weak参照の存在 weakポインタの一元管理と、解放時の自動nil化のため
関連オブジェクトの存在 カテゴリなどで追加されたプロパティの情報を保持するため

ハッシュ戦略とパフォーマンス最適化

SideTableへのアクセスはハッシュ関数によってバケットが計算されます。衝突時にはオープンアドレス法やチェーン法が適用されます。

重要な最適化として、Appleは StripedMap を採用しています。これは単一のグローバルSideTableではなく、複数のSideTableに分割(シャーディング)したものです。

static StripedMap<RuntimeSideTable> GlobalSideTables;

もし単一のテーブルだった場合、すべてのオブジェクトの参照カウント操作でグローバルロックの競合が発生し、パフォーマンスが著しく低下します。分片化(シャーディング)により、各テーブルが独立したロックを持つため、マルチスレッド環境での競合を大幅に削減しています。ただし、isa内部の参照に比べると、SideTableを経由する weak 参照のアクセスにはロックのオーバーヘッドが伴います。

retain と release の実行フロー

retainのロジック

id custom_objc_retain(id target_instance) {
    if (target_instance == NULL) return NULL;

    if (target_instance->isa.is_optimized) {
        // 高速パス: isa内のカウントをCASでインクリメント
        if (!attempt_inline_increment(target_instance)) {
            // 低速パス: 溢れた場合はSideTableをロックして更新
            increment_via_sidetable(target_instance);
        }
    } else {
        // 非最適化isa: 直接SideTableで管理
        increment_via_sidetable(target_instance);
    }
    return target_instance;
}

ここで利用される CAS (Compare-And-Swap) は、CPUレベルで提供されるアトミックな命令です。メモリ上の値が期待値と一致する場合のみ、新しい値に置き換えます。この操作は不可分であり、マルチスレッド環境でもロック(mutex)を使用せずに安全にデータを同期できます。

releaseのロジック

void custom_objc_release(id target_instance) {
    if (target_instance == NULL) return;

    if (target_instance->isa.is_optimized) {
        if (!attempt_inline_decrement(target_instance)) {
            if (!decrement_via_sidetable(target_instance)) {
                trigger_deallocation(target_instance);
            }
        }
    } else {
        if (!decrement_via_sidetable(target_instance)) {
            trigger_deallocation(target_instance);
        }
    }
}

参照カウントが0になった場合、trigger_deallocation が呼び出される前に、SideTable上のweak参照のクリアや関連オブジェクトの破棄といった後処理が実行されます。

weakポインタの裏側:weakテーブル

weak_tableの構造

struct WeakHashTable {
    WeakEntryNode** hash_buckets;
    size_t bucket_capacity;
    os_unfair_lock table_mutex;
};

struct WeakEntryNode {
    DisguisedPtr<objc_object> target_object;
    WeakPointerAddressList* pointing_addresses;
};

各 WeakEntryNode は、参照対象のオブジェクトと、そのオブジェクトを指しているすべてのweakポインタのメモリアドレスを保持しています。

weakポインタの登録プロセス

__weak id w = obj; が実行されると、以下のステップで登録が行われます。

  1. obj のNULLチェック。
  2. WeakHashTable 内から対象の WeakEntryNode を検索、または新規作成。
  3. weakポインタ w のアドレスを pointing_addresses リストに追加。
  4. 対象オブジェクトのisaフラグ has_weak_references を1に設定。

この登録処理もCASを用いてスレッドセーフに実行されます。

オブジェクト解放とweakの自動nil化

  1. custom_objc_release により参照カウントが0であることを検知。
  2. isaの has_weak_references フラグを確認。
  3. フラグが立っている場合、pointing_addresses を走査し、登録されているすべてのweakポインタの値をアトミックにNULL(nil)で書き換え。
  4. WeakEntryNode をテーブルから削除。
  5. 最終的に dealloc を実行。

AutoreleasePoolとARCの連携

autorelease メソッドが呼び出されると、オブジェクトは現在のスレッドに紐づく AutoreleasePoolPage という双方向リンクリストのスタックに追加されます。これにより、即座に解放されるのではなく、リリース処理が遅延されます。RunLoopのイテレーション終了時や、プールが明示的にdrainされたタイミングで、スタックに積まれたオブジェクトが一括して release されます。

オブジェクトのライフサイクル概要

  • オブジェクト生成
    • retain / release 操作は isa.inline_reference_count で処理
    • カウント超過 → SideTable.overflow_counts へ移行
    • weak参照の発生 → SideTable.weak_refs へアドレス登録
    • 関連オブジェクトの付与 → SideTable.associated へ格納
  • 参照カウント == 0 の検知
    • weakテーブルを走査し、関連するweakポインタをすべてnil化
    • 関連オブジェクトを破棄
    • メモリ解放(dealloc)を実行

タグ: Objective-C ARC iOS Runtime SideTable

10月3日 16:08 投稿