Иногда нам нужно вызвать несколько методов как реакцию на событие - что стоит использовать в этом случае, чтобы уменьшить потери в производительности? Ответ не совсем очевиден.
Тестовое окружение
Для тестов прогоним 10000 итераций вызовов события, повторим этот процесс 10 раз и затем усредним результат. Т.к мы это будем делать в Unity, то нужно учитывать следующее:
Мы не можем использовать Time.realtimeSinceStartup для измерений - точность таких данных будет очень низкой. Вместо этого следует использовать стандартный класс System.Diagnostics.Stopwatch.
Мы не можем начинать измерение прямо на старте - Unity требуется какое-то время для полной инициализации (подгрузки кода, графических ресурсов и т.п), необходимо выждать какое-то время, пока аппаратные ресурсы не освободятся. 3 секунд будет достаточно для этого теста.
Мы не можем доверять измерениям внутри редактора Unity, т.к код в этом случае будет работать в DEBUG-режиме и без оптимизаций. Для более корректного измерения нам необходим RELEASE-режим - для этого придется собрать билд приложения и запустить его вне редактора. Для фиксирования замеров будем использовать стандартный Debug.Log-метод и смотреть сформировавшиеся логи.
Event
Первый вариант - встроенный в C# event механизм обработки можественной подписки на события:
IEnumerator Start () { yieldreturnnewWaitForSeconds (3f); var result = 0; var sw = new System.Diagnostics.Stopwatch (); for (var i = 0; i < T; i++) { _events += TestCB; } for (var test = 0; test < TESTS; test++) { sw.Reset (); sw.Start (); for (var i = 0; i < T; i++) { _events (); } sw.Stop (); result += _sw.Elapsed.Milliseconds; } Debug.LogFormat ("event call: {0:F2}", result / (float) TESTS); } }
ActionList
Второй вариант - сохранение подписчиков в List<System.Action> с ручным вызовом в цикле:
List<Observer> _observers = new List<Observer> ();
IEnumerator Start () { yieldreturnnewWaitForSeconds (3f); var result = 0; var sw = new System.Diagnostics.Stopwatch (); for (var i = 0; i < T; i++) { _observers.Add (new Observer ()); } for (var test = 0; test < TESTS; test++) { sw.Reset (); sw.Start (); for (var i = 0; i < T; i++) { for (int j = 0; j < _observers.Count; j++) { _observers[j].Action (); } } sw.Stop (); result += _sw.Elapsed.Milliseconds; } Debug.LogFormat ("observer call: {0:F2}", result / (float) TESTS); }
List<IObserver> _interfaces = new List<IObserver> ();
IEnumerator Start () { yieldreturnnewWaitForSeconds (3f); var result = 0; var sw = new System.Diagnostics.Stopwatch (); for (var i = 0; i < T; i++) { _interfaces.Add (new VirtualObserver ()); } for (var test = 0; test < TESTS; test++) { sw.Reset (); sw.Start (); for (var i = 0; i < T; i++) { for (int j = 0; j < _interfaces.Count; j++) { _interfaces[j].Action (); } } sw.Stop (); result += _sw.Elapsed.Milliseconds; } Debug.LogFormat ("interface-observer call: {0:F2}", result / (float) TESTS); }
Очень странные результаты на iPhone6s / iPadPro13”: Observer-вариант на первом устройствезначительно быстрее, чем InterfaceObserver-вариант, на втором же - наоборот.
Остальные результаты достаточно стабильны на всех устройствах: ActionList-вариант быстрее или сравним с другими вариантами.
Event-вариант самый медленный во всех тестах, но поддерживает очень важный механизм - мы можем модифицировать коллекцию подписчиков прямо внутри обработки события без получения ошибок, все остальные варианты требуют дополнительной работы над изоляцией итераторов (или копированием данных во временный буфер на момент вызова).