Оптимизации пользовательского интерфейса посвящается.

image
Пользовательский интерфейс должен быть быстрым, очень быстрым, невероятно быстрым.

В попытках сэкономить наносекунды зачастую упускаются места где можно экономить секунды. Забавно, однажды на мое возмущение о двух секундной отрисовке небольшого списка, я получил ответ «Дабпиэф ничего не поделаешь», серьезно? Изучая всевозможные варианты реализации INotifyPropertyChanged habrahabr.ru/post/281294 возникает вопрос об идеальном балансе производительности пользовательского интерфейса и разработчика, который занимается этим интерфейсом. Захотелось понять как повлияет на работу интерфейса выбор конкретной реализации.

Наши атлеты:


  1. image OnPropertyChanged(string) — класический вызов с передачей имени свойства
  2. image OnPropertyChanged(nameof) — то же что и предыдущий, но...
  3. image OnPropertyChanged([CallerMemberName]) — автоматической определение имени свойства
  4. image OnPropertyChanged(()=>Expression) — передача выражения со свойством
  5. image SetProperty<T>(ref T storage, T value, [CallerMemberName]) — гибрид
  6. image ObservableObject<T> — об этом нам поведал astudent
  7. image АОП — прокси сгенерированный Unity, реализация из прошлого топика


(Раскраска улиток — самое интересное, пагубное влияние ухода во frontend)
Бег будет происходить в мешках, для этого каждый из участников будет подписан на PropertyChanged (пустой статический метод), а так же будет реализовать общий интерфейс, на этом этапе особое внимание уделяется участнику №6, в силу его врожденных мутаций. Если интерфейс еще можно вкарячить, то подписываться придется через DependencyPropertyDescriptor, он решил выпендриться. Трасса — цикл в 10 миллионов, для каждого участника.

На старт! Внимание! Го!


Дело то не быстрое, придется подождать.


image
Эксперимент проводился несколько раз и результаты примерно не отличаются.
10 000 000 1. string 2. nameof 3. CallerMemberName 4. Expression 5. SetProperty 6. ObservableObject 7. AOP
Seconds 0,129 0,13 0,121 9,016 0,372 4,248 22,643


Награждение.


Первое место заслуженно получают улитки 1,2,3!
Второе улитка №5, не много не дотянула
Бронза заслужено достается нестандартному решению №6
Далее идут выражения
И замыкает все это дело улиточный мастер спорта, его сгубила рефлексия.

Подведение итогов.


Очевидно, что для достижения максимальной скорости нужно брать инструменты из семейства 1,2,3. Использование аоп тормознет приложение напрочь, ну конечно использовалось неоптимальное решение, можно оптимизировать рефлект насоздавав делегатов, с экономив секунды 3, но общей картины это не изменит.
А теперь по факту! Использование любой из реализаций никак не влияет на производительность. 10 миллионов вызовов занимают 25 секунд, это значит для зависона на секунду нужно сделать 400к вызовов! 400к, если вдруг такое случиться VM нужно растворить в кислоте, без горя и сожалений.

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

Следующее без слов...
image

Комментарии (20)


  1. sidristij
    28.04.2016 08:56
    +7

    Если вы понимаете «разницу» между (1), (2), (3), то итоговая разница во времени случайна. (6) https://habrahabr.ru/post/281294/#comment_8847910


  1. areht
    28.04.2016 13:45
    +2

    > Использование аоп тормознет приложение на прочь

    Там проблема не в АОП. Это АОП курильщика.

    Возьмите Fody/PropertyChanged, KindOfMagic.


    1. agulyj
      29.04.2016 14:38

      Есть еще такой проект — AspectInjector разрабатыемый нами. В гитхабе как раз есть пример с INotifyPropertyChanged


    1. InWake
      29.04.2016 15:13

      речь идет о динамическом АОП, конкретная реализация АОП
      KindOfMagic например, это статическое АОП


      1. xXxVano
        04.05.2016 11:29

        Некоторое время назад хотел добавить немного АОП в свой код, но не устраивала постобработка скомпиленного кода и совместимость с текущим пайплайном. В результате для этого делал свой велосипед.
        Попробовал локально ваш тест прогнать, различия в районе 10% от 1-го решения. Мне кажется это более верная реализация АОП, если не хочется статического.
        Если захочется проверить, то в вся документация в тестах. Но что бы не копаться можно использовать такую реализацию:

        public class TestModel : NotificationObject, IData
        {
          [NotifyProperty]
          public virtual int Value { get; set; }
        
          public TestModel()
          {
            Notificator.Create(this);
          }
        }
        


  1. Ateist602
    28.04.2016 14:16
    +1

    Тяжело читать из-за колоссального количества орфографических и пунктуационных ошибок в тексте.


    1. zodchiy
      29.04.2016 13:57

      Что странно, я отписался в личку автору еще вчера утром, но до сих пор висят ошибки.


      1. InWake
        29.04.2016 14:36

        Только сейчас удалось выйти.


    1. InWake
      29.04.2016 14:42

      часть ошибок исправил, спасибо zodchiy


  1. Ky7m
    29.04.2016 01:48

    Ещё хотелось бы посмотреть код, с помощью которого получились данные результаты.


    1. InWake
      29.04.2016 14:43

      Выложу, чуть позже.


    1. InWake
      30.04.2016 21:52

      Обещанного три года ждут…
      но мы ж не улитки

      вот код программы

      class Program
      	{
      		[STAThread]
      		static void Main( string[] args )
      		{
      			IUnityContainer container = new UnityContainer();
      
      			container.RegisterViewModel<DataAop>();
      			var dataAop = container.Resolve<DataAop>();
      
      			List<IData> listOfData = new List<IData>();
      			listOfData.Add( dataAop );
      
      			var asm = System.Reflection.Assembly.GetExecutingAssembly();
      
      			var excludes = new HashSet<Type> { typeof( IData ), typeof( DataAop ), typeof( ProxyData ), typeof( DataObservableObject ) };
      
      			listOfData.AddRange( asm.GetTypes()
      				.Where( t => typeof( IData ).IsAssignableFrom( t ) && !excludes.Contains( t ) ).Select( t => Activator.CreateInstance( t ) as IData ) );
      
      			foreach (var notify in listOfData.Cast<INotifyPropertyChanged>())
      				notify.PropertyChanged += OnPropertyChanged;
      
      			DataObservableObject observableData = new DataObservableObject();
      
      			var propDesp = DependencyPropertyDescriptor.FromProperty( ObservableObject<int>.ValueProperty, typeof( ObservableObject<int> ) );
      			propDesp.AddValueChanged( observableData.RealValue, OnPropertyChanged );
      
      			listOfData.Add( observableData );
      
      			Console.WriteLine( string.Format( "Start ---> {0}", DateTime.Now ) );
      
      			Stopwatch sw = new Stopwatch();
      			foreach (var data in listOfData)
      			{
      				sw.Restart();
      				for (int i = 0; i < 10000000; i++)
      					data.Value = i;
      				sw.Stop();
      
                                      Console.WriteLine();
      
      				Console.WriteLine( string.Format( "Iteration date {0}, data name {1}, elapsed {2}",
      					DateTime.Now, data.GetType().Name, sw.ElapsedMilliseconds ) );
      			}
      
      			Console.WriteLine( string.Format( "Stop ---> {0}", DateTime.Now ) );
      
      			Console.ReadKey();
      		}
      
      		static void OnPropertyChanged( object sender, EventArgs e ) { }
      
      		static void OnPropertyChanged( object sender, PropertyChangedEventArgs e ) { }
      	}
      


      интерфейс улиток (эх название у интерфейса подкачало)
      public interface IData
          {
              int Value { get; set; }
          }
      


      пример реализации
       class DataString : BindableBase, IData
          {
              int _value;
      
              public int Value
              {
                  get
                  {
                      return _value;
                  }
      
                  set
                  {
                      if (_value != value)
                      {
                          _value = value;
                          OnPropertyChanged("Value");
                      }
                  }
              }
          }
      


      особо опасная реализация
       class DataObservableObject : IData
          {
              public DataObservableObject()
              {
                  RealValue = new ObservableObject<int>();
              }
              public ObservableObject<int> RealValue { get; private set; }
      
              public int Value
              {
                  get
                  {
                      return RealValue.Value;
                  }
      
                  set
                  {
                      RealValue.Value = value;
                  }
              }
          }
      


      запустил тест на другом компьютере, и вот результат
      image
      разница не велика


  1. ZimM
    29.04.2016 14:20

    Что такое [CallerMemberName]? Как происходит определение имени текущего свойства?


    1. InWake
      29.04.2016 14:57

      Атрибут, появился в с#5 https://msdn.microsoft.com/ru-ru/library/system.runtime.compilerservices.callermembernameattribute(v=vs.110).aspx
      имя подставляется во время выполнения
      можно посмотреть https://msdn.microsoft.com/en-us/library/system.runtime.compilerservices(v=vs.110).aspx


      1. ZimM
        29.04.2016 16:36

        Во время выполнения, или таки во время компиляции? Судя по отсутствию разницы в производительности между методами 1, 2, и 3, я бы предположил, что подстановка происходит прямо в вызове метода во время компиляции.


        1. InWake
          30.04.2016 21:18

          Во время запуска при анализе байт кода, это делается только 1 раз


  1. Urbem
    29.04.2016 14:38

    От куда вы все знаете?
    Вот как вы пришли к этим знаниям?
    И какие интерфейсы стоит изучать в первую очередь(.NET)?


    1. InWake
      29.04.2016 15:05

      Ну знаю я далеко не все
      Стандартный путь дзена — быдлокодинг -> шаблоны -> solid -> быдлокодинг -> просветление
      на самом деле очень важно работать в команде и обмениваться знаниями, а так же критика

      И какие интерфейсы стоит изучать в первую очередь(.NET)?
      что есть интерфейс?


  1. xmetropol
    29.04.2016 14:39

    Методы 1, 2 и 3 генерируют одинаковый IL код, разница лишь в предпочтении автора. 1-й может привести к ошибке, в случае переименования свойства,. 2-й метод в этом плане удобнее, т.к. инструмент вроде ReSharper автоматически переименует и свойство, и конструкцию nameof(Property). 5-й метод может быть удобен для реализации дополнительной логики: логирование, сравнение значений и генерация события лишь на действительном изменении значения, генерация события PropertyChanging а потом PropertyChanged. 4,6,7 не вижу смысла, да и вообще велосипеды облегчения реализации INotifyPropertyChanged не читаю, будто других проблем в WPF нет, как сэкономить 1 строку кода на 1-м свойстве. «Дабпиэф ничего не поделаешь» при отрисовке списка говорят, наверное, только джуны, никогда не читавшие исходников VirtualizingStackPanel и ItemsControl и не писавших свои панели, реализующие виртуализацию отображения, там в принципе нечему тормозить.


    1. InWake
      29.04.2016 14:48

      В целом поддерживаю, за исключением четвертой улитки, хорошая альтернатива nameof в условиях отсутствия c# 6 версии. Ну и про тормознутый список говорил не юниор, а разработчик которому было искрнни лень исправлять проблемный код