Неуспешан чланак о убрзавању рефлексије

Одмах ћу објаснити наслов чланка. Првобитни план је био да дам добре, поуздане савете како да убрзам коришћење рефлексије користећи једноставан, али реалан пример, али се током бенцхмаркинга показало да рефлексија није спора као што сам мислио, ЛИНК је спорији него у мојим ноћним морама. Али на крају се испоставило да сам и ја погрешио у мерењима... Детаљи ове животне приче су испод реза и у коментарима. Пошто је пример сасвим уобичајен и у принципу имплементиран као што се обично ради у предузећу, испоставило се да је прилично занимљива, како ми се чини, демонстрација живота: утицај на брзину главне теме чланка био је није приметно због спољашње логике: Мок, Аутофац, ЕФ Цоре и други "бандингс".

Почео сам да радим под утиском овог чланка: Зашто је Рефлексија спора

Као што видите, аутор предлаже коришћење компајлираних делегата уместо директног позивања метода рефлексивног типа као одличан начин да се у великој мери убрза апликација. Постоји, наравно, емисија ИЛ, али бих желео да је избегнем, јер је ово најинтензивнији начин за обављање задатка, који је препун грешака.

С обзиром на то да сам одувек имао слично мишљење о брзини рефлексије, нисам посебно имао намеру да доводим у питање ауторове закључке.

Често се сусрећем са наивном употребом рефлексије у предузећу. Тип се узима. Подаци о имовини се узимају. Позива се метода СетВалуе и сви се радују. Вредност је стигла у циљно поље, сви су срећни. Веома паметни људи - сениори и вође тима - пишу своје екстензије за објекат, на основу тако наивне имплементације "универзалних" мапера једног типа у други. Суштина је обично следећа: узимамо сва поља, узимамо сва својства, прелазимо преко њих: ако се имена чланова типа поклапају, извршавамо СетВалуе. С времена на време хватамо изузетке због грешака где нисмо пронашли неку особину у неком од типова, али чак и овде постоји излаз који побољшава перформансе. Покушај да ухватиш.

Видео сам људе како поново измишљају парсере и мапере, а да нису били потпуно наоружани информацијама о томе како функционишу машине које су биле пре њих. Видео сам људе како скривају своје наивне имплементације иза стратегија, иза интерфејса, иза ињекција, као да би то оправдало каснију вакханалију. Вртио сам нос на такве спознаје. У ствари, нисам мерио стварно цурење перформанси, и, ако је могуће, једноставно сам променио имплементацију на „оптималнију“ ако бих могао да је добијем. Стога су ме прва мерења о којима се говори у наставку озбиљно збунила.

Мислим да су многи од вас, читајући Рихтера или друге идеологе, наишли на потпуно поштену изјаву да је рефлексија у коду феномен који има изузетно негативан утицај на перформансе апликације.

Позивање рефлексије присиљава ЦЛР да прође кроз склопове како би пронашао ону која им је потребна, извукао њихове метаподатке, рашчланио их итд. Поред тога, рефлексија при преласку низа доводи до алокације велике количине меморије. Користимо меморију, ЦЛР открива ГЦ и фризови почињу. Требало би да буде приметно споро, верујте ми. Огромне количине меморије на савременим производним серверима или рачунарима у облаку не спречавају велика кашњења у обради. У ствари, што је више меморије, већа је вероватноћа да ћете ПРИМЕТИТИ како ГЦ ради. Рефлексија је, у теорији, за њега екстра црвена крпа.

Међутим, сви користимо ИоЦ контејнере и мапере датума, чији је принцип рада такође заснован на рефлексији, али обично нема питања о њиховим перформансама. Не, не зато што су увођење зависности и апстракција од екстерних модела ограниченог контекста толико неопходни да морамо да жртвујемо перформансе у сваком случају. Све је једноставније - то заиста не утиче много на перформансе.

Чињеница је да најчешћи оквири који се заснивају на технологији рефлексије користе разне трикове да би са њом што боље радили. Обично је ово кеш меморија. Обично су то изрази и делегати компајлирани из стабла израза. Исти аутомапер одржава конкурентан речник који повезује типове са функцијама које могу да конвертују један у други без позивања рефлексије.

Како се то постиже? У суштини, ово се не разликује од логике коју сама платформа користи за генерисање ЈИТ кода. Када се метода позове први пут, она се компајлира (и, да, овај процес није брз); на наредним позивима, контрола се преноси на већ преведену методу и неће бити значајних смањења перформанси.

У нашем случају, такође можете користити ЈИТ компилацију, а затим користити компајлирано понашање са истим перформансама као и његове АОТ колеге. Изрази ће нам у овом случају доћи у помоћ.

Принцип о коме је реч може се укратко формулисати на следећи начин:
Требало би да кеширате коначни резултат рефлексије као делегат који садржи преведену функцију. Такође има смисла кеширати све потребне објекте са информацијама о типу у пољима вашег типа, радника, која се чувају изван објеката.

Има логике у овоме. Здрав разум нам говори да ако се нешто може компајлирати и кеширати, онда то треба и урадити.

Гледајући унапред, треба рећи да кеш у раду са рефлексијом има своје предности, чак и ако не користите предложени метод састављања израза. Заправо, овде једноставно понављам тезе аутора чланка на који се горе позивам.

Сада о коду. Погледајмо пример који је заснован на мом недавном болу са којим сам се морао суочити у озбиљној продукцији озбиљне кредитне институције. Сви ентитети су фиктивни да нико не би погодио.

Постоји нека суштина. Нека буде Контакт. Постоје слова са стандардизованим телом, од којих парсер и хидратор стварају те исте контакте. Стигло је писмо, прочитали смо га, рашчланили на парове кључ-вредност, направили контакт и сачували га у бази података.

То је елементарно. Рецимо да контакт има својства Пуно име, старост и телефон за контакт. Ови подаци се преносе у писму. Предузеће такође жели подршку да може брзо да дода нове кључеве за мапирање својстава ентитета у парове у телу писма. У случају да је неко направио грешку у шаблону или ако је пре објављивања потребно хитно покренути мапирање од новог партнера, прилагођавајући се новом формату. Затим можемо додати нову корелацију мапирања као јефтину поправку података. Односно, животни пример.

Ми имплементирамо, креирамо тестове. Извођење радова.

Нећу давати код: постоји много извора и они су доступни на ГитХубу преко везе на крају чланка. Можете их натоварити, мучити до непрепознатљивости и мерити, како би то у вашем случају утицало. Даћу само код две шаблонске методе које разликују хидратор, који је требало да буде брз, од хидрататора који је требало да буде спор.

Логика је следећа: метода шаблона прима парове које генерише основна логика парсера. ЛИНК слој је парсер и основна логика хидратора, који поставља захтев контексту базе података и упоређује кључеве са паровима из парсера (за ове функције постоји код без ЛИНК-а за поређење). Затим се парови прослеђују главном методу хидратације и вредности парова се постављају на одговарајућа својства ентитета.

„Брзо“ (префикс Брзо у тестовима):

 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();
        }

Генерално, јасно је. Пролазимо кроз својства, креирамо делегате за њих који позивају сетере и чувамо их. Онда зовемо када је потребно.

„Споро“ (префикс Споро у тестовима):

        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;
        }

Овде одмах заобилазимо својства и директно позивамо СетВалуе.

Ради јасноће и као референца, имплементирао сам наивну методу која уписује вредности њихових корелационих парова директно у поља ентитета. Префикс – Мануал.

Сада узмимо БенцхмаркДотНет и испитамо перформансе. И одједном... (спојлер - ово није тачан резултат, детаљи су испод)

Неуспешан чланак о убрзавању рефлексије

Шта видимо овде? Методе које тријумфално носе префикс Фаст испадају спорије у скоро свим пролазима од метода са префиксом Слов. Ово важи и за алокацију и за брзину рада. С друге стране, лепа и елегантна имплементација мапирања коришћењем ЛИНК метода намењених томе где год је то могуће, напротив, у великој мери смањује продуктивност. Разлика је у реду. Тренд се не мења са различитим бројем пасова. Једина разлика је у размерама. Са ЛИНК-ом је 4 - 200 пута спорије, има више смећа на приближно истој скали.

УПДАТЕД

Нисам веровао својим очима, али што је још важније, наш колега није веровао ни мојим очима ни мојој шифри - Дмитри Тикхонов 0к1000000. Након што је још једном проверио моје решење, он је сјајно открио и указао на грешку коју сам пропустио због низа промена у имплементацији, од почетне до коначне. Након поправљања пронађене грешке у Мок подешавању, сви резултати су дошли на своје место. Према резултатима поновног тестирања, главни тренд се не мења - ЛИНК и даље више утиче на перформансе него на рефлексију. Међутим, лепо је што се рад са компилацијом Екпрессиона не ради узалуд, а резултат је видљив и у додели и времену извршења. Прво покретање, када се иницијализују статичка поља, је природно спорије за „брзи“ метод, али се онда ситуација мења.

Ево резултата поновног тестирања:

Неуспешан чланак о убрзавању рефлексије

Закључак: када користите рефлексију у предузећу, нема посебне потребе да се прибегава триковима - ЛИНК ће више појести продуктивност. Међутим, у методама високог оптерећења које захтевају оптимизацију, можете сачувати рефлексију у облику иницијализатора и компајлера делегата, који ће онда обезбедити „брзу“ логику. На овај начин можете одржати и флексибилност рефлексије и брзину апликације.

Референтни код је доступан овде. Свако може још једном да провери моје речи:
ХабраРефлецтионТестс

ПС: код у тестовима користи ИоЦ, ау бенцхмарк-у користи експлицитну конструкцију. Чињеница је да сам у коначној имплементацији одсекао све факторе који би могли утицати на перформансе и учинити резултат бучним.

ППС: Хвала кориснику Дмитри Тикхонов @0к1000000 за откривање моје грешке у подешавању Мок-а, што је утицало на прва мерења. Ако неко од читалаца има довољно карме, лајкујте га. Човек је стао, човек је прочитао, човек је још једном проверио и указао на грешку. Мислим да је ово вредно поштовања и саосећања.

ПППС: хвала пажљивом читаоцу који је дошао до дна стила и дизајна. Ја сам за униформност и удобност. Дипломатичност излагања оставља много да се пожели, али сам критике узео у обзир. Тражим пројектил.

Извор: ввв.хабр.цом

Додај коментар