Комментарии 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).
Про пример с упорядоченной коллекцией на вскидку добавить нечего. Если каждый вызов к данному методу метод просто сопоставляется с типами и извлекается из кеша, работать будет настолько быстро, насколько оптимальным в итоге оказалась скомпилированная функция. Если постоянно будут идти обращения к кастомным атрибутам, обход дерева, компиляция — тут интуитивно кажется, что утечки производительности будут. Но надо замерять, чтобы о чем-то говорить предметно.
Собственно исходник
За основу взят ваш код, только Autofac заменен родным Core DI, а вместо моков используется InMemoryDatabase.
Приведенная реализация не позволит добиться тех же результатов, что и рассматриваемая в статье — конечный тип не будет типом Contract. Из очевидного — его нельзя будет передать в типобезопасные методы. Так же нельзя будет сохранить его в БД с помощью DB контекста как другую валидную энтити. Ближайшее, что можно сделать — сунуть его в автомаппер, с надеждой что тот перемаппит этот динамический объект в нужную нам энтити, но по сути автомаппер сделает то же, что предлагаю делать я. Соответственно, замерять производительность нет смысла.
А к тому что подобные задачи можно решать эффективнее, за счет использования динамики. Несмотря на свою неспешность, DLR все еще производительнее рефлексии.
По сути, в момент вызова свойства там же идет не вызов геттера, а некий перехват, который надо обработать.
Не может такой доступ к члена быть быстрей или хотя бы таким же по скорости как просто вызов экземплярного метода.
Так что в лучшем случае мы тут ускоряемся на гидрации экземпляра и теряем при каждом чтении свойств.
Типобезопасность, да и в принципе сложность исполнения я бы сказал тут в паритете… Так что не является ли использование DynamicObject «кашей из топора»?
Т.е. первый вызов TryGetMember для каждого из параметров будет очень медленным, но потом будет использоваться уже закэшированная версия. По сути, тот же финт ушами, что и с заранее скомпилированной лямбдой для рефлексии, только автоматически и без рефлексии.
Но главная фишка не в этом. А в более гибком подходе, например, мы можем загрузить все исходные данные в список, потом сделать выборку и отрефлексить только нужные данные.
Этот подход не лучше и не хуже, описанного в статье, но именно на данном классе задач, динамика дает довольно большое преимущество перед статикой опирающейся на отражение.
Может все же название такое должно быть: "Статья о неудачном ускорении рефлексии"?
Когда до «CLR расчехляет ГЦ и понеслись фризы» дошёл, пытался понять, а не о сборщике ли мусора говорит автор. Потом дошло, что ГЦ — это транслитерированный gc.
И так далее. Приходится отрываться, додумывать, что же хотел сказать автор, искать что-то в Сети, иногда сообщать об опечатках… И потом, возвращаясь к статье, долго и мучительно искать то место, где закончил читать — абзацы-то друг от друга не отделены…
Извините, не смог себя заставить дочитать. Попозже сделаю ещё один «подход к снаряду»…
Насчет абзацев — да. Я отверстаю. Насчет них думал. Давно не хабре не размещал статьи, не был уверен в том, как будет выглядеть финальная верстка. На веб вью в ВК читать вообще трудно. Обратил внимание утром.
Я хоть и стараюсь не использовать жаргонизмы, особенно в письменной речи, но их наличие в статье не ухудшило читабельность. Видимо они уже давно вошли в мой обиход.
О какой скорости может идти речь если у вас там строковые сравнения имён свойств повсюду? Если уж кешировать, так кешируйте
Dictionary<Type / source type /, Dictionary< Type / destination type /, Func<object, object> / transform / >
Только кешируйте так чтоб transform ничего не знал уже про имена свойств и прочую рефлексию. Впрочем, проще взять автомаппер, я не проверял, но он скорей всего так и делает.
В конечном делегате вызова рефлексии и нет.
Автомаппер делает именно так, но он не смаппит Вам словарь в объект без настройки под конкретный DestinationType методом — трансформеров. Не помню, как они у него называются. резолверы вроде.
Ну как бы из базы надо читать в DTO и уже из него мапить. Чтоб сложить в DTO строки не нужны, потому что SqlDataReader умеет читать по номеру запрошенной колонки (вы ведь знаете какие вы колонки запросили).
А если так делать лень и всё-таки хочется строки — то надо Dictionary для имён использовать а не обходить все подряд через FirstOrDefault.
Второй пункт — жертва паттерну, а с Вашей точкой зрения я согласен. Там сначала и был словарь, который я убрал в конечной реализации и заменил на корреляционные пары. Dictionary<string, string> в шаблонном методе делает достаточно трудным восприятие контекста, в котором надо переопределять метод в наследнике. Если бы Вы не обратили внимание на утечку производительности тут, двое других читателей указали бы на некрасиво реализованный паттерн. Мне показалось второе более постыдным. Только поэтому я сделал так. Я считаю, что по Вашей рекомендации и надо было бы делать в энтерпрайзе, так как доступ по хэшу в словаре действительно быстрый, он был бы даже быстрее, чем поиск по хэшу типа через LINQ, как Вы предлагали изначально, так как не было бы итерирования (а с ним и мусора).
Я правильно понял что вы по сути делаете метод типа
_sqlConnection.ReadVector(«select field1, field2 from table») // returns MyEntity[]Пара уточняющих вопросов
- а почему не использовать EntityFramework/NHibernate/другой ORM который такое делает сам?
- сколько у вас записей — вам точно нужна оптимизация? Если вы вытягиваете «тысячи/миллионы» записей, возможно вам нужен другой механизм типа user-defined sql types для ms sql (позволяет передавать в базу массивы объектов, на запись в базу точно быстрее, насчёт чтения не уверен)
Странные у вас результаты получились. Написал самый простой “копирователь свойств” и как не крути, но рефлексия в 60 раз медленнее. Вот тут исходники: https://gist.github.com/0x1000000/5ee4d6d2fdf426f64b60a2cbd3263a4a
Могу только предположить, что 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?
Впрочем, я проверю.
Статью придется переписать еще раз. Большое спасибо за помощь.
В общем чуда не случилось. Выкинул из вашего кода всякую ересь типа Моков (автоматом ушел тот баг с 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, но это не значит, что вам тоже его надо использовать.
У вас же 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 вообще трудно заметить что-то остальное.
Добавил в код автора откорректироанный класс 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 |
Ещё одно узкое место в реализации автором метода ContactHydratorBase.GetPropertiesValues
— использование x.EntityName.ToUpperInvariant() == _typeName.ToUpperInvariant()
вместо x.EntityName.Equals(_typeName, StringComparison.InvariantCultureIgnoreCase)
. Так же был удален ну нужный вызов .TaArray()
в конце.
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 |
Кроме того, ToArray() — это важный вызов. Фактически, в этот момент вызывается LINQ-овский Deffered Execution. Если его не делать, LINQ возвратит IEnumerable, который в себе сохранил ноды выражений, встроенные в одно дерево и если вынешний код этого не учитывает, а работает с коллекцией как с коллекцией в памяти, можно словить лишнее итерирование, и оно вообще производительность убьет. Поэтому ToList() и ToArray() вызываются не редко в конце цепочки, это факт.
В остальном, не было цели оптимизировать код, была цель сравнить влияние двух подходов к отражению на фоне иной логики. После обнаружения бага не весь код был переписан с выпиливанием артефактов, за это прошу меня извинить.
Первое — сравнение строк в верхнем регистре — это по сути и есть эквалс, но!
По сути да, но не по производительности. В тегах указана "высокая производительность".
Вам следует на будущее учитывать, что EF не понимает Equals, и извлечение из бд нужно делать по оператору ==. Если будете ставить Equals, EF не сможет транслировать код в SQL.
IMHO, такие вещи должны решаться, во время добавления данных в БД, т.е. данные для сравнения без учета регистра, должны быть сохранены в БД или в верхнем или нижнем регистрах, если опять же речь про высокую производительность.
Если говорить про БД, то есть высокая вероятность, что выборка всей таблицы ContactMapSchemas
в методе GetPropertiesValuesWithoutLinq
сведет к минимуму преимущества в производительности.
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, а в бизнес-логике.
В остальном используйте .Equals всегда — он короче и понятней.
В остальном магии нет, разницы не может быть, так как сравнение с игнором кейса — это сравнение к приведением к какому-то кейсу. Если Вы не вызываете ToUpper() напрямую, это не значит, что он не вызывается под капотом.
Как Вы сравните строки с игнором регистра без приведения к одному регистру? Магии никакой выполняться не будет: кодам символов надо сопоставить другие коды символов в символьной таблице. Потом сравнить. Все.
Вот результат бенча, в котором берется коллекция примерно из 60 итемов и фильтруется через Equals и == по одному свойству. Потом приводится к массиву. ToLower показывает странно большую скорость на .netCore. но не ясно почему. Рекомендации еще с Рихтера были сравнивать всегда в верхнем кейсе. Надо разбираться.
Что мы видим? Мусора при == .ToUpper() больше, но скорость +- одинаковая. ToLower выглядит идеально, но это странно. Возможно, изменилась логика работы BCL с момента, когда я этим интересовался. Но сказанного в первых абзацев это все равно не отменяет.
|------------------- |---------:|----------:|----------:|-------:|------:|------:|----------:|
| 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.
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 |
Резюме:
- Tест
CountUpperBoth
самый медленный. В этом тесте используется сравнениеitem.ToUpperInvariant() == _strToFind.ToUpperInvariant()
, точно такое же что у вас в методеContactHydratorBase.GetPropertiesValues
. - Tест
CountUpperSingle
быстрее предыдущего. В этом тесте используется сравнениеitem.ToUpperInvariant() == _strToFindUpper
, где_strToFindUpper
референсная строка заранее преобразованная в верхний регистр. Это решение я предлагал в коментарии выше. - Tест
CountEqualIgnoreCase
самый быстрый тест для длинных строк. В этом тесте используется сравнениеitem.Equals(_strToFind, StringComparison.InvariantCultureIgnoreCase)
. Это решение Вы используете в методеContactHydratorBase.GetPropertiesValuesWithoutLinq
.
Вывод:
Ваше утверждение
По производительности в том числе они почти эквивалентны.
некорректно.
Для тестирования реальной скорости выполнения методов, с Linq и без, в Ваших тестах, надо использовать одинаковые функции/операторы сравнения строк в методах
ContactHydratorBase.GetPropertiesValues
иContactHydratorBase.GetPropertiesValuesWithoutLinq
.
В Вашей статье написано
Методы, победоносно носящие префикс Fast, почти при всех проходах оказываются медленнее, чем методы с префиксом Slow.
LINQ сожрет производительность сильней.
IMHO, процитированные целое и часть предложения некорректны, в свете вышеуказанных результатов тестов с измененным кодом. И код был изменен, в первом случае, чтобы использовать словарь, как и было Вами задумано, а во втором случае, чтобы заставить тест показать разницу скорости работы кода с Linq и без него, а не сравнивать производительность x.EntityName.ToUpperInvariant() == _typeName.ToUpperInvariant()
и x.EntityName.Equals(_typeName, StringComparison.InvariantCultureIgnoreCase)
.
Он и будет.
Вопрос был в другом — LINQ если писать его безопасно, не рискуя наткнуться на гиперитерации или извлечение всей таблицы в память, зашумляет результат так, что разницы просто не заметно.
Хм. Посмотрел результаты Вашего ретеста и там Fast-тесты выполняются быстрее Slow-тестов при 10, 100 и 1000 итераций. Это без каких либо изменений моих изменений в вашем коде.
Т.е. в любом случае Ваш первоначальный вывод, что
Методы, победоносно носящие префикс Fast, почти при всех проходах оказываются медленнее, чем методы с префиксом Slow.
не корректен.
1.a Нету смысла в цикле
for (int i = 0; i < N; i++)
, этим занимается Benchmarkdotnet!2.b Немогу скзать точно, но думаю асинк-машину стоит убрать для бенчмаркинга єтой задачи!
2. в FastContactHydrator используете ConcurrentDictionary, в SlowContactHydrator массив и потом примитивный проход (так не честно :), ConcurrencyDictionary нелинейно пробегает по связаному списку, смотрит в разные массивы, Для массива наоборот линейный пробег, плюс енумератор структурка. Используются оптимизации. Вообще смысла нету в Вашем случае в Collections.Concurrent!
3. Если создаете словарь, используйте его по назанчению (DefaultRawStringParser), Хотя как по мне лучше подготовить словарь для ContactHydratorBase._mapSchemas
1а. Не правда, этим N бенчмарк и управляет. Такой шаблон прямо предложен Акиньшиным в одной из статей по бенчмарку. Этот N позволяет явно задать количество повторяемых операций.
2б. Не нужно, это эмуляция реального кода. В реальном коде были бы именно асинхронные обращения к хранилищу. Со всеми его лагами и мусором.
2. В обоих случаях используется примитивный итератор без условий досрочного выхода. Какие оптимизации? Вы считаете, что IEnumerator, который вернет массив, даст прирост или что? Или потеряет на боксинге в интерфейс? Но насчет того, что надо было использовать один алгоритм — я согласен, Dictionary — артефакт. Смысла в конкурентных коллекциях в текущей реализации нет.
3. Мне кажется, это уже все обсуждалось выше. Во всяком случае, я признаю, что мог многое упустить. Все же более недели назад все было проделано уже.
Enumerator, который вернет массив, даст прирост или что
Если вы используете foreach и массив, энумератора вообще не будет. Будет счетчик.
https://stackoverflow.com/questions/11179156/how-is-foreach-implemented-in-c
.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 |
Неудачная статья про ускорение рефлексии