Иногда нам нужно вызвать несколько методов как реакцию на событие - что стоит использовать в этом случае, чтобы уменьшить потери в производительности? Ответ не совсем очевиден.
Тестовое окружение
Для тестов прогоним 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-вариант самый медленный во всех тестах, но поддерживает очень важный механизм - мы можем модифицировать коллекцию подписчиков прямо внутри обработки события без получения ошибок, все остальные варианты требуют дополнительной работы над изоляцией итераторов (или копированием данных во временный буфер на момент вызова).
Вы можете помочь автору подпиской (с доступом к коду):