Как стать автором
Обновить

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

Вопрос: может ли проверка на null во втором случае повлиять на результат как-то?


На самом деле непонятно, почему "Fast" вообще должен быть быстрее "Slow". Вряд ли можно как-то оптмизировать сеттеры инстанс-пропертей, потому что вы не скмпилируете метод, который требует this. Рискну предположить что если бы были доступны статические методы вида void Set_PropertyName(ThisType @this, PropType value) => @this.PropertyName = value;, то тогда их можно было бы скомпилировать без проблем и закэшировать.


У меня в одном из проектов выполняется похожий доступ к свойствам объекта (запись/чтение), но важен порядок. Чтобы не переписывать все руками, порядок полей размечен атрибутами, и в нужный момент, при первом досутпе к одному из упорядоченных методов сначала собирается упорядоченная коллекция PropertyInfo, после чего на основе нее генерируется динамический метод. Условно, если вы хотите скопировать значения в порядке, определнным атрибутами, вы пишете метод вида @this.Prop1 = that.Prop1; @this.Prop2 = that.Prop2; и т.д., но делаете это в рантайме. После чего такой метод компилируется в делегат Action<MyType, MyType> и вызывается где нужно. За скорость не скажу, но вряд ли это будет медленне вызова геттеров/сеттеров через рефлекшн.

Ответ: да, там элвис в цепочке вызовов. Можно его перебросить в нижний вызов и убрать саму проверку. Тогда рискуем только ненужными операциями по установке нулл значения туда, где и так нулл по дефолту.

Может я чего-то не знаю, но что мешает нам компилировать сеттер объекта, который работает с this? То есть, извлекая из проперти (допустим FullName) сеттер я получаю тот самый экземплярный Set_FullName, который вызывается как метод инстанса параметра. То есть конечная лямба перед компиляцией будет выглядеть так: (entity, value) => entity.Set_FullName(value).

Про пример с упорядоченной коллекцией на вскидку добавить нечего. Если каждый вызов к данному методу метод просто сопоставляется с типами и извлекается из кеша, работать будет настолько быстро, насколько оптимальным в итоге оказалась скомпилированная функция. Если постоянно будут идти обращения к кастомным атрибутам, обход дерева, компиляция — тут интуитивно кажется, что утечки производительности будут. Но надо замерять, чтобы о чем-то говорить предметно.
Данный кейс один из немногих, где использование dynamic оправдано. Но все, всегда, про него забывают…
А как бы тут помог dynamic?
А не было бы интересно побенчмаркать?
Интересно, я просто не пойму, как его тут предлагают использовать.
Набросал небольшой прототип. В кратце, для данного типа задач, если использовать динамику — рефлексия вовсе не нужна и производительность будет упираться в скорость парсера и эффективность алгоритма сопоставления псевдонимов.

Собственно исходник
За основу взят ваш код, только Autofac заменен родным Core DI, а вместо моков используется InMemoryDatabase.
Нет, это не то же самое. Я логику понял. Доступ к членам заменяется на доступ к словарю по ключам. Тут словарь надо ставить конкурентный, но это мелочь.

Приведенная реализация не позволит добиться тех же результатов, что и рассматриваемая в статье — конечный тип не будет типом Contract. Из очевидного — его нельзя будет передать в типобезопасные методы. Так же нельзя будет сохранить его в БД с помощью DB контекста как другую валидную энтити. Ближайшее, что можно сделать — сунуть его в автомаппер, с надеждой что тот перемаппит этот динамический объект в нужную нам энтити, но по сути автомаппер сделает то же, что предлагаю делать я. Соответственно, замерять производительность нет смысла.

Вы абсолютно правы. Мой изначальный комментарий относился не к сути статьи.

А к тому что подобные задачи можно решать эффективнее, за счет использования динамики. Несмотря на свою неспешность, DLR все еще производительнее рефлексии.
А производительнее ли?

По сути, в момент вызова свойства там же идет не вызов геттера, а некий перехват, который надо обработать.

Не может такой доступ к члена быть быстрей или хотя бы таким же по скорости как просто вызов экземплярного метода.

Так что в лучшем случае мы тут ускоряемся на гидрации экземпляра и теряем при каждом чтении свойств.

Типобезопасность, да и в принципе сложность исполнения я бы сказал тут в паритете… Так что не является ли использование DynamicObject «кашей из топора»?
Тут надо измерять, но в случае с DynamicObject мы подключаем целый рантайм, который берет на себя многие вещи, в том числе и кэширование.

Т.е. первый вызов TryGetMember для каждого из параметров будет очень медленным, но потом будет использоваться уже закэшированная версия. По сути, тот же финт ушами, что и с заранее скомпилированной лямбдой для рефлексии, только автоматически и без рефлексии.

Но главная фишка не в этом. А в более гибком подходе, например, мы можем загрузить все исходные данные в список, потом сделать выборку и отрефлексить только нужные данные.

Этот подход не лучше и не хуже, описанного в статье, но именно на данном классе задач, динамика дает довольно большое преимущество перед статикой опирающейся на отражение.
Буду иметь в виду Вашу точку зрения. Может быть однажды докажу ее или опровергну.
Я объяснил выше. Изначальной целью статьи было другое. Так что я бы скорей назвал её неудачной саму по себе. :) Цели были иные — вызвать больше интереса к деревьям выражений.
Очень здорово что даже получив отрицательный результат, Вы оформили это в статейку, дабы добро не пропадало в назидание будущим поколениям.
Спасибо. Рад, что в конечном счете труд не пропал зря.
Тема мне интересна, статья богата полезным материалом, рассуждениями. Но подача… Да, я активно с рефлекшном не работал. Но проекты были большие и разнородные. Последний раз писал на c# около года назад. Сижу и «гуглю», что же такое «IL эмиссия», почему её «лучше нафиг» и что же такое этот XD.

Когда до «CLR расчехляет ГЦ и понеслись фризы» дошёл, пытался понять, а не о сборщике ли мусора говорит автор. Потом дошло, что ГЦ — это транслитерированный gc.

И так далее. Приходится отрываться, додумывать, что же хотел сказать автор, искать что-то в Сети, иногда сообщать об опечатках… И потом, возвращаясь к статье, долго и мучительно искать то место, где закончил читать — абзацы-то друг от друга не отделены…

Извините, не смог себя заставить дочитать. Попозже сделаю ещё один «подход к снаряду»…
Жаргонизмы, к сожалению, их хода мыслей не убрать, потому что со временем они становятся теми абстракциями, на которых разработчик мыслит. Так что за ГЦ — прошу извинить, но он останется хотя бы ради популяризации. На указанные неточности обращу внимание, в том числе на эмиссию и опечатки в окончаниях.
Насчет абзацев — да. Я отверстаю. Насчет них думал. Давно не хабре не размещал статьи, не был уверен в том, как будет выглядеть финальная верстка. На веб вью в ВК читать вообще трудно. Обратил внимание утром.
Я не жаргонизмы имел в виду. И опечатки — это тоже не беда, всё нормально. Просто часто мне было трудно понять, что имеется в виду в том или ином случае.

Спасибо, что разделили абзацы — стало лучше))
Тога ревью пройдено. Мердж, коллега. )

Я хоть и стараюсь не использовать жаргонизмы, особенно в письменной речи, но их наличие в статье не ухудшило читабельность. Видимо они уже давно вошли в мой обиход.

О какой скорости может идти речь если у вас там строковые сравнения имён свойств повсюду? Если уж кешировать, так кешируйте


Dictionary<Type / source type /, Dictionary< Type / destination type /, Func<object, object> / transform / >

Только кешируйте так чтоб transform ничего не знал уже про имена свойств и прочую рефлексию. Впрочем, проще взять автомаппер, я не проверял, но он скорей всего так и делает.

Строковые сравнения не являются причиной утечки производительности в данном случае. И их нельзя заменить на сравнение хэшей типов, так как в условии примера был доступ к данным из БД. Корреляция в данном случае устанавливается по строковым ключам.

В конечном делегате вызова рефлексии и нет.

Автомаппер делает именно так, но он не смаппит Вам словарь в объект без настройки под конкретный DestinationType методом — трансформеров. Не помню, как они у него называются. резолверы вроде.

Ну как бы из базы надо читать в DTO и уже из него мапить. Чтоб сложить в DTO строки не нужны, потому что SqlDataReader умеет читать по номеру запрошенной колонки (вы ведь знаете какие вы колонки запросили).


А если так делать лень и всё-таки хочется строки — то надо Dictionary для имён использовать а не обходить все подряд через FirstOrDefault.

Первый пункт — мы выносим сопоставление типов по строкам из конечной логики в логику инициализации, сопоставляя при чтении из бд и маппинге строковые значения объектам PropertyInfo в DTO. Этих объектов в общем случае больше, чем реальных пропертей, потому что на одно свойство может быть N маппингов. Тут мы делаем много лишней работы, тем более что строку к PropertyInfo просто так, без кода резолва SqlDataReader не приведет. Это новая логика, новая работа, новый мусор. Я согласен, что это имеет смысл, если за один парсинг перебирается условно много писем, а самих маппингов в бд условно мало на каждое свойство. Но что будет работать быстрей — надо устанавливать эмпирически.

Второй пункт — жертва паттерну, а с Вашей точкой зрения я согласен. Там сначала и был словарь, который я убрал в конечной реализации и заменил на корреляционные пары. Dictionary<string, string> в шаблонном методе делает достаточно трудным восприятие контекста, в котором надо переопределять метод в наследнике. Если бы Вы не обратили внимание на утечку производительности тут, двое других читателей указали бы на некрасиво реализованный паттерн. Мне показалось второе более постыдным. Только поэтому я сделал так. Я считаю, что по Вашей рекомендации и надо было бы делать в энтерпрайзе, так как доступ по хэшу в словаре действительно быстрый, он был бы даже быстрее, чем поиск по хэшу типа через LINQ, как Вы предлагали изначально, так как не было бы итерирования (а с ним и мусора).

Я правильно понял что вы по сути делаете метод типа


_sqlConnection.ReadVector(«select field1, field2 from table») // returns MyEntity[]

Пара уточняющих вопросов


  • а почему не использовать EntityFramework/NHibernate/другой ORM который такое делает сам?
  • сколько у вас записей — вам точно нужна оптимизация? Если вы вытягиваете «тысячи/миллионы» записей, возможно вам нужен другой механизм типа user-defined sql types для ms sql (позволяет передавать в базу массивы объектов, на запись в базу точно быстрее, насчёт чтения не уверен)
Нет, мне извлекает схемы маппинга EF Core из DbContext'а.
Я про оптимизацию при работе с БД ни слова не говорил, я отвечаю просто на изначальный вопрос о сравнении по ключам.
Можете загрузить мои исходники и проверить самостоятельно. Найдете ошибку — все вместе будем знать.
Могу только предположить, что PropertyInfo.SetMethod.Invoke работает медленней, чем SetValue или кешируется иначе самой CLR.
А в целом, я тоже считаю свой результат странным. Но для меня было важно именно SetValue проверить против скомпилированного делегата с обращением напрямую к сеттеру.

SetMethod.Invoke, на самом деле, работает чуть быстрее (на 5-10%) так как SetValue собственно к нему и обращается:


internal sealed class RuntimePropertyInfo : PropertyInfo
{
...
    [DebuggerStepThrough]
    [DebuggerHidden]
    public override void SetValue(
      object obj,
      object value,
      BindingFlags invokeAttr,
      Binder binder,
      object[] index,
      CultureInfo culture)
    {
      MethodInfo setMethod = this.GetSetMethod(true);
      if (setMethod == (MethodInfo) null)
        throw new ArgumentException(SR.Arg_SetMethNotFnd);
      object[] parameters;
      if (index != null)
      {
        parameters = new object[index.Length + 1];
        for (int index1 = 0; index1 < index.Length; ++index1)
          parameters[index1] = index[index1];
        parameters[index.Length] = value;
      }
      else
        parameters = new object[1]{ value };
      setMethod.Invoke(obj, invokeAttr, binder, parameters, culture);
    }
}
Надо разбирать тогда пошагово. Возможно та разница, которая видна у Вас, в моем случае просто зашумляется остальной работой.

Нашел одну из проблем:


public static IStorage InstanceDb()
{
    var mock = new Mock<HabraDbContext>();
    mock.Setup(x => x.ContactMapSchemas).ReturnsDbSet(GetFakeData());

    return mock.Object;
}

Setup надо выставлять перед каждым вызовом (или лучше вообще эти моки убрать), а у вас получается, что "FakeData" возвращается всего один раз, при первом вызове, а потом null, что отключает всю работу по созданию Contact .

Это не должно быть проблемой — я проверял с возвратом инициализированной коллекции напрямую без Мокью. Не было разницы.

Кроме того, я не совсем понял ход Ваших мыслей. Почему возвращается null?

Впрочем, я проверю.
А Вы правы. Сейчас откалибруюсь.
Результаты и правда другие. Судя по всему, когда я проверял работу без Moq, я упустил какой-то другой фактор и это исправление не сказалось.

Статью придется переписать еще раз. Большое спасибо за помощь.

В общем чуда не случилось. Выкинул из вашего кода всякую ересь типа Моков (автоматом ушел тот баг с Setup), Автофаков, Тасков. Вынес парсинг за переделы теста и получилось то, что и ожидалось — Рефлексия гораздо медленнее!


| FastHydration | 1 |   836.9 ns | 14.60 ns | 28.82 ns | 0.4053 |     - |     - |   1.24 KB |
| SlowHydration | 1 | 2,488.7 ns | 46.45 ns | 47.70 ns | 0.5646 |     - |     - |   1.73 KB |

Вот исходники (извините, что я запихал все в один файл :) ): https://gist.github.com/0x1000000/668bb6286042bceba6ecb0f40c1d91d3

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

А я вот читал в книжках, что эту ересь типа LINQ в критичных для производительности участках принято вычищать. Не может ли быть такое что эта ересь у вас реально используется а у других может и не использоваться?


Ещё мне не очень понятно, почему при таких составных бенчмарках не был использован профайлер чтобы посмотреть что именно там является узким местом. Насколько я знаю, в benchmark.net есть даже встроенная возможность собирать трейсы для профайлеров.


P.s. Про производительность на .net есть несколько хороших книжек. Например, dreamwalker написал конкретно про бенчмарки. Возможно, там можно почерпнуть что-то, чтобы изменять корректнее и делать более правильные выводы.

Профайлер не был использован, потому что там профилировать нечего. Ошибка была найдена достаточно быстро благодаря комментариям.

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

А LINQ — это неизбежное зло в бизнес-логике, его просто так не вычистишь. Есть проекты в которых за спринт меняются требования и переписывается код по нескольку раз. Писать логику без LINQ — самоубийство. Оптимизировать логику в таких местах можно только после того, как продукт стабилизируется.

Таким образом я не задаюсь глобальным вопросом оптимизации, в данной статье рассматривался конкретно один кейс с конкретно одним результатом, который подтвердить из-за ошибки удалось не сразу. Если что-то осталось за рамками статьи, это не значит, что у меня это вызывает какие-то вопросы или требует заполнения пробелов. Это значит лишь то, что я не ставил своей целью писать еще одну книгу о перформансе.

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

С моей точки зрения это минус — тратить время человека, если можно пользоваться инструментом.


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

Не увидели ли бы вы сразу узкое место, если воспользовались профайлером? Может сложность из-за этого?


А LINQ — это неизбежное зло в бизнес-логике, его просто так не вычистишь.

Его не нужно вычищать весь. Достаточного только узкие места. Я бы скорее отнес ваш код к инфраструктур не ому уровню чем к бизнес логике.


Оптимизировать логику в таких местах можно только после того, как продукт стабилизируется.

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


Результат генерации рефлекшеном поведения необходимо компилировать в делегаты для ускорения и сокращения выброса мусора

Тут какая-то описка. Если это то о чем я думаю, то зачем вообще нужна статья, что в ней нового сказано?

С моей точки зрения это минус — тратить время человека, если можно пользоваться инструментом.

Код ревью — тоже потеря времени? Или вы за все идеальное против всего реалистичного? Bugs happens.

Не увидели ли бы вы сразу узкое место, если воспользовались профайлером? Может сложность из-за этого?

Нет, сложностей нет. Есть ошибка и недоработка. К сожалению, совпало много фактов, которые не сильно усложняют работу по отдельности, но в сумме скрыли источник бага.

Его не нужно вычищать весь. Достаточного только узкие места. Я бы скорее отнес ваш код к инфраструктур не ому уровню чем к бизнес логике.

Тут Ваше мнение против моего, и мне не принципиально доказывать свое. Инфраструктурный это уровень или нет — вопрос философский. Я в таких спорах не участвую. Мой уровень отраслевой культуры мне этого не позволяет. Он низковат для онтологических обсуждений.

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

Согласен, результатом этой мысли стало название статьи. Я не считаю, что добился изначальных целей, публикуя её.

Тут какая-то описка. Если это то о чем я думаю, то зачем вообще нужна статья, что в ней нового сказано?

Вам не нужна. Для Вас это банальность. А кому-то лишнее напоминание. По моим меркам интерес большой, я ожидал гораздо более прохладной реакции. )
Код ревью — тоже потеря времени?

Да, если можно пользоваться инструментом. Например на финальное кодревью надо присылать код в котором не падают тесты и так далее.


реальной жизни заметить разницу между хорошим подходом и плохим достаточно сложно

Нет, сложностей нет.

Да, если можно пользоваться инструментом. Например на финальное кодревью надо присылать код в котором не падают тесты и так далее.

Тесты не падали. Вы пересмотрели свое отношение к ревью?

По второй цитате — Я не буду спорить с самим собой, потому, что я понимаю и помню контекст, в которых были написаны обе фразы. Они друг другу не противоречат. Если Вы воспринимаете их в другом ключе, перестройте аргумент линейно и закрыто — чтобы я ответил да или нет, и я озвучу свою позицию. Или уточню.
Тесты не падали. Вы пересмотрели свое отношение к ревью?

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


У меня складывается впечатление, что вы мои коментарии читаете читаете в упрощенном виде. Я пишу, что в критических по производительности участках LINQ принято вычищать — вы читаете что я призываю избавиться от всего LINQ, я пишу, кто кодревью потеря времени, если можно проверить автоматически, вы читаете, что все код ревью потеря времени.


в реальной жизни заметить разницу между хорошим подходом и плохим достаточно сложно

У меня такой ход рассуждений:


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


Почему с этим сложность?
Потому, что мы сравниваем общую производительность смеси а не по отдельности.


Можно ли как-то измерять производительность компонентов из смеси?
Да, для этого есть специальные инструменты — профайлеры причем используемый фреймворк для бенчмаркинга поддерживает интеграцию c ними.


Можно отправить код на ревью вместо использование профайлера, но:


  • это тратит время другого человека, а не инструмента
  • это менее точно (человек может пропустить что-то — инструмент не отвлекается, хотя у него есть свои ограничения)

зато:


  • человек может что-то подсказать
  • человек, в отличие от профайлера, может проанализировать не только тот сценарий который используется но и другие

Поэтому логично сначала воспользоваться инструментом, а потом человеком, если останется в этом потребность. На кодревью присылать компилирующийся код, по которому проходят тесты.

Так цели абстрактно сравнить производительность конкретно двух подходов к рефлексии и не было, это же сделано до меня в статье, на которую я сослался в одном из первых абзацев.

В общем, я не знаю, о чем наш разговор — то, что Вы пишете — это прописные истины, это факт. Незыблемый факт. Я не буду с этим спорить. Согласен со всем на 100%.

Я могу сослаться только на недопонимание.
А можете проверить как изменится производительность если вместо ConcurrentDictionary использовать просто Dictionary?
Я понимаю, что автомаппер использует ConcurrentDictionary, но это не значит, что вам тоже его надо использовать.
У вас же Slow и Manual подходы не потокобезопасные.
Не факт, что это что-то изменит, но проверить стоит мне кажется.

P.S. я бы и сам проверил, но дотнета нигде под рукой нет.
Я проверял — разницы нет. По сути, там и с обычным словарем будет поведение потокобезопасное, потому что в него ничего не пишется.

Какой смысл в использовании Dictionary в классе FastContactHydrator? На сколько я понимаю, все его использование сводится к инициализации и циклом по всем его элементам. Поиска в словаре я не нашел.

И действительно. Никакого. Артефакт изначальной реализации, когда я хотел итерировать по свойствам, а из словаря извлекать по ключу. Но потом забыл поменять. )

Я правильно понимаю, что замена метода FastContactHydrator.GetContact на метод похожий на нижеследующий, выполняет те же функции, что и код автора и должен улучшить время тестов для этого класса?


protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
{
  var contact = new Contact();
  foreach (var corrItem in correlations) {
    if (_proprtySettersMap.TryGetValue(corrItem.PropertyName, out var action)) {
      action(contact, corrItem.Value);
    }
  }
  return contact;
}
Да, без LINQ работать будет быстрей гораздо + меньше мусора.
Но это внесет шум в замеры. Там так вызодит, что на фоне LINQ вообще трудно заметить что-то остальное.

Добавил в код автора откорректироанный класс FastContactHydrator, с использованием словаря как задумывалось.


Новый класс выглядит так
using FastReslectionForHabrahabr.Interfaces;
using FastReslectionForHabrahabr.Models;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

namespace FastReslectionForHabrahabr.Hydrators
{
    public class FastContactHydrator2 : ContactHydratorBase
    {
        private static readonly Dictionary<string, Action<Contact, string>> _proprtySettersMap = 
            new Dictionary<string, Action<Contact, string>>();
        static FastContactHydrator2()
        {
            var type = typeof(Contact);
            foreach (var property in type.GetProperties())
            {
                _proprtySettersMap[property.Name] = GetSetterAction(property);
            }
        }

        public FastContactHydrator2(IRawStringParser normalizer, IStorage db) : base(normalizer, db)
        {
        }

        private static Action<Contact, string> GetSetterAction(PropertyInfo property)
        {
            var setterInfo = property.GetSetMethod();
            var paramValueOriginal = Expression.Parameter(property.PropertyType, "value");
            var paramEntity = Expression.Parameter(typeof(Contact), "entity");
            var setterExp = Expression.Call(paramEntity, setterInfo, paramValueOriginal).Reduce();

            var lambda = (Expression<Action<Contact, string>>)Expression.Lambda(setterExp, paramEntity, paramValueOriginal);

            return lambda.Compile();
        }

        protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
        {
            var contact = new Contact();
            foreach (var corrItem in correlations) {
                if (_proprtySettersMap.TryGetValue(corrItem.PropertyName, out var action)) {
                    action(contact, corrItem.Value);
                }
            }
            return contact;
        }
    }
}

Получились следующие результаты. Намерянно убрал тесты для 1000 итераций, ибо долго ждать.


Таблица
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.815 (1909/November2018Update/19H2)
Intel Core i5-8265U CPU 1.60GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.201
  [Host]     : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT
  DefaultJob : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT

|              Method |   N |         Mean |       Error |      StdDev |     Gen 0 |     Gen 1 | Gen 2 |   Allocated |
|-------------------- |---- |-------------:|------------:|------------:|----------:|----------:|------:|------------:|
|     ManualHydration |   1 |     209.9 us |     1.27 us |     1.19 us |   16.6016 |    0.4883 |     - |    51.72 KB |
|      FastHydration2 |   1 |     213.1 us |     1.18 us |     1.04 us |   16.6016 |    0.4883 |     - |    51.72 KB |
|       SlowHydration |   1 |     216.0 us |     1.32 us |     1.23 us |   17.5781 |         - |     - |    54.06 KB |
|       FastHydration |   1 |     216.3 us |     1.80 us |     1.69 us |   17.5781 |         - |     - |    54.56 KB |
|      FastHydration2 | 100 |   3,724.5 us |    31.73 us |    28.13 us |  558.5938 |  148.4375 |     - |  2080.88 KB |
|     ManualHydration | 100 |   3,880.2 us |    29.49 us |    27.59 us |  562.5000 |  164.0625 |     - |  2080.88 KB |
|       SlowHydration | 100 |   3,912.3 us |    76.08 us |    74.72 us |  718.7500 |   31.2500 |     - |  2287.67 KB |
|       FastHydration | 100 |   4,236.1 us |    33.16 us |    31.02 us |  609.3750 |  187.5000 |     - |  2356.08 KB |
| ManualHydrationLinq |   1 |   5,166.2 us |    24.01 us |    18.74 us |   85.9375 |   39.0625 |     - |   282.62 KB |
|   SlowHydrationLinq |   1 |   5,214.2 us |    23.14 us |    21.65 us |   85.9375 |   39.0625 |     - |   286.85 KB |
|   FastHydrationLinq |   1 |   7,125.2 us |   349.07 us | 1,012.71 us |         - |         - |     - |   292.84 KB |
| ManualHydrationLinq | 100 | 503,598.7 us | 3,772.22 us | 3,528.53 us | 8000.0000 | 4000.0000 |     - | 25351.68 KB |
|   FastHydrationLinq | 100 | 503,719.4 us | 2,781.42 us | 2,601.74 us | 8000.0000 | 4000.0000 |     - | 25196.27 KB |
|   SlowHydrationLinq | 100 | 504,187.5 us | 2,680.45 us | 2,376.15 us | 8000.0000 | 4000.0000 |     - | 25782.91 KB |
Тут выигрыш за счет того, что Вы оптимизируете доступ к элементам, а не гидрацию, что было примером статьи. Фактически, LINQ сильно зашумляет результат. За ним иной разницы мало заметно. Выше с пользователем Dmitry Tikhonov 0x1000000 мы установили реальную причину таких результатов.
И статья опять потеряла смысл. Вернее, при таких бенчах она должна быть о другом.
Ну почему? Я переписал её, восстановив часть исходного смысла. Так выводы сделать можно. Я думаю. Во-первых, LINQ зашумляет до полной незаметности влияние рефлексии, а LINQ там относительно не много. Во-вторых, даже не считая IO, на фоне остальной работы рефлексия вредит не так сильно, и в-третьих, пример оптимизации все равно наглядно демонстрируется и реализует свою задачу. Понимаю, что целостное восприятие три раза переписанного не самая простая задача, но такова наша жизнь — диалектическое воспритие меняющейся системы в нескольких её состояниях. #мыжпрограммисты.
Блин, проблема рефлексии как бе не в скорости а в ошибка в рантайме. Которые потом мучится и чинить такие баги.
А проблема Expression'ов еще и в усложненном дебаге. Статья рассматривает конкретный аспект и не навязывает использование рефлексии. Но если её использовать. Было бы неплохо идти до конца и компилировать результат.

Ещё одно узкое место в реализации автором метода ContactHydratorBase.GetPropertiesValues — использование x.EntityName.ToUpperInvariant() == _typeName.ToUpperInvariant() вместо x.EntityName.Equals(_typeName, StringComparison.InvariantCultureIgnoreCase). Так же был удален ну нужный вызов .TaArray() в конце.


Измененный ContactHydratorBase.GetPropertiesValues
private async Task<PropertyToValueCorrelation[]> GetPropertiesValues2(string rawData, CancellationToken abort)
{
  var mailPairs = _normalizer.ParseWithLinq(rawData: rawData, pairDelimiter: Environment.NewLine);
  var mapSchemas = 
                _mapSchemas
                .Where(x => x.EntityName.Equals(_typeName, StringComparison.InvariantCultureIgnoreCase))
                .Select(x => new { x.Key, x.Property });  
  return
                mailPairs
                .Join(mapSchemas, x => x.Key, x => x.Key, 
                    (x, y) => new PropertyToValueCorrelation { PropertyName = y.Property, Value = x.Value })
                .ToArray();
}

Результат ниже. Как видно Linq не так уж и плох. При 100 итерациях — самый быстрый тест.


Обновление: В тесте FastHydrationLinq2 используется модифицированный класс FastContactHydrator2, код которого можно найти выше в моём коментарии.


Таблица
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.815 (1909/November2018Update/19H2)
Intel Core i5-8265U CPU 1.60GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.201
  [Host]     : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT
  DefaultJob : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT

|              Method |   N |       Mean |    Error |    StdDev |     Gen 0 |  Gen 1 | Gen 2 |  Allocated |
|-------------------- |---- |-----------:|---------:|----------:|----------:|-------:|------:|-----------:|
|     ManualHydration |   1 |   185.9 us |  2.74 us |   2.57 us |   12.6953 | 3.9063 |     - |   39.39 KB |
|       FastHydration |   1 |   187.1 us |  1.50 us |   1.25 us |   13.9160 | 4.6387 |     - |    42.9 KB |
|      FastHydration2 |   1 |   189.4 us |  3.35 us |   5.12 us |   12.6953 | 4.3945 |     - |    39.4 KB |
|       SlowHydration |   1 |   194.3 us |  1.25 us |   1.16 us |   14.1602 | 4.8828 |     - |   43.71 KB |
| ManualHydrationLinq |   1 |   196.2 us |  2.05 us |   1.81 us |   22.4609 |      - |     - |   69.04 KB |
|  FastHydrationLinq2 |   1 |   199.0 us |  3.96 us |   6.83 us |   17.0898 | 0.4883 |     - |   53.66 KB |
|   SlowHydrationLinq |   1 |   205.0 us |  2.65 us |   2.48 us |   23.9258 | 0.9766 |     - |   73.36 KB |
|     ManualHydration |  10 |   424.4 us |  5.31 us |   4.96 us |   49.8047 |      - |     - |  153.47 KB |
|  FastHydrationLinq2 |  10 |   425.0 us |  5.32 us |   4.98 us |   96.6797 |      - |     - |   296.1 KB |
|      FastHydration2 |  10 |   429.0 us |  4.26 us |   3.99 us |   49.8047 |      - |     - |  153.47 KB |
|       FastHydration |  10 |   468.7 us |  4.86 us |   4.31 us |   61.5234 |      - |     - |   188.5 KB |
| ManualHydrationLinq |  10 |   480.5 us |  4.21 us |   3.94 us |  146.4844 |      - |     - |  449.99 KB |
|       SlowHydration |  10 |   494.9 us |  9.61 us |  12.16 us |   63.4766 |      - |     - |  196.57 KB |
|   FastHydrationLinq |  10 |   521.0 us |  8.21 us |   7.68 us |  158.2031 |      - |     - |  485.04 KB |
|   FastHydrationLinq |   1 |   526.4 us | 29.40 us |  79.99 us |         - |      - |     - |   76.91 KB |
|   SlowHydrationLinq |  10 |   564.9 us | 11.15 us |  18.64 us |  161.1328 |      - |     - |  493.16 KB |
|  FastHydrationLinq2 | 100 | 2,484.9 us | 19.80 us |  17.55 us |  886.7188 |      - |     - | 2720.61 KB |
|     ManualHydration | 100 | 2,799.1 us | 18.37 us |  17.18 us |  421.8750 |      - |     - | 1294.32 KB |
|      FastHydration2 | 100 | 2,808.1 us | 15.46 us |  13.70 us |  421.8750 |      - |     - | 1294.29 KB |
|       FastHydration | 100 | 3,218.1 us | 32.94 us |  29.20 us |  535.1563 |      - |     - | 1644.62 KB |
| ManualHydrationLinq | 100 | 3,226.3 us | 15.63 us |  14.62 us | 1390.6250 |      - |     - | 4258.44 KB |
|       SlowHydration | 100 | 3,385.6 us | 60.55 us |  74.36 us |  562.5000 |      - |     - | 1725.93 KB |
|   FastHydrationLinq | 100 | 3,800.7 us | 74.25 us | 126.08 us | 1503.9063 |      - |     - | 4608.42 KB |
|   SlowHydrationLinq | 100 | 4,038.8 us | 26.15 us |  24.46 us | 1531.2500 |      - |     - | 4689.66 KB |
Первое — сравнение строк в верхнем регистре — это по сути и есть эквалс, но! Вам следует на будущее учитывать, что EF не понимает Equals, и извлечение из бд нужно делать по оператору ==. Если будете ставить Equals, EF не сможет транслировать код в SQL. Посмотрите по истории коммитов, там сперва был Мок DB Context'a EF Core. То, что Вы видите — суть артефакт от него. Как и ToArray(). В исходной реализации тут вызывался ToArrayAsync().

Кроме того, ToArray() — это важный вызов. Фактически, в этот момент вызывается LINQ-овский Deffered Execution. Если его не делать, LINQ возвратит IEnumerable, который в себе сохранил ноды выражений, встроенные в одно дерево и если вынешний код этого не учитывает, а работает с коллекцией как с коллекцией в памяти, можно словить лишнее итерирование, и оно вообще производительность убьет. Поэтому ToList() и ToArray() вызываются не редко в конце цепочки, это факт.

В остальном, не было цели оптимизировать код, была цель сравнить влияние двух подходов к отражению на фоне иной логики. После обнаружения бага не весь код был переписан с выпиливанием артефактов, за это прошу меня извинить.
Первое — сравнение строк в верхнем регистре — это по сути и есть эквалс, но!

По сути да, но не по производительности. В тегах указана "высокая производительность".


Вам следует на будущее учитывать, что EF не понимает Equals, и извлечение из бд нужно делать по оператору ==. Если будете ставить Equals, EF не сможет транслировать код в SQL.

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


Если говорить про БД, то есть высокая вероятность, что выборка всей таблицы ContactMapSchemas в методе GetPropertiesValuesWithoutLinq сведет к минимуму преимущества в производительности.

1. По производительности в том числе они почти эквивалентны.
2. Мы сейчас идем в добри. Если мы храним данные в ДБ в верхнем регистре, мы избегаем одной из операций — приведения в верхний регистр строки в предикате. Экономим на вызове UPPER() в оракле или её эквивалента в других СУБД. Но мы должны быть уверены, что все данные туда добавят в верхнем регистре. Однажды кто-то нарушит эту схему, и будет баг. Если это бутылочное горлышко по перформансу, имеет смысл так сделать, если нет — можно заигнорировать. Хранить в нижнем регистре не надо, сравнение в верхнем всегда идет быстрей.

3. Есть вероятность того. Это зависит от многих факторов. Если таблицы большие, а предикаты будут примитивные, надо будет возвращать LINQ и через EF фильтровать. Если таблицы будут небольшие, надо будет смотреть, как это триггерит ГЦ. В общем случае лучше не строить предикат, если можно безболезненно вытащить всю таблицу в память.


По производительности в том числе они почти эквивалентны.

Я не уверен, надо бы написать тест.


Кажется я нашел проблему в Вашем коде, которая делает Linq-тесты медленнее. У Вас происходит преобразование своейства _typeName в верхний регистр в каждой итерации. Достаточно инициализировать это свойсво строкой в верхнем регистре, т.е. _typeName = type.FullName.ToUpperInvariant(); и ниже сравнивать как .Where(x => x.EntityName.ToUpperInvariant() == _typeName)


После такого изменения, Linq-тесты работают быстрее, для тестов с 10 и более итераций.


Вот результаты откоректированных тестов
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.815 (1909/November2018Update/19H2)
Intel Core i5-8265U CPU 1.60GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.201
  [Host]     : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT
  DefaultJob : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT

|              Method |    N |        Mean |     Error |    StdDev |      Gen 0 |  Gen 1 | Gen 2 |   Allocated |
|-------------------- |----- |------------:|----------:|----------:|-----------:|-------:|------:|------------:|
| ManualHydrationLinq |    1 |    190.3 us |   0.79 us |   0.61 us |    20.0195 |      - |     - |    62.03 KB |
|     ManualHydration |    1 |    197.0 us |   1.04 us |   0.97 us |    12.6953 | 4.3945 |     - |    39.39 KB |
|       FastHydration |    1 |    200.7 us |   1.09 us |   0.96 us |    13.9160 | 4.6387 |     - |     42.9 KB |
|   SlowHydrationLinq |    1 |    201.0 us |   1.34 us |   1.25 us |    21.4844 |      - |     - |    66.36 KB |
|       SlowHydration |    1 |    204.4 us |   0.69 us |   0.61 us |    14.1602 | 4.8828 |     - |     43.7 KB |
| ManualHydrationLinq |   10 |    451.5 us |   3.59 us |   3.36 us |   124.0234 |      - |     - |    379.8 KB |
|   FastHydrationLinq |   10 |    490.6 us |   5.70 us |   5.33 us |   135.7422 |      - |     - |   414.97 KB |
|   FastHydrationLinq |    1 |    507.2 us |  38.36 us | 101.05 us |          - |      - |     - |    69.91 KB |
|   SlowHydrationLinq |   10 |    526.7 us |  10.44 us |  11.17 us |   137.6953 |      - |     - |   423.09 KB |
|     ManualHydration |   10 |    542.4 us |   3.05 us |   2.85 us |    49.8047 |      - |     - |   153.47 KB |
|       FastHydration |   10 |    599.6 us |   8.18 us |   7.65 us |    61.5234 |      - |     - |   188.51 KB |
|       SlowHydration |   10 |    607.4 us |   3.43 us |   3.04 us |    63.4766 |      - |     - |   196.57 KB |
| ManualHydrationLinq |  100 |  2,917.6 us |  30.89 us |  24.11 us |  1164.0625 |      - |     - |  3558.41 KB |
|   FastHydrationLinq |  100 |  3,290.5 us |  11.59 us |  10.84 us |  1277.3438 |      - |     - |  3908.44 KB |
|   SlowHydrationLinq |  100 |  3,590.5 us |  17.10 us |  16.00 us |  1304.6875 |      - |     - |  3989.66 KB |
|     ManualHydration |  100 |  4,159.8 us |  73.01 us |  64.72 us |   421.8750 |      - |     - |  1294.29 KB |
|       FastHydration |  100 |  4,308.2 us |  16.49 us |  15.42 us |   531.2500 |      - |     - |  1644.62 KB |
|       SlowHydration |  100 |  4,621.8 us |  25.11 us |  23.49 us |   562.5000 |      - |     - |  1725.93 KB |
| ManualHydrationLinq | 1000 | 26,088.9 us | 145.04 us | 128.58 us | 11531.2500 |      - |     - | 35318.53 KB |
|   FastHydrationLinq | 1000 | 30,015.9 us | 392.34 us | 347.80 us | 12656.2500 |      - |     - | 38818.53 KB |
|   SlowHydrationLinq | 1000 | 33,952.4 us | 614.22 us | 574.54 us | 12933.3333 |      - |     - | 39631.04 KB |
|     ManualHydration | 1000 | 38,119.2 us | 324.99 us | 304.00 us |  4076.9231 |      - |     - | 12693.55 KB |
|       FastHydration | 1000 | 40,715.2 us | 322.91 us | 286.25 us |  5230.7692 |      - |     - | 16193.55 KB |
|       SlowHydration | 1000 | 44,197.9 us | 366.73 us | 343.04 us |  5500.0000 |      - |     - | 17005.88 KB |

Опять же получается проблема с низкой скоростью выполнения Linq-тестов, не в Linq, а в бизнес-логике.

Смотрите: EF не транслирует .Equals() в SQL, поэтому у вас нет никакого выбора. Если Вам надо делать сопоставление строк в памяти — это Equals, если в SQL — это == с приведением к верхнему регистру.

В остальном используйте .Equals всегда — он короче и понятней.

В остальном магии нет, разницы не может быть, так как сравнение с игнором кейса — это сравнение к приведением к какому-то кейсу. Если Вы не вызываете ToUpper() напрямую, это не значит, что он не вызывается под капотом.

Как Вы сравните строки с игнором регистра без приведения к одному регистру? Магии никакой выполняться не будет: кодам символов надо сопоставить другие коды символов в символьной таблице. Потом сравнить. Все.

Вот результат бенча, в котором берется коллекция примерно из 60 итемов и фильтруется через Equals и == по одному свойству. Потом приводится к массиву. ToLower показывает странно большую скорость на .netCore. но не ясно почему. Рекомендации еще с Рихтера были сравнивать всегда в верхнем кейсе. Надо разбираться.

Что мы видим? Мусора при == .ToUpper() больше, но скорость +- одинаковая. ToLower выглядит идеально, но это странно. Возможно, изменилась логика работы BCL с момента, когда я этим интересовался. Но сказанного в первых абзацев это все равно не отменяет.

Тест
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|------------------- |---------:|----------:|----------:|-------:|------:|------:|----------:|
| ProcessWithToUpper | 3.466 us | 0.0337 us | 0.0281 us | 1.6937 | — | — | 5320 B |
| ProcessWithToLower | 1.649 us | 0.0291 us | 0.0273 us | 0.0877 | — | — | 280 B |
| ProcessWithEquls | 3.325 us | 0.0198 us | 0.0165 us | 0.0877 | — | — | 280 B |

Попробую объяснить по другому.


В коде x.EntityName.ToUpperInvariant() == _typeName.ToUpperInvariant(), свойство _typeName преобразуется в верхний регистр в каждом вызове.


Это избыточная операция. Если я не прав, то какой смысл преобразовывать свойство _typeName в верхний регистр в каждом вызове метода GetPropertiesValues


Достаточно написать


static ContactHydratorBase()
{
  var type = typeof(Contact);
  _typeName = type.FullName.ToUpperInvariant();
}
//....
private async Task<PropertyToValueCorrelation[]> GetPropertiesValues(string rawData, CancellationToken abort)
{
    var mailPairs = _normalizer.ParseWithLinq(rawData: rawData, pairDelimiter: Environment.NewLine);
    var mapSchemas = 
        _mapSchemas
        .Where(x => x.EntityName.ToUpperInvariant() == _typeName)
        .Select(x => new { x.Key, x.Property })
        .ToArray();

    return
        mailPairs
        .Join(mapSchemas, x => x.Key, x => x.Key, 
            (x, y) => new PropertyToValueCorrelation { PropertyName = y.Property, Value = x.Value })
        .ToArray();
}

и Linq-тесты начнут выполняться быстрее тестов без Linq.

Написал тест для измерения сравнивания строк через == .ToUpper() и .Equals
using System;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;

namespace CompareStrinngsTest
{
    public class Benchmarks
    {
        [Params(10, 100, 1000)]
        public int StringLength = 10;

        public int ListSize = 1000;

        private List<string> _lst;
        private string _strToFind;
        private string _strToFindUpper;

        private static Random random = new Random();
        private static string RandomString(int length)
        {
            const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
            return new string(Enumerable.Repeat(chars, length)
            .Select(s => s[random.Next(s.Length)]).ToArray());
        }

        [GlobalSetup]
        public void Setup() {
            _strToFind = RandomString(StringLength);
            _strToFindUpper = _strToFind.ToUpperInvariant();
            _lst = new List<string>(ListSize);
            for (int i = 0; i < ListSize; i++) {
                if (i == ListSize / 2) {
                    _lst.Add(_strToFind);
                }
                else {
                    _lst.Add(RandomString(StringLength));
                }
            }
        }

        [Benchmark]
        public int CountUpperBoth() {
            return _lst.Count(item => item.ToUpperInvariant() == _strToFind.ToUpperInvariant());
        }

        [Benchmark]
        public int CountUpperSingle() {
            return _lst.Count(item => item.ToUpperInvariant() == _strToFindUpper);
        }

        [Benchmark]
        public int CountEqualIgnoreCase() {
            return _lst.Count(item => item.Equals(_strToFind, StringComparison.InvariantCultureIgnoreCase));
        }
    }
}

Результаты
Intel Core i5-8265U CPU 1.60GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.201
  [Host]     : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT
  DefaultJob : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT

|               Method | StringLength |      Mean |     Error |    StdDev |
|--------------------- |------------- |----------:|----------:|----------:|
|       CountUpperBoth |           10 |  61.34 us |  0.432 us |  0.383 us |
|     CountUpperSingle |           10 |  41.28 us |  0.216 us |  0.202 us |
| CountEqualIgnoreCase |           10 |  53.66 us |  0.251 us |  0.196 us |
|       CountUpperBoth |          100 | 159.48 us |  1.247 us |  1.166 us |
|     CountUpperSingle |          100 |  91.42 us |  0.478 us |  0.447 us |
| CountEqualIgnoreCase |          100 |  53.80 us |  0.622 us |  0.486 us |
|       CountUpperBoth |         1000 | 993.25 us | 14.811 us | 26.327 us |
|     CountUpperSingle |         1000 | 518.15 us |  3.508 us |  3.281 us |
| CountEqualIgnoreCase |         1000 |  54.46 us |  0.265 us |  0.221 us |

Резюме:


  1. Tест CountUpperBoth самый медленный. В этом тесте используется сравнение item.ToUpperInvariant() == _strToFind.ToUpperInvariant(), точно такое же что у вас в методе ContactHydratorBase.GetPropertiesValues.
  2. Tест CountUpperSingle быстрее предыдущего. В этом тесте используется сравнение item.ToUpperInvariant() == _strToFindUpper, где _strToFindUpper референсная строка заранее преобразованная в верхний регистр. Это решение я предлагал в коментарии выше.
  3. Tест CountEqualIgnoreCase самый быстрый тест для длинных строк. В этом тесте используется сравнение item.Equals(_strToFind, StringComparison.InvariantCultureIgnoreCase). Это решение Вы используете в методе ContactHydratorBase.GetPropertiesValuesWithoutLinq.

Вывод:


  1. Ваше утверждение


    По производительности в том числе они почти эквивалентны.

    некорректно.


  2. Для тестирования реальной скорости выполнения методов, с Linq и без, в Ваших тестах, надо использовать одинаковые функции/операторы сравнения строк в методах ContactHydratorBase.GetPropertiesValues и ContactHydratorBase.GetPropertiesValuesWithoutLinq.


В Вашей статье написано


Методы, победоносно носящие префикс Fast, почти при всех проходах оказываются медленнее, чем методы с префиксом Slow.

LINQ сожрет производительность сильней.

IMHO, процитированные целое и часть предложения некорректны, в свете вышеуказанных результатов тестов с измененным кодом. И код был изменен, в первом случае, чтобы использовать словарь, как и было Вами задумано, а во втором случае, чтобы заставить тест показать разницу скорости работы кода с Linq и без него, а не сравнивать производительность x.EntityName.ToUpperInvariant() == _typeName.ToUpperInvariant() и x.EntityName.Equals(_typeName, StringComparison.InvariantCultureIgnoreCase).

Если использовать словарь, то мы не LINQ сравниваем с его отсуствием и не компилированный делегат с динамической рефлексией, а оптимально написанный метод и не оптимально написанный метод. Такое сравнение глупо, потому что результат предопределен. Доступ к элементу через хештейбл словаря намного быстрей, чем доступ через итератор по порядку, особенно при больших последовательностях. Вы сейчас просто начали переписывать код и сообщать, что он работает быстрей.
Он и будет.
Вопрос был в другом — LINQ если писать его безопасно, не рискуя наткнуться на гиперитерации или извлечение всей таблицы в память, зашумляет результат так, что разницы просто не заметно.

Хм. Посмотрел результаты Вашего ретеста и там Fast-тесты выполняются быстрее Slow-тестов при 10, 100 и 1000 итераций. Это без каких либо изменений моих изменений в вашем коде.


Т.е. в любом случае Ваш первоначальный вывод, что


Методы, победоносно носящие префикс Fast, почти при всех проходах оказываются медленнее, чем методы с префиксом Slow.

не корректен.

Верно. Результат закономерный. Выше в комментариях и в самом низу статьи есть объяснение, где дьявол порылся.
1. Инициализацию нужно проводить в [GlobalSetup], ваши моки и тому подобное!
1.a Нету смысла в цикле
for (int i = 0; i < N; i++)
, этим занимается Benchmarkdotnet!
2.b Немогу скзать точно, но думаю асинк-машину стоит убрать для бенчмаркинга єтой задачи!
2. в FastContactHydrator используете ConcurrentDictionary, в SlowContactHydrator массив и потом примитивный проход (так не честно :), ConcurrencyDictionary нелинейно пробегает по связаному списку, смотрит в разные массивы, Для массива наоборот линейный пробег, плюс енумератор структурка. Используются оптимизации. Вообще смысла нету в Вашем случае в Collections.Concurrent!
3. Если создаете словарь, используйте его по назанчению (DefaultRawStringParser), Хотя как по мне лучше подготовить словарь для ContactHydratorBase._mapSchemas
1. Это не новость. Я уже не помню, почему было сделано иначе, но такой вариант тоже был.
1а. Не правда, этим N бенчмарк и управляет. Такой шаблон прямо предложен Акиньшиным в одной из статей по бенчмарку. Этот N позволяет явно задать количество повторяемых операций.
2б. Не нужно, это эмуляция реального кода. В реальном коде были бы именно асинхронные обращения к хранилищу. Со всеми его лагами и мусором.
2. В обоих случаях используется примитивный итератор без условий досрочного выхода. Какие оптимизации? Вы считаете, что IEnumerator, который вернет массив, даст прирост или что? Или потеряет на боксинге в интерфейс? Но насчет того, что надо было использовать один алгоритм — я согласен, Dictionary — артефакт. Смысла в конкурентных коллекциях в текущей реализации нет.
3. Мне кажется, это уже все обсуждалось выше. Во всяком случае, я признаю, что мог многое упустить. Все же более недели назад все было проделано уже.
Enumerator, который вернет массив, даст прирост или что

Если вы используете foreach и массив, энумератора вообще не будет. Будет счетчик.


https://stackoverflow.com/questions/11179156/how-is-foreach-implemented-in-c


https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgJgIwFgBQyAMABMugCwDcOyAzEagQMIEDeOBbRNyJBAsgBQBKZq3aiA9GIIA3AIYAnAjIIBeAgDsApgHcCAGTABnYAB5ieAHxMARDKtQCVgEZWAvhWyjRshUtWadANoAuta29k6uZJ7sItGxngBmAPZyGjIAxgAWfN4EYHlqigLxoiwe0Z7EAJx8YALuFQQu8c3YLkA===

Кстати, да. Я этого не знал. На stackoverflow указано, что только для массива, длина которого известна на этапе компиляции, но это, по всей видимости, не так. Для динамический формируемых массивов — тоже.
Сделал генератор на подобии Вашего наитивного:
.Lambda #Lambda1<System.Action`2[FastReslectionForHabrahabr.Hydrators.Contact,FastReslectionForHabrahabr.Hydrators.FastContactHydrator+ReadOnlyListWrapper]>(
    FastReslectionForHabrahabr.Hydrators.Contact $contact,
    FastReslectionForHabrahabr.Hydrators.FastContactHydrator+ReadOnlyListWrapper $correlations) {
    .Block(
        FastReslectionForHabrahabr.Services.PropertyNameEqualityComparerHolder+ReferenceEqualityComparer $comparer,
        System.Int32 $i) {
        $comparer = (FastReslectionForHabrahabr.Services.PropertyNameEqualityComparerHolder+ReferenceEqualityComparer)FastReslectionForHabrahabr.Services.PropertyNameEqualityComparerHolder.Instance;
        $i = 0;
        .Loop  {
            .Block(
                FastReslectionForHabrahabr.Models.PropertyToValueCorrelation $item,
                System.String $propName,
                System.Int32 $hashcode) {
                .If ($i >= $correlations.Count) {
                    .Break #Label1 { }
                } .Else {
                    .Default(System.Void)
                };
                $item = $correlations.Item[$i];
                $propName = $item.PropertyName;
                $hashcode = .Call $comparer.GetHashCode($propName);
                .Switch ($hashcode) {
                .Case (45988614):
                        .If (
                            .Call $comparer.Equals(
                                "FullName",
                                $propName)
                        ) {
                            $contact.FullName = $item.Value
                        } .Else {
                            .Default(System.Void)
                        }
                .Case (11244347):
                        .If (
                            .Call $comparer.Equals(
                                "Phone",
                                $propName)
                        ) {
                            $contact.Phone = $item.Value
                        } .Else {
                            .Default(System.Void)
                        }
                .Case (34090260):
                        .If (
                            .Call $comparer.Equals(
                                "Age",
                                $propName)
                        ) {
                            $contact.Age = $item.Value
                        } .Else {
                            .Default(System.Void)
                        }
                };
                ++$i
            }
        }
        .LabelTarget #Label1:
    }
}
``` ini

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18362.720 (1903/May2019Update/19H1)
Intel Core i5-6440HQ CPU 2.60GHz (Skylake), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=2.2.110
  [Host]     : .NET Core 2.2.8 (CoreCLR 4.6.28207.03, CoreFX 4.6.28208.02), X64 RyuJIT
  DefaultJob : .NET Core 2.2.8 (CoreCLR 4.6.28207.03, CoreFX 4.6.28208.02), X64 RyuJIT


```
|              Method |     Mean |    Error |   StdDev | Ratio |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------------------- |---------:|---------:|---------:|------:|-------:|------:|------:|----------:|
|   FastHydrationLinq | 22.54 μs | 0.171 μs | 0.160 μs |  1.14 | 3.8452 |     - |     - |  11.91 KB |
|       FastHydration | 19.81 μs | 0.084 μs | 0.066 μs |  1.00 | 3.0518 |     - |     - |   9.47 KB |
|   SlowHydrationLinq | 29.76 μs | 0.156 μs | 0.146 μs |  1.51 | 4.4556 |     - |     - |  13.72 KB |
|       SlowHydration | 26.34 μs | 0.202 μs | 0.189 μs |  1.33 | 3.6621 |     - |     - |  11.28 KB |
| ManualHydrationLinq | 22.75 μs | 0.114 μs | 0.106 μs |  1.15 | 3.9673 |     - |     - |  12.22 KB |
|     ManualHydration | 19.75 μs | 0.056 μs | 0.052 μs |  1.00 | 3.1738 |     - |     - |   9.78 KB |

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

Пропертя DebugView от Expression, суто для дебага того что получилось

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории