Built-in C# event と UnityEvent のパフォーマンス比較
処理速度とGC Allocの観点から,C#のeventとUnityEventを利用した時のパフォーマンス比較.
Jackson Dunstanの記事の翻訳です.
毎週月曜に記事が公開されている模様なので,翻訳しつつ追ってみます.
- C# Event と UnityEvent の呼び出し方の例
- リスナー追加時の GC Alloc パフォーマンス比較
- イベント発火時の GC Alloc パフォーマンス比較
- 処理速度のパフォーマンス比較
- 結論
- 終わりに
C# Event と UnityEvent の呼び出し方の例
C# Event
Action型を定義. リスナーがいない場合も考慮して,?.演算子を付与している. (個人的にはnullチェックの簡略化のために,よく?.演算子を利用している.)
class MyClass { // Listener追加のためのevent宣言 event Action<int,int> OnClick; void Foo() { // event発火.コールバック実行. OnClick?.Invoke(11, 22); } } // +=を利用して,リスナーを追加し,コールバック関数を定義 var myc = new MyClass(); myc.OnClick += (x, y) => Debug.LogFormat("clicked at {0}, {0}", x, y);
UnityEvent
引数を2~4まで定義したい場合は,継承クラスを作成し,Inspector表示のために[Serializable]の追加が必要. 引数が1つまでならば,上記のタスクは不要.
// UnityEventを利用する際の名前空間 using UnityEngine.Events; // UnityEventを継承したクラスを作成し,[Serializable]をつける [Serializable] class Int2Event : UnityEvent<int, int> { } class MyClass { // UnityEvent宣言 Int2Event OnClick = new Int2Event(); void Foo() { // UnityEvent発火.コールバック実行. OnClick?.Invoke(11, 22); } } // AddListenerを利用して,リスナーを追加し,コールバック関数を定義 var myc = new MyClass(); myc.OnClick.AddListener((x, y) => Debug.LogFormat("clicked at {0}, {0}", x, y));
リスナー追加時の GC Alloc パフォーマンス比較
各種のイベントにリスナー追加を増やしていき,両者のGC Alloc を比較.
検証コード
using System; using UnityEngine; using UnityEngine.Events; public class TestScript : MonoBehaviour { event Action csharpEv0; UnityEvent unityEv0 = new UnityEvent(); void Start() { AddCsharpListener(); AddUnityListener(); AddCsharpListener2(); AddUnityListener2(); AddCsharpListener3(); AddUnityListener3(); AddCsharpListener4(); AddUnityListener4(); } void AddCsharpListener() { csharpEv0 += NoOp; } void AddUnityListener() { unityEv0.AddListener(NoOp); } void AddCsharpListener2() { csharpEv0 += NoOp; } void AddUnityListener2() { unityEv0.AddListener(NoOp); } void AddCsharpListener3() { csharpEv0 += NoOp; } void AddUnityListener3() { unityEv0.AddListener(NoOp); } void AddCsharpListener4() { csharpEv0 += NoOp; } void AddUnityListener4() { unityEv0.AddListener(NoOp); } static void NoOp(){} }
結果
リスナー数が0~1個の場合は,C# event の方がゴミは少ない.
リスナー数が2個以上になった場合,UnityEvent の方がゴミが少なくなる.
イベント発火時の GC Alloc パフォーマンス比較
リスナー数を2個とした場合のイベント発火時のパフォーマンス比較.
検証コード
using System; using UnityEngine; using UnityEngine.Events; public class TestScript : MonoBehaviour { event Action csharpEv0; UnityEvent unityEv0 = new UnityEvent(); string report; void Start() { csharpEv0 += NoOp0; csharpEv0 += NoOp0; unityEv0.AddListener(NoOp0); unityEv0.AddListener(NoOp0); DispatchCsharpEvent(); DispatchUnityEvent(); } void DispatchCsharpEvent() { csharpEv0.Invoke(); } void DispatchUnityEvent() { unityEv0.Invoke(); } static void NoOp0(){} }
結果
イベント発火時はC# event はゴミを生成しないが,UnityEventは生成している. つまり,イベント発火時の場合は,C# event の方がパフォーマンスはよい.
一応,UnityEventがゴミを生成するのは最初の発火の際であり,それ以降の発火時は生成しない.
処理速度のパフォーマンス比較
イベントの束をたくさん発火させて,比較. 発火回数は1000万回,引数が0~2個のイベントを1~5個のリスナーに送る.
検証時の環境
2.3 Ghz Intel Core i7-3615QM Mac OS X 10.11.2 Apple SSD SM256E, HFS+ format Unity 5.3.1f1, Mac OS X Standalone, x86_64, non-development 640×480, Fastest, Windowed
検証コード
using System; using UnityEngine; using UnityEngine.Events; public class TestScript : MonoBehaviour { const int NumReps = 10000000; const int MaxListeners = 5; const int MaxArgs = 3; [Serializable] class IntEvent1 : UnityEvent<int> { } [Serializable] class IntEvent2 : UnityEvent<int,int> { } event Action csharpEv0; event Action<int> csharpEv1; event Action<int,int> csharpEv2; UnityEvent unityEv0 = new UnityEvent(); IntEvent1 unityEv1 = new IntEvent1(); IntEvent2 unityEv2 = new IntEvent2(); string report; void Start() { var stopwatch = new System.Diagnostics.Stopwatch(); var csharpEvTimes = new long[MaxArgs,MaxListeners]; var unityEvTimes = new long[MaxArgs,MaxListeners]; for (var numListeners = 0; numListeners < MaxListeners; ++numListeners) { csharpEv0 += NoOp0; stopwatch.Reset(); stopwatch.Start(); for (var j = 0; j < NumReps; ++j) { csharpEv0.Invoke(); } csharpEvTimes[0,numListeners] = stopwatch.ElapsedMilliseconds; unityEv0.AddListener(NoOp0); stopwatch.Reset(); stopwatch.Start(); for (var j = 0; j < NumReps; ++j) { unityEv0.Invoke(); } unityEvTimes[0,numListeners] = stopwatch.ElapsedMilliseconds; } for (var numListeners = 0; numListeners < MaxListeners; ++numListeners) { csharpEv1 += NoOp1; stopwatch.Reset(); stopwatch.Start(); for (var j = 0; j < NumReps; ++j) { csharpEv1.Invoke(11); } csharpEvTimes[1,numListeners] = stopwatch.ElapsedMilliseconds; unityEv1.AddListener(NoOp1); stopwatch.Reset(); stopwatch.Start(); for (var j = 0; j < NumReps; ++j) { unityEv1.Invoke(11); } unityEvTimes[1,numListeners] = stopwatch.ElapsedMilliseconds; } for (var numListeners = 0; numListeners < MaxListeners; ++numListeners) { csharpEv2 += NoOp2; stopwatch.Reset(); stopwatch.Start(); for (var j = 0; j < NumReps; ++j) { csharpEv2.Invoke(11, 22); } csharpEvTimes[2,numListeners] = stopwatch.ElapsedMilliseconds; unityEv2.AddListener(NoOp2); stopwatch.Reset(); stopwatch.Start(); for (var j = 0; j < NumReps; ++j) { unityEv2.Invoke(11, 22); } unityEvTimes[2,numListeners] = stopwatch.ElapsedMilliseconds; } report = "Num Args,Num Listeners,C# Event Time,UnityEvent Time\n"; for (var i = 0; i < MaxArgs; ++i) { for (var j = 0; j < MaxListeners; ++j) { report += i + "," + (j+1) + "," + csharpEvTimes[i,j] + "," + unityEvTimes[i,j] + "\n"; } } } void OnGUI() { GUI.TextArea(new Rect(0, 0, Screen.width, Screen.height), report); } static void NoOp0(){} static void NoOp1(int a){} static void NoOp2(int a, int b){} }
結果
引数が0のとき
引数が1のとき
引数が2のとき
考察
UnityEvent の処理速度は少なくとも C# event の2倍はかかる. 最悪のケースでは40倍くらいの時間がかかる.
結論
メモリの観点
リスナー数が1個までの場合は,C# eventの方がメモリを食わない.
リスナー数が2個以上の場合は,UnityEventの方がメモリを食わない.
最初のイベント発火時では,UnityEventはメモリを食うが,C#eventは食わない.
処理速度
C# event の方が UnityEvent より処理速度が最低2倍程度早い.
終わりに
これから毎週月曜日にJackson Dunstan氏の記事を翻訳して紹介しようと思います. 結構C#とUnityに関するノウハウがたまってる….
C# event の方が UnityEvent より速いのですが,UnityEventはInspectorで簡単にリンクできるのが強みで,
デザイナーや初心者がプロジェクトにいる場合はUnityEventを利用した方が学習コストを考慮すると良い場合がある.