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

# はじめに

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

https://github.com/neuecc/UniRx

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

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

```CS: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#コンパイラは値を包み込む形で新規にクラスを自動生成するという挙動を示すようです。

**参考**

https://ufcpp.net/study/csharp/sp2_anonymousmethod.html#closure

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

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

**参考**

https://neue.cc/2016/01/06_525.html

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

https://neue.cc/2016/05/14_529.html

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

# 計測結果

![スクリーンショット 2022-12-21 230114.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987563/00b413f0-f1c9-eacf-5fa6-d067cc8ba8c5.png)

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

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

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

![スクリーンショット 2022-12-21 230244.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987563/a347c3ca-6739-e926-f5e2-fe78c9a8b3f6.png)

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