3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C# / Unity】型制約なしゼロアロケでDisposeする方法

3
Posted at

1. DisposeHelperの必要性

汎用コレクションやオブジェクトプールなどの汎用コンテナを実装する際、「中身の型 T が IDisposable を実装していれば破棄したい」というシチュエーションがよくあります。

しかし、通常の実装では以下のジレンマに直面します。

  • where T : IDisposable の型制約をつけると、IDisposable を実装していない一般的な型(通常の構造体やクラス)をコンテナに格納できなくなる。
  • 型制約を外して (IDisposable)item や item as IDisposable を呼ぶと、T が構造体(値型)だった場合に毎回ボクシング(GC Alloc)が発生し、Unity ではフレームレート低下や GC スパイクの原因になる。

「型制約をつけずに任意の型を受け入れつつ、構造体のボクシングを発生させずに Dispose を呼ぶ」ための仕組みが必要になります。

2. Unsafe.Unbox による解決アプローチ

通常のアンボックスキャスト (T)boxed は、ヒープにあるデータからスタック上に値のコピーを取り出します。

一方、System.Runtime.CompilerServices.Unsafe.Unbox<T> は、ヒープ上のボックス化領域への直接の参照(ref T) を取得できます。

この特性を利用し、以下の手順でボクシングを初回のみに抑えます。

  1. 型 T のボックス化インスタンスをヒープ上に 1 つだけ生成して保持する。
  2. 破棄対象の構造体データを、Unsafe.Unbox<T> を介してヒープ内の領域へ直接上書きコピーする。
  3. そのインスタンスに対して IDisposable.Dispose() を呼び出す。
  4. 破棄処理による変更内容をスタック上の元の変数へ書き戻す。

これにより、2 回目以降の呼び出しでは新たな GC アロケーションが一切発生しなくなります。

3. 実装コード

再帰呼び出し(ツリー構造の破棄)への対策としてボックスを Stack で管理し、参照型フィールドの GC リーク防止や、クラスと構造体のオーバーロード解決も含めた完成コードです。

Unity の Allow 'unsafe' Code は無効のままで動作します。
(検証環境: Unity 6.3)

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

public static class DisposeHelper
{
    private static class DisposeStructHelper<T> where T : struct
    {
        private static readonly Stack<IDisposable> Stack = new(1);

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void Dispose(ref T item)
        {
            if (!Stack.TryPop(out var disposer))
            {
                T dummy = default;
                disposer = dummy as IDisposable; // ここでボクシング
            }

            // キャッシュしたボックスを再利用して、
            // 対象(item)のDisposeを処理して反映する。
            Unsafe.Unbox<T>(disposer) = item;
            try
            {
                disposer.Dispose();
                item = Unsafe.Unbox<T>(disposer);
            }
            finally
            {
                // itemが参照型フィールドを保持していた場合のメモリリークを防止
                Unsafe.Unbox<T>(disposer) = default;
                Stack.Push(disposer);
            }
        }
    }

    // クラスと構造体で同一シグネチャのオーバーロードを作るためのマーカー
    public readonly struct ClassMarker { }

    // 型判定のキャッシュ(未実装型での不要な Stack 生成とリフレクション負荷を防止)
    private static class TypeTraits<T> where T : struct
    {
        public static readonly bool IsDisposable = typeof(IDisposable).IsAssignableFrom(typeof(T));
    }

    #region Dispose (破棄のみ)

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Dispose<T>(ref T item) where T : struct
    {
        if (TypeTraits<T>.IsDisposable)
        {
            DisposeStructHelper<T>.Dispose(ref item);
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Dispose<T>(ref T item, ClassMarker _ = default) where T : class
    {
        if (item is IDisposable disposable) { disposable.Dispose(); }
    }

    // プロパティ等の破棄用(値渡し)
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Dispose<T>(T item) where T : class
    {
        if (item is IDisposable disposable) { disposable.Dispose(); }
    }

    #endregion

    #region SafeDispose (破棄 + 初期化)

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void SafeDispose<T>(ref T item) where T : struct
    {
        Dispose(ref item);
        item = default;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void SafeDispose<T>(ref T item, ClassMarker _ = default) where T : class
    {
        Dispose(ref item);
        item = null;
    }

    #endregion
}

実装の要点

  • 再帰呼び出し対策: ボックスインスタンスを単一の static フィールドではなく Stack<IDisposable> で保持することで、ツリー構造のオブジェクト破棄などで処理が再帰してもデータ破壊が起きません。
  • メモリリーク対策: finally 句でボックス領域を default で上書きし、T がクラス参照を持っていた場合に GC 対象から外れるのを防いでいます。
  • オーバーロード解決: where T : struct と where T : class は本来同一シグネチャで多重定義できませんが、クラス側にデフォルト引数 ClassMarker _ = default を付与することで、呼び出し側は引数 1 つのままコンパイル時に解決させています。
  • スレッドセーフについて: スタックを静的領域に確保しているため Unity のメインスレッド等シングルスレッド前提です。マルチスレッド環境で共用する場合は [ThreadStatic] やスレッドごとの管理が必要です。

4. まとめ

  • Unsafe.Unbox<T> でヒープ上のボックス領域を直接書き換えることで、構造体のボックス化を初回の 1 回に抑えられます。
  • 型制約なし・ゼロアロケーション・再帰安全を同時に達成できるため、低レイヤなコンテナやメモリ管理機構の実装に有用です。
  • Unsafe.Unbox はポインタ構文を使わないため、Unity プロジェクトで Allow 'unsafe' Code を有効にする必要はありません。
  • IDisposable に限らず、例えば自前のインターフェイス向けに同様のヘルパーを用意しても役に立つ場面はありそうです。
3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?