Сравнение скорости разных вариантов взаимодействия скриптов Unity3D

Вступление


Я не много знаю о тонкостях работы Unity, так как не занимаюсь этим профессионально, а как хобби. Обычно я просто изучаю все необходимое по мере надобности, поэтому эта статья ориентирована на таких же как я.


Я, как наверное и любой кто начинал писать на юнити, быстро понял, что самого банального метода взаимодействия (через синглтоны-менеджеры, Find, GetComponent и т.п.) становится недостаточно и нужно искать новые варианты.


И тут на сцену выходит система сообщений/уведомлений


Порывшись в разных статьях я нашел несколько различных вариантов реализации этой системы:


  • На основе встроенного UnityEvents
  • С использованием классической для C# пары Event/Delegate
  • Еще один встроенный старый встроенный функционал SendMessage

В большинстве статей практически нет информации по быстродействию тех или иных подходов, их сравнению и прочее. Обычно встречается только такое упоминание о быстродействии "Используйте SendMessage только в крайних случаях, а лучше не используйте вообще"


Окей, у этого подхода, видимо, есть существенные проблемы со скоростью, но как тогда обстоят дела у других?


Какой то вменяемой и упорядоченной информации на этот вопрос я не смог найти (может плохо искал) и решил выяснить это опытным путем, а заодно и опробовать эти подходы на практике, что очень помогло избавиться от каши в голове после прочтения десятков статей.


Сравнивать решил эти 3 подхода, а так же обычный прямой вызов функции на объекте по его ссылке.
И как бонус — посмотрим наглядно, как медленно работает Find при поиске объекта каждый Update (о чем кричат все гайды для новичков) Погнали.


Подготовка скриптов


Для теста нам потребуется создать на сцене 2 объекта:


  • Отправитель, назовем его Sender, создадим и прикрепим на него скрипт Sender.cs
  • Получатель, назовем его Receiver, создадим и прикрепим на него скрипт Receiver.cs

Начнем с получателя 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 вызовов

И дописываем функцию TestFunction так, что бы она могла считать за какое время она выполнилась testIterations раз и выплюнуть эту инфу в консоль. В аргументах будем принимать строку 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;
        }
    }

С этим закончили. Наша функция будет сама считать время выполнения какого то цикла вызовов себя и выводить это в консоль вместе с именем того, кто ее вызывал.
Мы еще вернемся к ней для того, что бы подписаться на отправитель и для того, что бы менять количество вызовов (хотя, можно привязаться к такой же переменной в отправителе, что бы не менять в двух местах, либо передавать вторым аргументом в функции, но не будем тратить на это время)


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

Подготовка завершена. Переходим к написанию тестов.


Прямой вызов функции (Direct Call)


Переходим в 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 timer 0.0005178452
DirectCallTest SELF timer 0.0001906157

(напомню, что результаты со словом SELF нам отдает та функция которую мы вызываем, а без SELF — та, которая вызывает)


Итак, данные в консоли и мы видим интересную картину — функция на получателе отработала в ~2,7 раза быстрее чем на отправителе.
Я так и не понял с чем это связано. Может в том, что на получателе после расчета времени дополнительно вызывается Debug.Log или в чем то другом… Если кто знает, то напишите мне и я внесу это в статью.


В любом случае нам это не особо важно, т.к. мы хотим сравнивать разные реализации между собой, поэтому переходим к следующему тесту.


Отправка сообщений через SendMessage


Старая и поносимая всеми кому не лень… посмотрим на что ты способна.


(Вообще, я не очень понимаю зачем она нужна, если для нее все равно нужна ссылка на объект как и в прямом вызове. Видимо, что бы не делать методы public, не понятно)


Добавляем функцию 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;
    }

Тааак, мы рассылаем всем подписавшимся сообщение о том, что событие произошло и хотим передать туда "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# на Event/Delegate


Помним, что нам надо реализовать event/delegate с возможностью отправки сообщения в качестве аргумента.
В отправителе Sender.cs создаем event и delegate:


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

Анализ результатов


Запускаем все тесты по 10000 итераций каждый и получаем такие результаты (я сразу отсортирую по времени выполнения цикла на нашем отправителе (Sender), т.к. на этом этапе я уже выяснил опытным путем, что время теста на получателе сильно отличалось из-за одного вызова Debug.Log, который выполнялся в 2 раза дольше чем сам цикл вызовов!


Название теста Время теста на отправителе
DirectCallTest 0.0001518726
EventDelegateTest 0.0001523495
UnityEventTest 0.002335191
SendMessageTest 0.003899455
DirectCallWithGettingComponentTest 0.007876277
SendMessageTestWithGettingComponentTest 0.01255739

Для наглядности визуализируем данные (по вертикали время исполнения всех итераций, по горизонтали названия тестов)



Давайте теперь повысим точность наших тестов и повысим количество итераций до 10млн.


Название теста Время теста на отправителе
DirectCallTest 0.1496105
EventDelegateTest 0.1647663
UnityEventTest 1.689937
SendMessageTest 3.842893
DirectCallWithGettingComponentTest 8.068002
SendMessageTestWithGettingComponentTest 12.79391

В принципе, ничего не изменилось. Становится видно, что система сообщений на обычном Event/Delegate почти не отличается по скорости от Direct Call, чего не скажешь о UnityEvent и уж тем более SendMessage.


Два последних столбца, я думаю, навсегда отучат использовать поиск объекта в цикле/апдейте.



Заключение


Надеюсь кому то это будет полезно как маленькое исследование или как небольшой гайд по системам событий.


Полный код получившихся файлов:


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 = = — -


Мне написали некоторые важные замечания, которые я хотел бы прокомментировать в статье:


1. Тестить в самом редакторе нельзя (код всегда собирается в DEBUG-режиме), обязательно нужно собирать standalone-билд и замерять в нем.

  1. Нельзя просто покрутить цикл N итераций и взять результаты. Нужно запустить цикл M-раз по N итераций и усреднить — это сгладит различные сайд-эффекты типа изменения частоты процессора и прочих вещей.
  2. Способ подсчета времени следует поменять — точность Time.realtimeSinceStartup никуда не годится.


Спасибо всем за эти уточнения, так как некоторые достаточно существенные.
Кстати, это все есть в этой статье — Performance tests of Event, ActionList, Observer, InterfaceObserver. Советую прочитать и учитывать эту информацию.


Что касается нашего теста — 1) и 3) я обязательно проверю прямо сейчас, а вот пункт 2) конкретно в нашем тесте я пропущу, т.к. в нашем случае мы не гонимся за точностью каждого результата, а сравниваем их между собой. Я запускал руками тест несколько раз и не увидел каких либо серьезных отклонений в результатах, поэтому пока пропустим это (если дойдут руки, то реализуем и этот момент)


Первое что мы проверим — это


Тест в собранном приложении


Тут все просто — собираем приложение под винду(предварительно добавим задержку в 5с перед запуском тестов. На всякий случай, что бы все точно прогрузилось) так же на 10млн итераций и запускаем.


Далее идем C:\Users\username\AppData\LocalLow\CompanyName\ProductName\output_log.txt
если у вас винда (или смотрим документацию) и изучаем логи.


И тут видим интересные изменения:


  • Во первых: узнаем, что в релизе все работает в 3-10 раз быстрее чем в редакторе! Я, например, не думал, что разница такая существенная/
  • Во вторых: узнаем, что прямой вызов с поиском объекта каждую итерацию не такой уж и медленный. Конечно, медленнее чем UnityEvents, но уже быстрее чем SendMessage. Хотя, возможно дело в том, что на сцене просто мало объектов… нужно исследовать это отдельно

Получаем такое сравнение для релиза



Кроме поменявшихся местами двух вышеуказанных тестов больше ничего не поменялось и график выглядит таким же как и в дебаге, т.е. DirectCall и DelegateEvent все так же быстрее чем UnityEvent и остальные


Оптимизируем подсчет времени


Хотя, как и пункт 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 раза в дебаг версии, но в релизе все соотношение опять возвращается. Увидите это в итоговой таблице). Как я и говорил — вид графика не изменился:



Делаем итоговую табличка со всеми значениями (время затраченное на выполнение 10млн итераций). Данные по последним тестам в разных вариантах debug/release, unity/c# time(способ измерения времени) :


Название теста Debug, Unity time Debug, C# time Release, Unity time Release, C# time
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 раз.


P.S. Еще был комментарий, что за результат надо брать время выполнения одной итерации, а не общее время выполнения. В нашем случае я не вижу надобности в этом, т.к. эти цифры обратно пропорциональны друг другу и ничего, кроме чуть другого вида графика не изменится, особенно выводы.


Используемая литература:



Спасибо за внимание!

Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 23

    +1
    Замечания:
    1. Тестить в самом редакторе нельзя (код всегда собирается в DEBUG-режиме), обязательно нужно собирать standalone-билд и замерять в нем.
    2. Нельзя просто покрутить цикл N-раз и взять результаты. Нужно запустить цикл M-раз и усреднить — это сгладит различные сайд-эффекты типа изменения частоты процессора и прочих вещей.
    3. Эвенты не равняются прямым вызовам — внутри есть проверки + защита списка вызовов от изменения. Т.е мы можем изменить список подписчиков на событие прямо в процессе самого вызова события и это ничего не поломает. Другое дело, что каждая подписка / отписка вызывает memory allocation, поэтому эвенты желательно обрабатывать своим списком с циклом по нему.
    Как пример, можно посмотреть тесты отсюда.
      +1
      По пункту 3 не так, внутри ивента(на самом деле делегата) нет защиты списка вызовов от изменений, просто делегат это иммутабельный тип — каждое изменение порождает копию.
        0
        Даже не так. Эвент, это обертка над списком, потому что сам эвент может принимать новых подписчиков и создавать копию старого списка с изменениями. Этот список будет использоваться при последующих вызовах, создавая видимость иммутабельности.
          +1
          Нет, не так. Ивент, по-умолчанию, это обертка над мультикаст делегатом, а мультикаст делегат внутри использует массив, а не список(см. referencesource.microsoft.com/#mscorlib/system/multicastdelegate.cs).
          Нет никакой иллюзии иммутабельности, а есть настоящая неизменяемость — делегаты не могут быть изменены после создания, любые операции надо ними приводят к созданию копии массива(cм., например, реализацию сложения делегатов: referencesource.microsoft.com/#mscorlib/system/multicastdelegate.cs,fcbf8bdc05d28aeb,references)

            –1
            внутри использует массив, а не список

            «Список» — имелось ввиду хранилище, а не конкретная реализация в виде List. К тому же ссылка некорректна, в юнити не используется последняя версия .net framework. :)

            любые операции надо ними приводят к созданию копии массива

            А я что написал? Снаружи остается тот же самый объект-обертка, внутри создается новая копия хранилища подписчиков на основе текущей + изменения в подписке.
              +1
              Объект не тот же самый, объект новый. Ссылка корректна моновская имплементация совпадает с референсной в большинстве случаев, а это базовая вещь для c# — делегаты в нем неизменяемые объекты.
              Но если очень хочется вот ссылка на тот же файл в моно:
              github.com/mono/mono/blob/master/mcs/class/corlib/System/MulticastDelegate.cs
                –1
                Объект не тот же самый, объект новый.

                Что?
                event Action OnTest = delegate {};
                ...
                OnTest += () => {};
                OnTest += () => {};
                

                И у меня теперь OnTest не указывает на тот же самый объект, содержащий внутри себя массив на 3 подписчика?
                  +1
                  Объект который лежит под OnTest(сам OnTest это не объект это синтаксический сахар, в лучшем случае поле класса), указывает на объект содержащий 3 подписчика, но это не тот же самый объект что лежал под OnTest до добавления в него первого делегата
                  Вот убедительно показывающий неизменяемость делегата пример:
                  using System;
                  namespace ConsoleApplication1
                  {
                  	class Program
                  	{
                  		static void Main(string[] args)
                  		{
                  			Action a = () => { Console.WriteLine("1"); };
                  			Action b = a;
                  			a += () => { Console.WriteLine("2"); };
                  
                  			Console.WriteLine("A:");
                  			a();
                  			Console.WriteLine("B:");
                  			b();
                  		}
                  	}
                  }

                  Вывод:
                  A:
                  1
                  2
                  B:
                  1
                    0
                    Все верно, сам затупил:
                    OnTest += ...

                    Присваивание нового инстанса же.
                    0
                    Вот пример ещё лучше конкретно про ивенты, это иллюстрация ошибки, которую я видел от опытного программиста в реальном проекте.
                    	public class B
                    	{
                    		public event Action evt = () => { };
                    
                    		public void Call()
                    		{
                    			evt();
                    		}
                    		
                    	}
                    
                    	public class A
                    	{
                    		private B _b;
                    		private Action a = () => { };
                    		public A(B b)
                    		{
                    			_b = b;
                    			_b.evt += a;
                    		}
                    
                    		
                    		public event Action evt
                    		{
                    			add { a += value; }
                    			remove { a -= value; }
                    		}
                    	}
                    
                    	class Program
                    	{
                    		static void Main(string[] args)
                    		{
                    			B b = new B();
                    			A a = new A(b);
                    
                    			a.evt += () => Console.WriteLine("1");
                    			b.evt += () => Console.WriteLine("2");
                    			b.Call();
                    		}
                    	}
                    

                    Какой по вашему будет вывод?
                    Скрытый текст
                    2
                    Потому что объект под ивентом иммутабельный

          +1

          Спасибо, проведу тесты в standalone и добавлю инфу

            +1
            Тогда еще и способ подсчета времени следует поменять — точность Time.realtimeSinceStartup никуда не годится.
              0

              А какой использовать? Я не нашел лучше варианта

                +1
                Я же кинул линк (первый комментарий), там есть пример: leopotam.com/3
                Test environment

                We will run 10000 iterations and will repeat this process 10 times, then will take average result time. We will measure time for calling our callbacks for one event at each implementation. As we will do this inside Unity we should knows that:

                We can’t use Time.realtimeSinceStartup for performance measurements — accuracy is very low. Instead we will use standard System.Diagnostics.Stopwatch class.
                We can’t start measure right on game start — Unity needs time to full initialization, we should wait few seconds when hardware resources will be freed. 3 seconds — enough for this test.

                Editor always compiles and runs code in DEBUG mode. For proper measure we need RELEASE version — we should creates standalone build and makes measurements with this build outside Unity editor. We will use Debug.Log method for save results to external logs.
                  0

                  Спасибо, изучу это и внесу изменения в соответствии с новыми данными

            +1
            Дописал обновление с учетом ваших замечаний в пунктах 1 и 2
            0
            Не хватает теста явного хранения списка подписчиков реализующих общий интерфейс. Вызовы виртуальных функций должны быть дешевле ивентов/делегатов.
              –1

              Можно какой нибудь пример реализации? Не уверен, что правильно понимаю о чем речь, но интересно протестировать

                0
                Я имею ввиду простую реализацию паттерна observer, супер простой пример вот:
                Скрытый текст
                public interface INaiveObservable<T>
                	{
                		void Subscribe(INaiveObserver<T> observer);
                		void Unsubscribe(INaiveObserver<T> observer);
                	}
                
                	public interface INaiveObserver<T>
                	{
                		void OnValue(T value);
                	}
                
                	public class NaiveObservable<T>: INaiveObservable<T>
                	{
                		readonly List<INaiveObserver<T>> _observers = new List<INaiveObserver<T>>();
                		public void Subscribe(INaiveObserver<T> observer)
                		{
                			_observers.Add(observer);
                		}
                
                		public void Unsubscribe(INaiveObserver<T> observer)
                		{
                			_observers.Remove(observer);
                		}
                
                		public void Push(T value)
                		{
                			//No exception handling
                			//No Subsribe/unsubscribe inside subscribers
                			//This is a naive implementation
                			foreach (var observer in _observers)
                			{
                				observer.OnValue(value);
                			}
                		}
                	}
                
                	public class WritingObserver<T> : INaiveObserver<T>
                	{
                		void INaiveObserver<T>.OnValue(T value)
                		{
                			Console.WriteLine(value);
                		}
                	}
                
                
                	class Program
                	{
                		static void Main(string[] args)
                		{
                			NaiveObservable<int> observable = new NaiveObservable<int>();
                			WritingObserver<int> observer = new WritingObserver<int>();
                			observable.Subscribe(observer);
                			observable.Push(10);
                			observable.Push(11);
                			observable.Push(12);
                		}
                	}
                
              –2

              Спасибо за статью. Рельно удивлен что реализация через делегат настолько быстрее чем Event System.

                0
                Итак, данные в консоли и мы видим интересную картину — функция на получателе отработала в ~2,7 раза быстрее чем на отправителе.
                Я так и не понял с чем это связано. Может в том, что на получателе после расчета времени дополнительно вызывается Debug.Log или в чем то другом… Если кто знает, то напишите мне и я внесу это в статью.

                Вывод в лог — жутко прожорливая операция, не удивлюсь если основную разницу дает именно вывод в лог.
                  0

                  Спасибо за статью.
                  Про Debug.Log он действительно очень тормозит.
                  Можно попробовать без него или посмотреть профайлером.

                    0
                    Что касательно SendMessage, единственное ее преимущество — универсальность, она отправляется на GameObject и отправляет сообщение всем компонентам в объекте, у кого есть соответствующий метод, тот и выполняет. На сколько знаю сейчас используется в основном в плагинах что бы передавать сообщения на пользовательские скрипты при необходимости.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое