1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

横国ゲーム制作部Advent Calendar 2022

Day 21

UniRxのSubscribe時におけるアロケーション調査

Posted at

はじめに

Unityでは、イベント処理を便利に記述できるUniRxというライブラリを使うことができます。

UniRxは便利な一方、使用した際の負荷もまた気になるところではないでしょうか。
今回は、UniRxのObservableに対するSubscribe実行時にどの程度のアロケーション(メモリ確保)が発生しているのかをUnityのProfilerを使って計測してみたいと思います。

テストコード

アロケーション調査に用いるスクリプトはこんな感じです(テスト用にDisposeなどは行っていません)。

UniRxTest.cs
using UniRx;
using UnityEngine;
using UnityEngine.Profiling;

public class UniRxTest : MonoBehaviour
{
    private readonly CustomSampler sampler1 = CustomSampler.Create($"{nameof(subject1)}.OnNext");
    private readonly Subject<int> subject1 = new();

    private readonly CustomSampler sampler2 = CustomSampler.Create($"{nameof(subject2)}.OnNext");
    private readonly Subject<int> subject2 = new();

    private readonly CustomSampler sampler3 = CustomSampler.Create($"{nameof(subject3)}.OnNext");
    private readonly Subject<int> subject3 = new();

    private readonly CustomSampler sampler4 = CustomSampler.Create($"{nameof(subject4)}.OnNext");
    private readonly Subject<int> subject4 = new();

    private readonly CustomSampler sampler5 = CustomSampler.Create($"{nameof(subject5)}.OnNext");
    private readonly Subject<int> subject5 = new();

    private int hogeCount = 0;
    private static int piyoCount = 0;

    private void Start()
    {
        // 1. ラムダ式内で完結する場合
        var startSampler1 = CustomSampler.Create($"{nameof(subject1)}.Subscribe");
        startSampler1.Begin();
        subject1.Subscribe(_ => { });
        startSampler1.End();

        var localVar = 1;
        // 2. ローカル変数を使う場合
        var startSampler2 = CustomSampler.Create($"{nameof(subject2)}.Subscribe");
        startSampler2.Begin();
        subject2.Subscribe(_ => localVar++);
        startSampler2.End();

        // 3. インスタンスメソッドを直接呼び出す場合
        var startSampler3 = CustomSampler.Create($"{nameof(subject3)}.Subscribe");
        startSampler3.Begin();
        subject3.Subscribe(HogeMethod);
        startSampler3.End();

        // 4. インスタンスメソッドをSubscribeWithStateを使って呼び出す場合
        var startSampler4 = CustomSampler.Create($"{nameof(subject4)}.Subscribe");
        startSampler4.Begin();
        subject4.SubscribeWithState(this, (count, own) => own.HogeMethod(count));
        startSampler4.End();

        // 5. 静的メソッドを直接呼び出す場合
        var startSampler5 = CustomSampler.Create($"{nameof(subject5)}.Subscribe");
        startSampler5.Begin();
        subject5.Subscribe(PiyoMethod);
        startSampler5.End();
    }

    private void Update()
    {
        sampler1.Begin();
        subject1.OnNext(1);
        sampler1.End();

        sampler2.Begin();
        subject2.OnNext(2);
        sampler2.End();

        sampler3.Begin();
        subject3.OnNext(3);
        sampler3.End();

        sampler4.Begin();
        subject4.OnNext(4);
        sampler4.End();

        sampler5.Begin();
        subject5.OnNext(5);
        sampler5.End();
    }

    private void HogeMethod(int hoge)
    {
        hogeCount += hoge;
    }

    private static void PiyoMethod(int piyo)
    {
        piyoCount += piyo;
    }
}

このテストコードでは5つのパターンにおけるSubscribe時のアロケーションを確認する他、ついでにOnNext実行時のアロケーションも見るようにしています。
パターンごとの意図を説明します。

1. 処理がラムダ式内で完結する場合

Subscribeに渡すラムダ式が外部変数のキャプチャを行わない、一番シンプルな形です。

2. ラムダ式内でローカル変数を使用する場合

Subscribeに渡すラムダ式がローカル変数をキャプチャしている場合です。
ローカル変数をキャプチャするラムダ式はクロージャと呼ばれ、C#コンパイラは値を包み込む形で新規にクラスを自動生成するという挙動を示すようです。

参考

クロージャの生成は実行時の性能を求める場合は可能な限り避けるべきだというのがなんとなくの知識としてありましたが、実際どの程度の負荷がかかっているのでしょうか。

3. インスタンスメソッドを直接呼び出す場合

Subscribeに直接インスタンスメソッドHogeMethodを渡しています。
この場合C#コンパイラはデリゲートを生成して包むという挙動を示すようです。

参考

4. SubscribeWithStateを使用してインスタンスメソッドを呼び出す場合

Subscribeの代わりにSubscribeWithStateを使って、インスタンスメソッドを呼び出す場合です。
外部変数を囲い込むことができるのでアロケーションは抑制できるはずですが、どうなるでしょうか。
SubscribeWithStateについては、以下の記事が詳しいです(上でも紹介しましたが、UniRxの開発・保守をされている方の記事です)。

5. 静的メソッドを直接呼び出す場合

Subscribeに直接静的メソッドPiyoMethodを渡しています。
この場合も3の時と同様に、C#コンパイラはデリゲートを生成して包むという挙動を示すようです。

計測結果

スクリーンショット 2022-12-21 230114.png

結果としては、

  • 1の場合が104Bと最もアロケーションが小さい
  • SubscribeWithStateを使った4の場合が8B増加して112Bと2番目に小さい
  • ローカル変数や外部のメソッドを使用した2, 3, 5の場合は確保されたメモリが232Bと1, 4の場合と比較して2倍以上

のようになり、概ね予想通りの順番になりました。
特にアロケーションを抑制するために用意されたSubscribeWithStateを使うだけで確保されるメモリ領域を半分以下に減らせるのはとても便利なので、今後も積極的に使っていきたいと思います。

また、ついでに測定したOnNext時のアロケーションですが、以下のような結果で、今回のケースではどのパターンにおいてもアロケーションは発生しませんでした。

スクリーンショット 2022-12-21 230244.png

なお、今回のテストコードを用いた計測は実行環境などによって異なる結果となる場合がありますのでご了承下さい。

1
3
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
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?