異なるUnity3Dスクリプト相互作用オプションの速度の比較

エントリー



私はユニティの複雑さについてはあまり知りません。なぜなら、私はプロとしてではなく、趣味としてそれをやるからです。 通常、必要に応じて必要なものをすべて勉強するので、この記事は私のような人を対象としています。







私は、おそらく結束について書き始めた人と同じように、(シングルトンマネージャー、Find、GetComponentなどを介した)最も平凡な対話方法では不十分であり、新しいオプションを探す必要があることにすぐに気付きました。







そして、メッセージ/通知システムがシーンに入ります







さまざまな記事を読み、このシステムを実装するためのいくつかの異なるオプションを見つけました。









ほとんどの記事では、特定のアプローチのパフォーマンス、それらの比較などに関する情報はほとんどありません。 通常、 「極端な場合にのみSendMessageを使用し、まったく使用しない」という速度についての言及のみがあります。







さて、このアプローチには重大な速度の問題があるようですが、他の人はどうですか?







私は正気を見つけることができず、この質問に関する情報を注文し(おそらく私はひどく見ていました)、実験的にそれを見つけることを決定し、同時にこれらのアプローチを実際に試してみることにしました。







これら3つのアプローチと、リンクによるオブジェクトの通常の直接関数呼び出しを比較することにしました。

おまけとして、オブジェクトを検索するときにFindがどのように動作するかを見てみましょう。各Update(初心者向けのすべてのガイドが叫んでいます)が運転しました。







スクリプトの準備



テストのために、シーン上に2つのオブジェクトを作成する必要があります。









レシーバーReceiver.csから始めましょう。 コードが少なくなります。

実際には、最初は、外部から呼び出される空の関数に限定することを考えました。 そして、このファイルはシンプルに見えるでしょう:







using UnityEngine; public class Receiver : MonoBehaviour { public void TestFunction(string name) { } }
      
      





しかし、後で、すべての呼び出し/メッセージの送信の実行時間を、送信者だけでなく受信者でも特定することにしました(信頼性のため)。







これには、4つの変数が必要です。







  float t_start = 0; //    float t_end = 0; //    float count = 0; //    int testIterations = 10000; //   .   10000 
      
      





そして、 TestFunctions関数を追加して、 testIterationsを 1回完了するのにかかった時間をカウントし、この情報をコンソールに出力できるようにします。 引数では、文字列testNameを使用します。 テスト文字列には、テストするメソッドの名前が入ります。 関数自体は、誰がそれを呼び出すかを知りません。 この情報は、コンソールへの出力にも追加されます。 その結果、以下が得られます。







  public void TestFunction(string testName) { count++; //     //     ,     if (count == 1) { t_start = Time.realtimeSinceStartup; } //    ,    ,           (t_end - t_start) else if (count == testIterations) { t_end = Time.realtimeSinceStartup; Debug.Log(testName + " SELF timer = " + (t_end - t_start)); count = 0; } }
      
      





これは完了です。 この関数は、それ自体の呼び出しサイクルの実行時間を計算し、呼び出した人の名前とともにコンソールに出力します。

送信者にサブスクライブし、呼び出しの数を変更するために戻ります(ただし、送信者の同じ変数にバインドして、2つの場所で変更しないようにするか、関数に2番目の引数を渡しますが、私たちはこれに時間を無駄にしません)







Receiver.csを完全に
 using UnityEngine; public class Receiver : MonoBehaviour { float t_start = 0; //    float t_end = 0; //    float count = 0; //    int testIterations = 10000; //    public void TestFunction(string testName) { count++; //     //     ,     if (count == 1) { t_start = Time.realtimeSinceStartup; } //    ,    ,           (t_end - t_start) else if (count == testIterations) { t_end = Time.realtimeSinceStartup; Debug.Log(testName + " SELF timer = " + (t_end - t_start)); count = 0; } } }
      
      





準備が完了しました。 テストを書くことにします。







ダイレクトコール機能



Sender.csに移動し、最初のテストのコードを準備します。 最も一般的で最も簡単なオプションは、Start()で受信者インスタンスを見つけ、そのリンクを保存することです。







 using System; using UnityEngine; using UnityEngine.Events; public class Sender : MonoBehaviour { float t_start = 0; //    float t_end = 0; //    int testIterations = 10000; //    Receiver receiver; void Start () { receiver = GameObject.Find("Receiver").GetComponent<Receiver>(); }
      
      





DirectCallTest関数を記述します。これは、他のすべてのテスト関数のワークピースになります。







  float DirectCallTest() { t_start = Time.realtimeSinceStartup; for (int i = 0; i < testIterations; i++) { receiver.TestFunction("DirectCallTest"); } t_end = Time.realtimeSinceStartup; return t_end - t_start; }
      
      





各反復で、レシーバーでTestFunctionを呼び出し、テストの名前を渡します。







コンソールに結論を出してこのテストを開始するのは今のままなので、Start()に次の行を追加します。







  void Start () { receiver = GameObject.Find("Receiver").GetComponent<Receiver>(); Debug.Log("DirectCallTest time = " + DirectCallTest()); }
      
      





できた! 最初のデータを起動して取得します。 (SELFという単語を含む結果は、呼び出す関数によって与えられ、SELFを使わない場合は呼び出す関数によって与えられることを思い出します)







そのようなプレートにそれらを配置します:







テスト名 試験時間
DirectCallTestタイマー 0.0005178452
DirectCallTest SELFタイマー 0.0001906157


(SELFという単語を含む結果は、呼び出す関数によって与えられ、SELFを使わない場合は呼び出す関数によって与えられることを思い出します)







そのため、コンソールのデータと興味深い画像が表示されます-受信機の機能は送信機よりも約2.7倍速く動作しました。

私はまだそれが何に関連しているか理解していません。 たぶん、受信機で、時間を計算した後、Debug.Logが追加で呼び出されるか、または何か他のものが...誰かが知っているなら、私に書いて、私はこれを記事に追加します。







いずれにしても、これは私たちにとって特に重要ではありません。 異なる実装を互いに比較したいので、次のテストに進みます。







SendMessageを介してメッセージを送信する



誰もが古くて虐待している...あなたが何ができるか見てみましょう。







(実際には、直接呼び出しのようにオブジェクトへの参照が必要な場合、なぜ必要なのかよくわかりません。どうやら、パブリックメソッドを実行しないことは明らかではありません)







SendMessageTest関数を追加します。







  float SendMessageTest() { t_start = Time.realtimeSinceStartup; for (int i = 0; i < testIterations; i++) { receiver.SendMessage("TestFunction", "SendMessageTest"); } t_end = Time.realtimeSinceStartup; return t_end - t_start; }
      
      





Start()の行:







  Debug.Log("SendMessageTest time = " + SendMessageTest());
      
      





そのような結果が得られます(テーブルの構造を少し変更しました):







テスト名 送信者テスト時間 受信者テスト時間
DirectCallTest 0.0005178452 0.0001906157
SendMessageTest 0.004339099 0.003759265


うわー、違いは一桁です! 引き続きテストを作成し、最終的に分析を行います。これにより、すでにすべてを使用できる人は、分析までスクロールできます。 そして、これは、私のように、コンポーネント間の相互作用のシステムの実装を自分で勉強し、選択する人を対象としています。







組み込みのUnityEventsを使用します



Sender.csに UnityEventを作成 、その後、受信者に署名します。







  public static UnityEvent testEvent= new UnityEvent();
      
      





新しいUnityEventTest関数を作成しています。







  float UnityEventTest() { t_start = Time.realtimeSinceStartup; for (int i = 0; i < testIterations; i++) { testEvent.Invoke("UnityEventTest"); } t_end = Time.realtimeSinceStartup; return t_end - t_start; }
      
      





Soooo、イベントが発生したすべての署名者にメッセージを送信し、 そこ「UnityEventTest」を送信したいのですが、イベントは引数を受け入れません。

マニュアルを読み、このためにUnityEventクラスのタイプを再定義する必要があることを理解します。 これを行うとともに、この行に変更を加えます。







  public static UnityEvent testEvent= new UnityEvent();
      
      





次のコードが判明します。







  [Serializable] public class TestStringEvent : UnityEvent<string> { } public static TestStringEvent testStringEvent = new TestStringEvent();
      
      





UnityEventTest()でtestEventをtestStringEventに置き換えることを忘れないでください。







次に、レシーバーReceiver.csのイベントにサブスクライブします。







  void OnEnable() { Sender.testStringEvent.AddListener(TestFunction); }
      
      





OnEnable()メソッドでサブスクライブして、オブジェクトがシーンでアクティブ化されたとき(作成時を含む)イベントにサブスクライブするようにします。

また、オブジェクトがステージ上で切断(削除を含む)されたときに呼び出されるOnDisable()メソッドのイベントのサブスクライブを解除する必要がありますが、テストにはこれが必要ないため、コードのこの部分は記述しませんでした。







始めます。 すべてうまくいきます! 次のテストに合格します。







C# イベント/デリゲートのイベント



引数としてメッセージを送信する機能を持つイベント/デリゲートを実装する必要があることを忘れないでください。

Sender.cs sender 、イベントとデリゲートを作成します。







  public delegate void EventDelegateTesting(string message); public static event EventDelegateTesting BeginEventDelegateTest;
      
      





新しい関数EventDelegateTestを作成しています。







  float EventDelegateTest() { t_start = Time.realtimeSinceStartup; for (int i = 0; i < testIterations; i++) { BeginEventDelegateTest("EventDelegateTest"); } t_end = Time.realtimeSinceStartup; return t_end - t_start; }
      
      





次に、レシーバーReceiver.csのイベントにサブスクライブします。







  void OnEnable() { Sender.testStringEvent.AddListener(TestFunction); Sender.BeginEventDelegateTest += TestFunction; }
      
      





起動して確認します。 素晴らしい、すべてのテストの準備ができました。







ボーナス



興味を引くために、DirectCallTestメソッドとSendMessageTestメソッドのコピーを追加します。各反復で、アクセスする前にステージ上のオブジェクトを検索します。これにより、新規ユーザー 、このようなエラーを発生させるコストを理解できます。







  float DirectCallWithGettingComponentTest() { t_start = Time.realtimeSinceStartup; for (int i = 0; i < testIterations; i++) { GameObject.Find("Receiver").GetComponent<Receiver>().TestFunction("DirectCallWithGettingComponentTest"); } t_end = Time.realtimeSinceStartup; return t_end - t_start; } float SendMessageTestWithGettingComponentTest() { t_start = Time.realtimeSinceStartup; for (int i = 0; i < testIterations; i++) { GameObject.Find("Receiver").GetComponent<Receiver>().SendMessage("TestFunction", "SendMessageTestWithGettingComponentTest"); } t_end = Time.realtimeSinceStartup; return t_end - t_start; }
      
      





結果分析



それぞれ10,000回の繰り返しのすべてのテストを実行し、そのような結果を取得します(送信者(送信者)のサイクルの実行時間ですぐにソートします。この段階で、受信者のテスト時間がDebug.Logを呼び出します。呼び出しサイクル自体の2倍の時間がかかりました。







テスト名 送信者テスト時間
DirectCallTest 0.0001518726
EventDelegateTest 0.0001523495
UnityEventTest 0.002335191
SendMessageTest 0.003899455
DirectCallWithGettingComponentTest 0.007876277
SendMessageTestWithGettingComponentTest 0.01255739


明確にするために、データを視覚化します(垂直にすべての反復の実行時間、水平にテストの名前)









テストの精度を上げて、反復回数を1,000万回に増やしましょう。







テスト名 送信者テスト時間
DirectCallTest 0.1496105
EventDelegateTest 0.1647663
UnityEventTest 1.689937
SendMessageTest 3.842893
DirectCallWithGettingComponentTest 8.068002
SendMessageTestWithGettingComponentTest 12.79391


原則として、何も変わっていません。 通常のイベント/デリゲートのメッセージシステムの速度は、UnityEventや、さらにはSendMessageについては言えないダイレクトコールとほとんど変わらないことが明らかになります。







最後の2列は、ループ/更新でオブジェクト検索を使用することからあなたを永遠に引き離すと思います。









おわりに



少しの研究として、またはイベントシステムの小さなガイドとして、誰かがそれを役に立つと思うことを願っています。







結果のファイルの完全なコード:







Sender.cs
 using System; using System.Collections; using UnityEngine; using UnityEngine.Events; public class Sender : MonoBehaviour { [Serializable] public class TestStringEvent : UnityEvent<string> { } public delegate void EventDelegateTesting(string message); public static event EventDelegateTesting BeginEventDelegateTest; float t_start = 0; //    float t_end = 0; //    int testIterations = 10000000; //    public static TestStringEvent testStringEvent = new TestStringEvent(); Receiver receiver; System.Diagnostics.Stopwatch stopWatch; void Start () { receiver = GameObject.Find("Receiver").GetComponent<Receiver>(); stopWatch = new System.Diagnostics.Stopwatch(); StartCoroutine(Delay5sec()); // ,      Debug.Log("UnityEventTest time = " + UnityEventTest()); Debug.Log("DirectCallTest time = " + DirectCallTest()); Debug.Log("DirectCallWithGettingComponentTest time = " + DirectCallWithGettingComponentTest()); Debug.Log("SendMessageTest time = " + SendMessageTest()); Debug.Log("SendMessageTestWithGettingComponentTest time = " + SendMessageTestWithGettingComponentTest()); Debug.Log("EventDelegateTest time = " + EventDelegateTest()); // stopWatch.Elapsed.Seconds(); } IEnumerator Delay5sec() { yield return new WaitForSeconds(5); } float UnityEventTest() { //t_start = Time.realtimeSinceStartup; stopWatch.Reset(); stopWatch.Start(); for (int i = 0; i < testIterations; i++) { testStringEvent.Invoke("UnityEventTest"); } //t_end = Time.realtimeSinceStartup; //return t_end - t_start; stopWatch.Stop(); return stopWatch.ElapsedMilliseconds / 1000f; } float DirectCallTest() { //t_start = Time.realtimeSinceStartup; stopWatch.Reset(); stopWatch.Start(); for (int i = 0; i < testIterations; i++) { receiver.TestFunction("DirectCallTest"); } //t_end = Time.realtimeSinceStartup; //return t_end - t_start; stopWatch.Stop(); return stopWatch.ElapsedMilliseconds / 1000f; } float DirectCallWithGettingComponentTest() { //t_start = Time.realtimeSinceStartup; stopWatch.Reset(); stopWatch.Start(); for (int i = 0; i < testIterations; i++) { GameObject.Find("Receiver").GetComponent<Receiver>().TestFunction("DirectCallWithGettingComponentTest"); } //t_end = Time.realtimeSinceStartup; //return t_end - t_start; stopWatch.Stop(); return stopWatch.ElapsedMilliseconds / 1000f; } float SendMessageTest() { //t_start = Time.realtimeSinceStartup; stopWatch.Reset(); stopWatch.Start(); for (int i = 0; i < testIterations; i++) { receiver.SendMessage("TestFunction", "SendMessageTest"); } //t_end = Time.realtimeSinceStartup; //return t_end - t_start; stopWatch.Stop(); return stopWatch.ElapsedMilliseconds / 1000f; } float SendMessageTestWithGettingComponentTest() { //t_start = Time.realtimeSinceStartup; stopWatch.Reset(); stopWatch.Start(); for (int i = 0; i < testIterations; i++) { GameObject.Find("Receiver").GetComponent<Receiver>().SendMessage("TestFunction", "SendMessageTestWithGettingComponentTest"); } //t_end = Time.realtimeSinceStartup; //return t_end - t_start; stopWatch.Stop(); return stopWatch.ElapsedMilliseconds / 1000f; } float EventDelegateTest() { //t_start = Time.realtimeSinceStartup; stopWatch.Reset(); stopWatch.Start(); for (int i = 0; i < testIterations; i++) { BeginEventDelegateTest("EventDelegateTest"); } //t_end = Time.realtimeSinceStartup; //return t_end - t_start; stopWatch.Stop(); return stopWatch.ElapsedMilliseconds / 1000f; } }
      
      





Receiver.cs
 using UnityEngine; public class Receiver : MonoBehaviour { float t_start = 0; //    float t_end = 0; //    float count = 0; //    int testIterations = 10000000; //    void OnEnable() { Sender.testStringEvent.AddListener(TestFunction); Sender.BeginEventDelegateTest += TestFunction; } public void TestFunction(string testName) { count++; //     //     ,     if (count == 1) { t_start = Time.realtimeSinceStartup; } //    ,    ,           (t_end - t_start) else if (count == testIterations) { t_end = Time.realtimeSinceStartup; //Debug.Log(testName + " SELF timer = " + (t_end - t_start));   , .. ,    -     count = 0; } } }
      
      





--= =更新= =--



私は記事でコメントしたいいくつかの重要なポイントを書きました:







1.エディター自体でテストすることは不可能です(コードは常にDEBUGモードでコンパイルされます)。スタンドアロンビルドを収集して測定する必要があります。





  1. N回の繰り返しのループをねじって結果を取得することはできません。 N回の反復でM回のサイクルを開始し、平均する必要があります。これにより、プロセッサーの周波数の変更など、さまざまな副作用が滑らかになります。
  2. 時間のカウント方法を変更する必要があります-Time.realtimeSinceStartupの精度は価値がありません。




いくつかは非常に実質的であるため、これらの明確化のために皆に感謝します。

ところで、これはこの記事のすべてです-Event、ActionList、Observer、InterfaceObserverのパフォーマンステスト 。 この情報を読んで検討することをお勧めします。







テストについては-1)および3)今すぐ確認しますが、ポイント2)特にテストではスキップします。 この場合、各結果の精度を追求するのではなく、それらを互いに比較します。 私は手でテストを数回実行しましたが、結果に重大な逸脱は見られませんでしたので、今のところこれをスキップしましょう(手が届いたら、この瞬間を実感します)







最初に確認するのは







組み立てられたアプリケーションでテストする



ここではすべてが簡単です-Windowsでアプリケーションをコンパイルします(最初にテストを開始する前に5秒の遅延を追加します。万が一に備えて、すべてが正確にロードされるようにします)。







次に、 Cに移動します:\ユーザー\ユーザー名\ AppData \ LocalLow \ CompanyName \ ProductName \ output_log.txt

Windowsをお持ちの場合(またはドキュメントをご覧ください )、ログを調べてください。







そして、ここでは興味深い変化が見られます:









今回のリリースではこのような比較が行われます









上記の2つのテストで交換された場所に加えて、他に変更はなく、グラフはデバッグ時と同じように見えます。 DirectCallとDelegateEventは、UnityEventやその他のイベントよりも高速です







時間カウントを最適化します



ポイント2)のように、時間計算の最適化によって力の調整に変化が生じることはほとんどありませんが、この2つのアプローチが大きく異なる点を見つけるためにこのポイントを行います。 追われた。







以下のすべてを変更します。







 t_start = Time.realtimeSinceStartup; t_end = Time.realtimeSinceStartup; return t_end - t_start;
      
      





オン:







 //  System.Diagnostics.Stopwatch stopWatch; //   Start() stopWatch = new System.Diagnostics.Stopwatch(); //      stopWatch.Reset (); stopWatch.Start (); //    stopWatch.Stop (); //  return stopWatch.ElapsedMilliseconds / 1000f;
      
      





(最後に、最終的なコード全体をレイアウトします)







したがって、取得したデータを調べると、Time.realtimeSinceStartupとSystem.Diagnostics.Stopwatchの違いはこのケースでは重要ではなく、エラーに起因していると言えます(実際、DirectCallWithGettingComponentTestテストでは、デバッグバージョンではほぼ4回の違いがありますが、リリースでは全体の比率が再び戻ります。これを要約表で確認してください。 私が言ったように-チャートのビューは変更されていません:









すべての値(1000万回の反復の実行に費やされた時間)で最終版を作成します。 異なるバージョンのデバッグ/リリース、ユニティ/ c#時間(時間測定方法)の最新テストのデータ:







テスト名 デバッグ、Unity時間 デバッグ、C#時間 リリース、統一時間 リリース、C#時間
DirectCallTest 0.1496105 0.15 0,0498426 0,047
EventDelegateTest 0.1647663 0.155 0,0478754 0,047
UnityEventTest 1,689937 1,657 0,5706475 0.462
DirectCallWithGettingComponentTest 8,068002 2,112 0.8793411 0.851
SendMessageTest 3.842893 3,938 1,364239 1,375
SendMessageTestWithGettingComponentTest 12.79391 6,752 2,250246 2,244


要約すると、リリースバージョンでは時間を測定する新しい方法で実行速度が大幅に変更されましたが、テスト実行時間の比率は変更されていません。 DirectCallとEventDelegateは依然としてリーダーであり、UnityEventよりも10倍高速です。 ただし、各反復でオブジェクトを検索するDirectCallは、通常のSendMessageさえも追い越しました。







また、この更新から、リリースのバージョンがエディターよりも3〜10倍速いことがわかりました。







PS結果には、合計実行時間ではなく、1回の反復の実行時間が必要であるというコメントもありました。 私たちの場合、これが必要だとは思いません。 これらの数値は互いに反比例し、わずかに異なる種類のチャート、特に結論のみが変更されます。







使用された文献:









ご清聴ありがとうございました!




All Articles