Yansımayı hızlandırmayla ilgili başarısız makale

Hemen yazının başlığını açıklayacağım. Orijinal plan, basit ama gerçekçi bir örnek kullanarak yansıma kullanımının nasıl hızlandırılacağına dair iyi ve güvenilir tavsiyeler vermekti, ancak kıyaslama sırasında yansımanın düşündüğüm kadar yavaş olmadığı, LINQ'un kabuslarımda olduğundan daha yavaş olduğu ortaya çıktı. Ama sonunda ölçülerde de hata yaptığım ortaya çıktı... Bu hayat hikayesinin detayları kesim altında ve yorumlarda. Örnek oldukça sıradan olduğundan ve genellikle bir işletmede yapıldığı gibi prensipte uygulandığından, bana öyle geliyor ki oldukça ilginç bir yaşam gösterisi olduğu ortaya çıktı: makalenin ana konusunun hızı üzerindeki etkisi harici mantık nedeniyle farkedilemez: Moq, Autofac, EF Core ve diğer "bantlaşmalar".

Bu makalenin izlenimiyle çalışmaya başladım: Yansıma neden yavaş?

Gördüğünüz gibi yazar, uygulamayı büyük ölçüde hızlandırmanın harika bir yolu olarak yansıma türü yöntemleri doğrudan çağırmak yerine derlenmiş delegelerin kullanılmasını öneriyor. Elbette IL emisyonu var, ancak bundan kaçınmak istiyorum çünkü bu, görevi gerçekleştirmenin en emek yoğun ve hatalarla dolu yolu.

Düşünme hızı konusunda her zaman benzer bir düşünceye sahip olduğumu göz önünde bulundurarak, özellikle yazarın vardığı sonuçları sorgulamak niyetinde değildim.

İşletmelerde yansımanın naif kullanımıyla sık sık karşılaşıyorum. Tip alınır. Gayrimenkule ait bilgiler alınır. SetValue yöntemi çağrılır ve herkes sevinir. Hedef alana değer geldi, herkes mutlu. Çok akıllı insanlar - kıdemliler ve ekip liderleri - bir türden diğerine "evrensel" eşleyicilerin bu kadar saf bir uygulamasına dayanarak, itiraz etmek için uzantılarını yazarlar. İşin özü genellikle şudur: tüm alanları alırız, tüm özellikleri alırız, üzerlerinde yineleniriz: tür üyelerinin adları eşleşirse SetValue'yu çalıştırırız. Zaman zaman türlerden birinde bir özellik bulamadığımız hatalardan dolayı istisnalar yakalıyoruz, ancak burada bile performansı artıran bir çıkış yolu var. Deneyin/yakalayın.

İnsanların, kendilerinden önce gelen makinelerin nasıl çalıştığı hakkında tam olarak bilgi sahibi olmadan ayrıştırıcıları ve haritalayıcıları yeniden icat ettiğini gördüm. İnsanların naif uygulamalarını stratejilerin, arayüzlerin, enjeksiyonların arkasına sakladıklarını gördüm, sanki bu daha sonraki bakşaleyi mazur gösterecekmiş gibi. Bu tür farkındalıklara burun kıvırdım. Aslında, gerçek performans sızıntısını ölçmedim ve eğer mümkünse, uygulamayı elime geçirebilirsem daha "optimal" bir uygulamayla değiştirdim. Bu nedenle aşağıda tartışılan ilk ölçümler kafamı ciddi şekilde karıştırdı.

Sanırım birçoğunuz, Richter'i veya diğer ideologları okuyarak, koddaki yansımanın, uygulamanın performansı üzerinde son derece olumsuz etkisi olan bir olgu olduğuna dair tamamen adil bir ifadeyle karşılaşmışsınızdır.

Yansımayı çağırmak, CLR'yi ihtiyaç duydukları birini bulmak, meta verilerini almak, ayrıştırmak vb. için derlemelerden geçmeye zorlar. Ek olarak, diziler arasında geçiş yaparken yapılan yansıma, büyük miktarda belleğin tahsis edilmesine yol açar. Belleği kullanıyoruz, CLR GC'yi ortaya çıkarıyor ve frizler başlıyor. İnanın bana gözle görülür derecede yavaş olmalı. Modern üretim sunucularındaki veya bulut makinelerindeki devasa miktardaki bellek, yüksek işlem gecikmelerini engellemez. Aslında, bellek ne kadar fazla olursa, GC'nin nasıl çalıştığını fark etme olasılığınız da o kadar artar. Düşünmek onun için teorik olarak ekstra bir kırmızı bezdir.

Ancak hepimiz, çalışma prensibi de yansımaya dayanan IoC kapsayıcıları ve tarih eşleyicileri kullanırız, ancak bunların performansı hakkında genellikle hiçbir soru sorulmaz. Hayır, bağımlılıkların getirilmesi ve dış sınırlı bağlam modellerinden soyutlamanın her halükarda performansı feda etmemizi gerektirecek kadar gerekli olması nedeniyle değil. Her şey daha basit; performansı pek etkilemiyor.

Gerçek şu ki, yansıma teknolojisine dayanan en yaygın çerçeveler, onunla daha iyi çalışmak için her türlü hileyi kullanıyor. Genellikle bu bir önbellektir. Bunlar genellikle ifade ağacından derlenen İfadeler ve temsilcilerdir. Aynı otomatik haritalayıcı, yansıma çağırmadan türleri birbirine dönüştürebilen işlevlerle eşleştiren rekabetçi bir sözlüğe sahiptir.

Bu nasıl başarılıyor? Esas itibarıyla bu, platformun kendisinin JIT kodu oluşturmak için kullandığı mantıktan farklı değildir. Bir yöntem ilk kez çağrıldığında derlenir (ve evet, bu süreç hızlı değildir); sonraki çağrılarda kontrol önceden derlenmiş yönteme aktarılır ve önemli bir performans düşüşü yaşanmaz.

Bizim durumumuzda, JIT derlemesini de kullanabilir ve daha sonra derlenmiş davranışı AOT benzerleriyle aynı performansla kullanabilirsiniz. Bu durumda ifadeler imdadımıza yetişecektir.

Söz konusu ilke kısaca şu şekilde formüle edilebilir:
Yansımanın nihai sonucunu, derlenmiş işlevi içeren bir temsilci olarak önbelleğe almalısınız. Ayrıca, nesnelerin dışında depolanan, türünüzün (işçi) alanlarındaki tür bilgileriyle birlikte gerekli tüm nesneleri önbelleğe almak da mantıklıdır.

Bunda bir mantık var. Sağduyu bize, eğer bir şey derlenip önbelleğe alınabiliyorsa, o şeyin yapılması gerektiğini söyler.

İleriye baktığımızda, önerilen ifade derleme yöntemini kullanmasanız bile, yansımayla çalışmadaki önbelleğin avantajları olduğu söylenmelidir. Aslında burada sadece yukarıda atıfta bulunduğum makalenin yazarının tezlerini tekrarlıyorum.

Şimdi kod hakkında. Son dönemde ciddi bir kredi kuruluşunun ciddi bir prodüksiyonunda yaşamak zorunda kaldığım acıdan yola çıkarak bir örneğe bakalım. Tüm varlıklar, kimsenin tahmin edemeyeceği şekilde hayal ürünüdür.

Bir miktar öz var. İletişim olsun. Ayrıştırıcı ve nemlendiricinin aynı temas noktalarını oluşturduğu standart gövdeli harfler vardır. Bir mektup geldi, onu okuduk, anahtar-değer çiftlerine ayırdık, bir kişi oluşturduk ve veritabanına kaydettik.

Bu basit bir şey. Bir kişinin Tam Adı, Yaşı ve Kişi Telefonu özelliklerine sahip olduğunu varsayalım. Bu veriler mektupta iletilir. İşletme ayrıca, varlık özelliklerini mektubun gövdesindeki çiftler halinde eşlemek için yeni anahtarları hızlı bir şekilde ekleyebilmek için destek istiyor. Birisinin şablonda bir yazım hatası yapması durumunda veya yayınlanmadan önce, haritalamayı yeni bir ortaktan acilen başlatmanız ve yeni formata uyum sağlamanız gerekiyorsa. Daha sonra ucuz bir veri düzeltmesi olarak yeni bir eşleme korelasyonu ekleyebiliriz. Yani bir yaşam örneği.

Testleri uyguluyor, oluşturuyoruz. İşler.

Kodu vermeyeceğim: Çok sayıda kaynak var ve bunlara makalenin sonundaki bağlantı aracılığıyla GitHub'da ulaşılabilir. Sizin durumunuzu etkileyeceği için onları yükleyebilir, tanınmayacak kadar işkence edebilir ve ölçebilirsiniz. Hızlı olması gereken hidratörü, yavaş olması gereken hidratörden ayıran sadece iki şablon yönteminin kodunu vereceğim.

Mantık şu şekildedir: şablon yöntemi, temel ayrıştırıcı mantığı tarafından oluşturulan çiftleri alır. LINQ katmanı, veritabanı bağlamına bir istekte bulunan ve anahtarları ayrıştırıcıdaki çiftlerle karşılaştıran nemlendiricinin ayrıştırıcısı ve temel mantığıdır (bu işlevler için karşılaştırma için LINQ'suz kod vardır). Daha sonra çiftler ana hidrasyon yöntemine geçirilir ve çiftlerin değerleri varlığın karşılık gelen özelliklerine göre ayarlanır.

“Hızlı” (kıyaslamalarda Hızlı Öneki):

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

Gördüğümüz gibi, ayarlayıcı özelliklerine sahip statik bir koleksiyon kullanılır - ayarlayıcı varlığını çağıran derlenmiş lambdalar. Aşağıdaki kodla oluşturuldu:

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

Genel olarak açıktır. Özellikleri dolaşıyoruz, onlar için ayarlayıcıları çağıran delegeler oluşturuyoruz ve onları kaydediyoruz. Daha sonra gerektiğinde ararız.

“Yavaş” (Kıyaslamalarda Yavaş Öneki):

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

Burada hemen özellikleri atlayıp doğrudan SetValue'yu çağırıyoruz.

Açıklık sağlamak ve referans olması açısından, korelasyon çiftlerinin değerlerini doğrudan varlık alanlarına yazan saf bir yöntem uyguladım. Önek – Manuel.

Şimdi BenchmarkDotNet'i alıp performansı inceleyelim. Ve birden... (spoiler - bu doğru sonuç değil, detaylar aşağıda)

Yansımayı hızlandırmayla ilgili başarısız makale

Burada ne görüyoruz? Hızlı ön ekini başarıyla taşıyan yöntemlerin neredeyse tüm geçişlerde Yavaş ön ekini taşıyan yöntemlere göre daha yavaş olduğu ortaya çıktı. Bu hem tahsis hem de işin hızı için geçerlidir. Öte yandan, buna yönelik LINQ yöntemlerini kullanarak mümkün olan her yerde güzel ve zarif bir haritalama uygulaması, tam tersine verimliliği büyük ölçüde azaltır. Aradaki fark düzendir. Farklı geçiş sayılarıyla trend değişmiyor. Tek fark ölçektedir. LINQ ile 4 - 200 kat daha yavaştır, yaklaşık olarak aynı ölçekte daha fazla çöp vardır.

GÜNCEL

Gözlerime inanamadım ama daha da önemlisi meslektaşımız ne gözlerime ne de koduma inandı. Dmitry Tikhonov 0x1000000. Çözümümü iki kez kontrol ettikten sonra, başından sonuna kadar uygulamadaki bazı değişiklikler nedeniyle gözden kaçırdığım bir hatayı zekice keşfetti ve işaret etti. Moq kurulumunda bulunan hatayı düzelttikten sonra tüm sonuçlar yerine oturdu. Yeniden test sonuçlarına göre ana eğilim değişmiyor - LINQ hâlâ performansı yansımadan daha fazla etkiliyor. Ancak İfade derlemesi ile çalışmanın boşuna yapılmaması ve sonucun hem tahsis hem de yürütme zamanında görünür olması güzel. Statik alanlar başlatıldığında ilk başlatma, "hızlı" yöntem için doğal olarak daha yavaştır, ancak daha sonra durum değişir.

İşte yeniden testin sonucu:

Yansımayı hızlandırmayla ilgili başarısız makale

Sonuç: Bir kuruluşta yansımayı kullanırken hilelere başvurmaya özel bir gerek yoktur - LINQ üretkenliği daha fazla tüketecektir. Bununla birlikte, optimizasyon gerektiren yüksek yüklü yöntemlerde, yansımayı başlatıcılar ve temsilci derleyiciler biçiminde kaydedebilirsiniz; bu daha sonra "hızlı" mantık sağlayacaktır. Bu şekilde hem yansıma esnekliğini hem de uygulamanın hızını koruyabilirsiniz.

Karşılaştırma kodunu burada bulabilirsiniz. Herkes sözlerimi tekrar kontrol edebilir:
HabraYansımaTestleri

Not: Testlerdeki kod IoC'yi kullanıyor ve kıyaslamalarda açık bir yapı kullanıyor. Gerçek şu ki, son uygulamada performansı etkileyebilecek ve sonucu gürültülü hale getirebilecek tüm faktörleri kestim.

PPS: Kullanıcıya teşekkürler Dmitry Tikhonov @0x1000000 Moq'u ayarlarken ilk ölçümleri etkileyen hatamı keşfettiğim için. Okuyuculardan herhangi birinin yeterli karması varsa lütfen beğenin. Adam durdu, okudu, adam tekrar kontrol etti ve hatayı belirtti. Bunun saygı ve sempatiye değer olduğunu düşünüyorum.

PPPS: Stil ve tasarımın temeline inen titiz okuyucuya teşekkürler. Ben tekdüzelik ve rahatlıktan yanayım. Sunumun diplomasisi arzulanan çok şey bırakıyor ancak eleştirileri dikkate aldım. Mermiyi istiyorum.

Kaynak: habr.com

Yorum ekle