BatMap
13 Apr 2017BatMap 🦇 C# dili ile geliştirilmiş yeni bir Mapper. Hemen akıllara gelen soru: “Neden bir Mapper daha?”
Neden olmasın?
Hemen açıklayayım
Diyelim ki aşağıdaki gibi bir sınıf yapınız var:
Sipariş ve Sipariş Detay sınıfları iki taraflı ilişkili. DTO sınıflarınız da aşağıdaki gibi olsun:
Bu ilişkiyi DTO sınıflarında da yukarıdaki gibi koruduğunuzda bir çok Mapper kayıt aşamasında StackOverflowException hatası üretir. Neden mi? Eğer bu araçların nasıl çalıştıklarını incelediyseniz sizin için otomatik kod ürettiklerini farketmişsinizdir, aşağıdaki gibi:
Sorunu görebildiniz mi? “…” ile devamını getirmediğim kısım Mapper’ın sonsuz döngüye girip StackOverflowException hatasına sebep olmasıyla sonuçlanıyor.
Bazı Mapper’lar kayıt aşamasında ilişkiler için temsili bir atama oluşturarak bu hatanın önüne geçebiliyor, ancak sadece kayıt sırasında. Gerçekten bir Mapping işlemi yapmak istediğinizde bu iki ilişkinin de dolu olması durumunda beklenmedik bir anda aynı şekilde StackOverflowException alıyorsunuz.
Çözüm
Bir Map süreci başladığında bu süreci temsil eden bir sınıfımız olursa (MapContext) ve bu sınıfı kullanarak kayıt işlemlerini aşağıdaki gibi yaparsak:
Kayıt sırasında tüm ilişki ağacını gezmeden çok daha basit ifadeler oluşturabiliyoruz.
Bu yapı da çalışma zamanı aynı hataya sebep olabilir, bir şekilde tekrar tekrar aynı objelerin oluşturulmasına engel olabilmeliyiz.
MapWithCache metodu
Referans koruyarak Map işlemi burada devreye giriyor:
Eğer Map işlemimizi yukarıdaki şekilde çağırırsak atama için otomatik oluşturduğumuz kodun biraz değiştirilmiş bir versiyonu çalışacak:
OrderDetail için oluşturulan kodu hayal gücünüze bırakıyorum.
Şimdi kodun nasıl çalıştığını inceleyelim:
- Her bir Map işleminden önce kullanacağımız nesne için hedef tipteki bir nesneyi ön bellekten istiyoruz.
- Eğer bu nesne daha önce bu hedef tip için Map işlemine girmişse hazır nesneyi dönüyoruz.
- Ön bellekte bulamaz isek daha fazla ilerlemeden (ilişkiler için Map işlemi yapmadan) ön belleğe bu yeni nesneyi koyuyoruz.
- Normal bir şekilde Map işlemine devam ediyoruz.
Böylece istediğimiz karışıklıktaki sınıfları birbirine dönüştürebiliyoruz.
Performans?
Method | Mean |
---|---|
BatMap | 1.3309 ms |
Mapster | 2.0377 ms |
SafeMapper | 2.0371 ms |
HandWritten | 2.1175 ms |
AutoMapper | 2.6187 ms |
TinyMapper | 2.8119 ms |
ExpressMapper | 5.0814 ms |
FastMapper | 5.9380 ms |
Premature optimization is the root of all evil! sözünü unutmayın. Burada yaptığım performans çalışmalarını biraz deneysel biraz da Mapper projelerinin başarısında bir numaralı kriter kabul edildiği için yaptım. Performans, projeniz ilerledikçe hedeflediğiniz süreleri alamazsanız düşünmeniz gereken bir konu olmalı.
Gördüğünüz gibi diğer Mapper’lara göre hatırı sayılır derecede fazla işlem yapıyoruz (preserveReferences aktif olmadığında bile), peki nasıl bir çoğundan daha hızlı çalışıyor?
Genel kullanım bu Expression’ları .Compile() metodu ile çalıştırılabilir metodlara derlemektir (en basit yöntem de budur). Ancak pek bilinmeyen nokta .Compile() ile derlenmiş metodun çalışabilmesi için Delegate çevirimi yapılır:
Bunun sebebine en basit haliyle C# dilinin mimari olarak bir Assembly-Type ilişkisi olmayan fonksiyona sahip olamamasıdır diyebiliriz (dinamik ve fonksiyonel dillerin aksine). Aşağıdaki gibi bir kod ile her Map metodunu yeni oluşturduğumuz bir tip içine static metod olarak eklersek ve Delegate üretme kısmını biz üstlenirsek ölçümlerime göre %30 civarı bir performans artışı gözlemleyebiliyoruz (bazı kaynaklarda %90 üzeri olduğundan bahsediliyor ancak efektif değerlerde bunu göremedim):
Daha da performans
Diğer Mapper’lara göre daha fazla iş yaptığımızı söylemiştim, bu işlerden bir tanesi de çalıştırılacak tüm Map kodunu üretmek yerine MapContext üzerinden gerekli durumlarda çağrılar yapmak:
Otomatik oluşturulan kodlardan yukarıdaki satırı OrderDetail ve OrderDetailDTO arasındaki Map kodunu bul ve bunu o.OrderDetails parametresiyle çağır şeklinde düşünebiliriz. Yani bizim elimizde her Map kaydı yapılmış tip ikilileri için Map tanımları (MapDefinition) olması gerekiyor ve bunlara çok hızlı erişebilmemiz gerekiyor. Akla ilk gelen bu iki tip için bir Tuple oluşturup bu kayıtları aşağıdaki gibi bir Dictionary ile saklamak:
Bildiğiniz gibi Dictionary kayıtları saklarken Key nesnesinin GetHashCode metodundan dönen değeri indekslemek için kullanır.
Dolayısıyla Tuple’ın GetHashCode uygulamasıyla uygulamasını kullanan bir struct yazarak (MapPair) ürettiğimiz int değeri kullanarak önemli bir performans artışı sağlayabiliyoruz. Aşağıda Dictionary ve HashCode üreten kodu görebilirsiniz:
Not: Optimizasyonu biraz abarttım, aracı struct’ları (MapPair ve CachePair) sildim, onların yerine direk üretilecek olan int değerleri kullanıyorum. Böylece hem daha ufak bir bellek imzasına sahip oluyoruz hem de performansta %10’a kadar artış görebiliyoruz.
Bir diğer ufak optimizasyon noktası da bize verilen tipli bir listeyi başka tipli bir listeye çevirirken (MapToList) karşımıza çıkıyor. Bu durumla karşılaşan bir çok yazılımcı içgüdüsel olarak aşağıdaki kodu yazacaktır:
Bu kodda çok ufak değişiklikler yaparak GitHub üzerinden erişebileceğiniz Benchmark projesini çalıştırdığımızda %50-%60 arası performans artışı sağlayabiliyoruz:
Burada dikkat edilmesi gereken noktalar:
- Bir IEnumerable çok büyük ihtimalle (hele ki bir EF varlığı olduğunu düşünürsek) IList arayüzünü uygulamış olacağından tip dönüşümü yapıyoruz.
- List her .Add çağrısında yeterli bellek ayırılmış olup olmadığına bakar, gerekirse bellek ister. Biz tam ihtiyacımız kadar bellek ayırmasını ilk adımda sağlayarak bu adımdan kurtulmuş oluyoruz.
- Liste boş ise hiç GetMapDefinition çağırmadan boş liste dönüyoruz
- foreach ile Enumerator oluşumunu engelleyerek O(1) ile çalışan indeks erişimini for ile kullanıyoruz.