📜 ⬆️ ⬇️

Comparison of the speed of different variants of Unity3D script interaction

Introduction


I don’t know much about the intricacies of Unity, because I don’t do it professionally, but as a hobby. Usually I just learn everything I need as needed, so this article is aimed at people like me.


I, as probably anyone who started writing on a unit, quickly realized that the most banal method of interaction (through singletons managers, Find, GetComponent, etc.) is not enough and we need to look for new options.


And here comes the message / notification system.


Having rummaged through various articles, I found several different options for implementing this system:



In most articles, there is practically no information on the speed of various approaches, their comparison and so on. Usually there is only such a mention of speed "Use SendMessage only in extreme cases, but rather do not use at all"


Ok, this approach seems to have significant problems with speed, but then how are things with others?


I couldn’t find some sane and orderly information on this question (I could not search well) and decided to find it out by experience, and at the same time try these approaches in practice, which greatly helped to get rid of cereal in my head after reading dozens of articles.


I decided to compare these 3 approaches, as well as the usual direct function call on the object by its reference.
And as a bonus, let's see visually how Find is running slowly when searching for an object each Update (what all the guides for beginners shout about).


Scripting


For the test, we need to create on the stage 2 objects:



Let's start with the recipient Receiver.cs , because there will be the least code.
To tell the truth, at first I thought to limit myself to just an empty function that will be called from the outside. And then this file would look simple:


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

But later on, I decided to note the time for making all calls / sending messages not only at the sender, but also at the recipient (for reliability).


To do this, we need 4 variables:


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

And we add the function TestFunction so that it can take for what time it has run testIterations once and spit this info into the console. In the arguments we will take the string testName , in which the name of the test method will come, because the function itself does not know who will call it. This information is also added to the output in the console. As a result, we get:


  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; } } 

With this finished. Our function will count the time of execution of a certain call cycle itself and output it to the console along with the name of the person who called it.
We will return to it in order to subscribe to the sender and to change the number of calls (although you can bind to the same variable in the sender, not to change in two places, or pass the second argument to the function, but let's not waste time on it)


Receiver.cs completely
 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; } } } 

Preparation completed. We proceed to writing tests.


Direct Call function (Direct Call)


Go to Sender.cs and prepare the code for the first test. The most trivial and simplest option is to find a recipient instance in Start () and save a link to it:


 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>(); } 

Let's write our DirectCallTest function, which will be the preset for all other functions of the test:


  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; } 

In each iteration, we call our TestFunction on the receiver and pass the name of the test.


Now it remains to make an output to the console and run this test, so we add a line to Start ():


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

Done! We start and receive our first data. (I remind you that the results with the word SELF are given to us by the function that we call, and without SELF - the one that calls)


I will arrange them in such signs:


Test nameTest time
Directcalltest timer0.0005178452
DirectCallTest SELF timer0.0001906157

(I remind you that the results with the word SELF are given to us by the function that we call, and without SELF - the one that calls)


So, the data in the console and we see an interesting picture - the function on the receiver worked in ~ 2.7 times faster than on the sender.
I still do not understand what it is connected with. It may be that after the time is calculated on the recipient, the Debug.Log or something else is additionally called ... If anyone knows, write to me and I will add it to the article.


In any case, this is not particularly important to us, since we want to compare different implementations with each other, so we move on to the next test.


Sending messages via SendMessage


Old and blasphemed by all and sundry ... let's see what you are capable of.


(Actually, I don’t really understand why it is needed, if you still need a reference to an object for it as in a direct call. Apparently, it’s not clear what not to do public methods)


Add the SendMessageTest function:


  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; } 

And the line in Start ():


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

We get these results (slightly changed the structure of the table):


Test nameTest time on the senderTest Time at Recipient
Directcalltest0.00051784520.0001906157
SendMessageTest0.0043390990.003759265

Wow, the difference is one order! We will continue to write tests, and analyze the analysis at the end, so those who already know how to use all of this can go further to the analysis. And this is more intended for those who, like me, only study and choose for themselves the implementation of a system of interaction between components.


We use built-in UnityEvents


Create a UnityEvent in Sender.cs, to which we later sign our recipient:


  public static UnityEvent testEvent= new UnityEvent(); 

We are writing a new UnityEventTest function:


  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; } 

So, we send a message to all the signatories that an event has occurred and we want to send “UnityEventTest” there , but our event does not accept arguments.
We read the manual and we understand that for this we need to override the type of the UnityEvent class. Let's do this, as well as make changes to this line:


  public static UnityEvent testEvent= new UnityEvent(); 

Such code turns out:


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

Do not forget in UnityEventTest () to replace testEvent with testStringEvent.


Now subscribe to the event in the Receiver.cs recipient:


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

Subscribe in the OnEnable () method so that the object subscribes to events when it is activated on the stage (including when it is created).
You also need to unsubscribe from events in the OnDisable () method, which is called when an object is disabled (including deletion) on the stage, but for the test we don’t need it, so I didn’t write this part of the code.


We start. Everything works perfectly! Go to the next test.


C # Events at Event / Delegate


Remember that we need to implement event / delegate with the ability to send a message as an argument.
In the sender Sender.cs create the event and delegate:


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

Writing a new function 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; } 

Now subscribe to the event in the Receiver.cs recipient:


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

We start and check. Great, all tests are done.


Bonus


For the sake of interest, we will add copies of the DirectCallTest and SendMessageTest methods, where in each iteration we will look for an object on the stage, before addressing it, so that beginners can understand how expensive it is to make such errors:


  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; } 

Results analysis


We run all the tests at 10,000 iterations each and get these results (I immediately sort through the cycle time on our sender (Sender), because at this stage I found out by experience that the test time on the recipient was very different because of one call Debug.Log, which ran 2 times longer than the call cycle itself!


Test nameTest time on the sender
Directcalltest0.0001518726
Eventdelegatetest0.0001523495
UnityEventTest0.002335191
SendMessageTest0.003899455
DirectCallWithGettingComponentTest0.007876277
SendMessageTestWithGettingComponentTest0.01255739

For clarity, we visualize the data (vertically, the execution time of all iterations, horizontally the names of the tests)



Let's now increase the accuracy of our tests and increase the number of iterations to 10 million.


Test nameTest time on the sender
Directcalltest0.1496105
Eventdelegatetest0.1647663
UnityEventTest1.689937
SendMessageTest3.842893
DirectCallWithGettingComponentTest8.068002
SendMessageTestWithGettingComponentTest12.79391

In principle, nothing has changed. It becomes clear that the message system on the usual Event / Delegate is almost no different in speed from the Direct Call, which cannot be said about UnityEvent and especially SendMessage.


The last two columns, I think, will permanently wean out using the object search in a cycle / update.



Conclusion


I hope someone this will be useful as a small study or as a small guide to event systems.


The full code of the resulting files:


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; } } } 

- - = = UPDATE = = - -


I wrote some important notes that I would like to comment on in the article:


1. It is impossible to test in the editor itself (the code is always going to be in DEBUG mode), be sure to collect a standalone build and measure it.

  1. You can not just twist the loop N iterations and take the results. It is necessary to start the cycle M times by N iterations and average it - this will smooth out various side effects such as changing the processor frequency and other things.
  2. The time counting method should be changed - the accuracy of Time.realtimeSinceStartup is no good.


Thank you all for these clarifications, as some are quite substantial.
By the way, all this is in this article - Performance tests of Event, ActionList, Observer, InterfaceObserver . I advise you to read and take into account this information.


As for our test - 1) and 3) I will definitely check right now, but I’ll skip paragraph 2) specifically in our test, because in our case, we do not pursue the accuracy of each result, but compare them with each other. I ran the test with my hands several times and did not see any major deviations in the results, so for now let's skip it (if the hands reach, then we will realize this moment)


The first thing we check is


Test in the assembled application


Everything is simple here - we are building an application for Windows (we first add a delay of 5s before running the tests. Just in case, so that everything runs fine), also 10 million iterations and run.


Next, go to C: \ Users \ username \ AppData \ LocalLow \ CompanyName \ ProductName \ output_log.txt
if you have a Windows (or we look at documentation ) and we study logs.


And here we see interesting changes:



We get this comparison for the release



In addition to the exchanged two tests above, nothing else has changed and the graph looks the same as in the debug, i.e. DirectCall and DelegateEvent are still faster than UnityEvent and the rest


Optimize time counting


Although, like point 2) , the optimization of the calculation of time is unlikely to give us any changes in the alignment of forces, we will make this point in order to find out how much these two approaches differ between themselves. They drove.


We change all our:


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

On:


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

(at the end I'll post all the final code)


So, studying the data we can say that the difference of Time.realtimeSinceStartup from System.Diagnostics.Stopwatch is not significant in our case and can be attributed to the error the whole ratio comes back again. See this in the summary table). As I said, the view of the chart has not changed:



We make a final table with all the values ​​(the time spent on the execution of 10 million iterations). Data on the latest tests in different variants of debug / release, unity / c # time (time measurement method):


Test nameDebug, Unity timeDebug, C # timeRelease, Unity timeRelease, C # time
Directcalltest0.14961050.150.04984260.047
Eventdelegatetest0.16476630.1550.04787540.047
UnityEventTest1.6899371.6570.57064750.462
DirectCallWithGettingComponentTest8,0680022.1120.87934110.851
SendMessageTest3.8428933,9381,3642391,375
SendMessageTestWithGettingComponentTest12.793916,7522,2502462,244

Summing up - although, the speed of execution has changed significantly in the release version with a new way of measuring time, but the ratio of test execution time has not changed. DirectCall and EventDelegate are still in the lead and faster UnityEvent 10 times. But DirectCall with the search for the object in each iteration overtook even the usual SendMessage ..


We also learned from this update that the release version is 3-10 times faster than the editor.


PS There was also a comment that we should take the execution time of one iteration, and not the total execution time, for the result. In our case, I do not see the need for this, because These figures are inversely proportional to each other and nothing will change, except for a slightly different kind of graph, especially the conclusions.


Used Books:



Thanks for attention!


')

Source: https://habr.com/ru/post/353780/


All Articles