Неудачная статья про ускорение рефлексии

    Сразу объясню название статьи. Изначально планировалось дать хороший, надежный совет по ускорению использования рефлекшена на простом, но реалистичном примере, однако в ходе бенчмаркинга выяснилось, что рефлексия работает не так медленно, как я думал, LINQ работает медленнее, чем снилось в кошмарах. А в итоге оказалось, что мной еще и была допущена ошибка в замерах… Подробности этой жизненной истории под катом и в комментариях. Так как пример достаточно бытовой и реализованный в принципе как обычно делается в энтерпрайзе, получилось достаточно интересная, как мне кажется, демонстрация жизни: влияния на скорость работы основного предмета статьи было не заметно из-за внешней логики: Moq, Autofac, EF Core и прочей «обвязки».

    Начал я работу под впечатлением от этой статьи: Why is Reflection slow

    Как видно, автор предлагает использовать скомпилированные делегаты вместо прямого обращения к методам типов отражения как отличный способ сильно ускорить работу приложения. Есть там еще, конечно, IL эмиссия, но её хотелось бы избежать, так как это самый трудозатратный способ выполнения задачи, который чреват ошибками.

    Учитывая, что я придерживался всегда аналогичного мнения о скорости рефлексии, особо ставить под сомнения выводы автора я не собирался.

    Я не редко встречаюсь с наивным использованием рефлексии в энтерпрайзе. Берется тип. Берется информация о свойстве. Вызывается метод SetValue, и все радуются. Значение прилетело в целевое поле, все довольны. Люди весьма неглупые — синиоры и тимлиды — пишут свои расширения на object, основывая на такой наивной реализации «универсальные» мапперы одного типа в другой. Суть такова обычно: берем все поля, берем все свойства, итерируем по ним: при совпадении имен членов типа выполняем SetValue. Периодически ловим исключения на промахах там, где не нашли какое-то свойство у одного из типов, но и тут есть выход, добивающий производительность. Try/catch.

    Я видел, как люди переизобретают парсеры и мапперы, не будучи полностью вооруженными информацией о том, как работают изобретенные до них велосипеды. Я видел, как люди прячут свои наивные реализации за стратегиями, за интерфейсами, за инъекциями, как будто это извинит последующую вакханалию. От таких реализаций я воротил нос. По факту, реальную утечку производительности я не замерял, и при возможности просто менял реализацию на более «оптимальную», если руки доходили. Потому первые замеры, о которых идет речь ниже, меня серьёзно смутили.

    Думаю, многие из вас, читая Рихтера или других идеологов, сталкивались со вполне справедливым утверждением, что рефлексия в коде — это явление крайне негативно сказывающееся на перформансе приложения.

    Вызов отражения вынуждает CLR обходить сборки в поисках нужной, подтягивать их метаданные, парсить их и т.д. Кроме того, рефлексия во время обхода последовательностей приводит к аллокации большого объема памяти. Расходуем память, CLR расчехляет ГЦ и понеслись фризы. Это должно быть заметно медленно, поверьте. Огромные объемы памяти современных продакшен серверов или облачных машин не спасают от высоких задержек в обработке. Фактически, чем больше памяти, тем выше вероятность, что вы ЗАМЕТИТЕ, как работает ГЦ. Рефлексия – это, по идее, лишняя красная тряпка для него.

    Тем не менее, все мы используем и IoC контейнеры, и дата мапперы, принцип работы которых так же основан на рефлексии, однако вопросов к их производительности обычно не возникает. Нет, не потому что внедрение зависимостей и абстрагирования от моделей внешнего ограниченного контекста столь необходимые вещи, что перформансом нам приходится жертвовать в любом случае. Все проще – это действительно не сильно сказываются на производительности.

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

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

    В нашем случае так же можно воспользоваться JIT компиляцией и потом использовать скомпилированное поведение с той же производительностью, что и его AOT аналоги. На помощь нам в данном случае придут выражения.

    Кратко можно сформулировать принцип, о котором идет речь, следующим образом:
    Следует кешировать конечный результат работы рефлексии в виде делегата, содержащего скомпилированную функцию. Все необходимые объекты со сведениями о типах тоже имеет смысл кешировать в сохраняемых вне объектов полях вашего типа – воркера.

    Логика в этом есть. Здравый смысл нам говорит о том, что если что-то можно скомпилировать и закешировать, то это следует сделать.

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

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

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

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

    Реализуем, создаем тесты. Работает.

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

    Логика следующая: шаблонный метод получает пары, сформированные базовой логикой парсера. Уровень LINQ – это парсер и базовая логика гидратора, делающая запрос к контексту бд и сопоставляющая ключи с парами от парсера (для этих функций есть код без LINQ для сравнения). Далее пары передаются в основной метод гидрации и значения пар устанавливаются в соответствующие свойства энтити.

    «Быстрый» (Префикс Fast в бенчмарках):

     protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
            {
                var contact = new Contact();
                foreach (var setterMapItem in _proprtySettersMap)
                {
                    var correlation = correlations.FirstOrDefault(x => x.PropertyName == setterMapItem.Key);
                    setterMapItem.Value(contact, correlation?.Value);
                }
                return contact;
            }
    

    Как мы видим, используется статическая коллекция с сеттерами пропертей – скомпилированными лямбдами, вызывающими сеттер энтити. Создаются следующим кодом:

            static FastContactHydrator()
            {
                var type = typeof(Contact);
                foreach (var property in type.GetProperties())
                {
                    _proprtySettersMap[property.Name] = GetSetterAction(property);
                }
            }
    
            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();
            }
    

    В целом понятно. Обходим свойства, создаем по ним делегаты, вызывающие сеттеры, сохраняем. Потом вызываем, когда надо.

    «Медленный» (Префикс Slow в бенчмарках):

            protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
            {
                var contact = new Contact();
                foreach (var property in _properties)
                {
                    var correlation = correlations.FirstOrDefault(x => x.PropertyName == property.Name);
                    if (correlation?.Value == null)
                        continue;
    
                    property.SetValue(contact, correlation.Value);
                }
                return contact;
            }
    

    Тут сразу обходим свойства и вызываем напрямую SetValue.

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

    Теперь берем BenchmarkDotNet и исследуем производительность. И внезапно… (спойлер — это не правильный результат, подробности — ниже)



    Что мы тут видим? Методы, победоносно носящие префикс Fast, почти при всех проходах оказываются медленнее, чем методы с префиксом Slow. Это справедливо и для аллокации, и для скорости работы. С другой же стороны красивая и элегантная реализация маппинга с использованием везде, где можно, предназначенных для этого методов LINQ, наоборот, сильно отжирает производительность. Разница в порядки. Тенденция не меняется с разным количеством проходов. Разница только в масштабах. С LINQ в 4 — 200 раз медленнее, мусора больше примерно в таких же масштабах.

    UPDATED

    Я не поверил своим глазам, но что важнее, ни моим глазам, ни моему коду не поверил наш коллега — Dmitry Tikhonov 0x1000000. Перепроверив мой солюшн он блестяще обнаружил и указал на ошибку, которую я из-за ряда изменений в реализации упустил. После исправления найденного бага в настройке Moq, все результаты встали на свои места. По результатам ретеста основная тенденция не меняется — LINQ влияет на производительность все равно сильнее, чем рефлексия. Однако приятно, что работа с компиляцией Expression'ов делается не зря, и результат виден и по аллокации, и по времени выполнения. Первый запуск, когда инициализируются статические поля, закономерно медленней у «быстрого» метода, но дальше ситуация меняется.

    Вот результат ретеста:



    Вывод: при использовании в энтерпрайзе рефлексии прибегать к ухищрениям особо не требуется — LINQ сожрет производительность сильней. Тем не менее, в высоконагруженных методах, требующих оптимизации, можно сохранить рефлексию в виде инициализаторов и компиляторов делегатов, которые будут потом обеспечивать «быструю» логику. Так Вы можете сохранить и гибкость рефлексии, и скорость работы приложения.

    Код с бенчмарком доступен тут. Все желающие могут перепроверить мои слова:
    HabraReflectionTests

    PS: код в тестах использует IoC, а в бенчмарках – явную конструкцию. Дело в том, что в конечной реализации я отсек все способные сказаться на производительности и зашумить результат факторы.

    PPS: Спасибо пользователю Dmitry Tikhonov @0x1000000 за обнаружение моей ошибки в настройке Moq, которая сказалась на первых замерах. Если у кого-то из читателей будет достаточная карма, лайкните его, пожалуйста. Человек остановился, человек вчитался, человек перепроверил и указал на ошибку. Я считаю, что это достойно уважения и симпатии.

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 76

      +1

      Вопрос: может ли проверка на 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> и вызывается где нужно. За скорость не скажу, но вряд ли это будет медленне вызова геттеров/сеттеров через рефлекшн.

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

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

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

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

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

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

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

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

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

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

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

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

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

                        Этот подход не лучше и не хуже, описанного в статье, но именно на данном классе задач, динамика дает довольно большое преимущество перед статикой опирающейся на отражение.
                          0
                          Буду иметь в виду Вашу точку зрения. Может быть однажды докажу ее или опровергну.
          +2

          Может все же название такое должно быть: "Статья о неудачном ускорении рефлексии"?

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

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

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

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

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

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

                +3

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


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

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

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

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

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

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


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

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

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

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


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

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


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

                  Странные у вас результаты получились. Написал самый простой “копирователь свойств” и как не крути, но рефлексия в 60 раз медленнее. Вот тут исходники: https://gist.github.com/0x1000000/5ee4d6d2fdf426f64b60a2cbd3263a4a

                    0
                    Можете загрузить мои исходники и проверить самостоятельно. Найдете ошибку — все вместе будем знать.
                    Могу только предположить, что PropertyInfo.SetMethod.Invoke работает медленней, чем SetValue или кешируется иначе самой CLR.
                    А в целом, я тоже считаю свой результат странным. Но для меня было важно именно SetValue проверить против скомпилированного делегата с обращением напрямую к сеттеру.
                      +1

                      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);
                          }
                      }
                        0
                        Надо разбирать тогда пошагово. Возможно та разница, которая видна у Вас, в моем случае просто зашумляется остальной работой.
                        +1

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


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

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

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

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

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

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

                              В общем чуда не случилось. Выкинул из вашего кода всякую ересь типа Моков (автоматом ушел тот баг с 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

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

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


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


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

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

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

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

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

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

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


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

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


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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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


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


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

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


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


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


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


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


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

                                              зато:


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

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

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

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

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

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

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

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

                                  Я правильно понимаю, что замена метода 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;
                                  }
                                    0
                                    Да, без LINQ работать будет быстрей гораздо + меньше мусора.
                                    Но это внесет шум в замеры. Там так вызодит, что на фоне LINQ вообще трудно заметить что-то остальное.
                                      0

                                      Добавил в код автора откорректироанный класс 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 |
                                        0
                                        Тут выигрыш за счет того, что Вы оптимизируете доступ к элементам, а не гидрацию, что было примером статьи. Фактически, LINQ сильно зашумляет результат. За ним иной разницы мало заметно. Выше с пользователем Dmitry Tikhonov 0x1000000 мы установили реальную причину таких результатов.
                                          0
                                          И статья опять потеряла смысл. Вернее, при таких бенчах она должна быть о другом.
                                            0
                                            Ну почему? Я переписал её, восстановив часть исходного смысла. Так выводы сделать можно. Я думаю. Во-первых, LINQ зашумляет до полной незаметности влияние рефлексии, а LINQ там относительно не много. Во-вторых, даже не считая IO, на фоне остальной работы рефлексия вредит не так сильно, и в-третьих, пример оптимизации все равно наглядно демонстрируется и реализует свою задачу. Понимаю, что целостное восприятие три раза переписанного не самая простая задача, но такова наша жизнь — диалектическое воспритие меняющейся системы в нескольких её состояниях. #мыжпрограммисты.
                                              0
                                              Блин, проблема рефлексии как бе не в скорости а в ошибка в рантайме. Которые потом мучится и чинить такие баги.
                                                0
                                                А проблема Expression'ов еще и в усложненном дебаге. Статья рассматривает конкретный аспект и не навязывает использование рефлексии. Но если её использовать. Было бы неплохо идти до конца и компилировать результат.
                                0

                                Ещё одно узкое место в реализации автором метода 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 |
                                  0
                                  Первое — сравнение строк в верхнем регистре — это по сути и есть эквалс, но! Вам следует на будущее учитывать, что EF не понимает Equals, и извлечение из бд нужно делать по оператору ==. Если будете ставить Equals, EF не сможет транслировать код в SQL. Посмотрите по истории коммитов, там сперва был Мок DB Context'a EF Core. То, что Вы видите — суть артефакт от него. Как и ToArray(). В исходной реализации тут вызывался ToArrayAsync().

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

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

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


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

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


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

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

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


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

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


                                        Кажется я нашел проблему в Вашем коде, которая делает 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, а в бизнес-логике.

                                          0
                                          Смотрите: 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 |
                                            0

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


                                            В коде 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.

                                              +1
                                              Написал тест для измерения сравнивания строк через == .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.


                                        0

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


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

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

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

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

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


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


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

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

                                              0
                                              Верно. Результат закономерный. Выше в комментариях и в самом низу статьи есть объяснение, где дьявол порылся.
                                      +1
                                      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
                                        0
                                        1. Это не новость. Я уже не помню, почему было сделано иначе, но такой вариант тоже был.
                                        1а. Не правда, этим N бенчмарк и управляет. Такой шаблон прямо предложен Акиньшиным в одной из статей по бенчмарку. Этот N позволяет явно задать количество повторяемых операций.
                                        2б. Не нужно, это эмуляция реального кода. В реальном коде были бы именно асинхронные обращения к хранилищу. Со всеми его лагами и мусором.
                                        2. В обоих случаях используется примитивный итератор без условий досрочного выхода. Какие оптимизации? Вы считаете, что IEnumerator, который вернет массив, даст прирост или что? Или потеряет на боксинге в интерфейс? Но насчет того, что надо было использовать один алгоритм — я согласен, Dictionary — артефакт. Смысла в конкурентных коллекциях в текущей реализации нет.
                                        3. Мне кажется, это уже все обсуждалось выше. Во всяком случае, я признаю, что мог многое упустить. Все же более недели назад все было проделано уже.
                                          +1
                                          Enumerator, который вернет массив, даст прирост или что

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


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


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

                                            0
                                            Кстати, да. Я этого не знал. На stackoverflow указано, что только для массива, длина которого известна на этапе компиляции, но это, по всей видимости, не так. Для динамический формируемых массивов — тоже.
                                              0
                                              Сделал генератор на подобии Вашего наитивного:
                                              .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 |
                                              
                                              
                                                0
                                                А подскажите плиз, что это за зверь такой у Вас? В смысле где почитать про то, что у Вас в коде написано.
                                                  0

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

                                      Only users with full accounts can post comments. Log in, please.