Artikel yang tidak berjaya tentang mempercepatkan refleksi

Saya akan segera menerangkan tajuk artikel. Pelan asal adalah untuk memberi nasihat yang baik dan boleh dipercayai tentang cara mempercepatkan penggunaan refleksi menggunakan contoh yang mudah tetapi realistik, tetapi semasa penanda aras ternyata refleksi tidak selambat yang saya fikirkan, LINQ lebih perlahan daripada mimpi buruk saya. Tetapi akhirnya ternyata saya juga tersilap dalam ukuran... Butiran kisah hidup ini ada di bawah potong dan di komen. Oleh kerana contoh itu agak biasa dan dilaksanakan secara prinsip seperti yang biasanya dilakukan dalam perusahaan, ternyata agak menarik, seperti yang saya nampak, demonstrasi kehidupan: kesan ke atas kelajuan subjek utama artikel itu adalah tidak ketara kerana logik luaran: Moq, Autofac, EF Core dan lain-lain "tali".

Saya mula bekerja di bawah tanggapan artikel ini: Kenapa Refleksi lambat

Seperti yang anda lihat, penulis mencadangkan menggunakan perwakilan yang disusun dan bukannya memanggil kaedah jenis refleksi secara langsung sebagai cara terbaik untuk mempercepatkan aplikasi. Sudah tentu, terdapat pelepasan IL, tetapi saya ingin mengelakkannya, kerana ini adalah cara yang paling intensif buruh untuk melaksanakan tugas, yang penuh dengan kesilapan.

Memandangkan saya sentiasa mempunyai pendapat yang sama tentang kelajuan refleksi, saya tidak berniat untuk mempersoalkan kesimpulan pengarang.

Saya sering menghadapi penggunaan refleksi yang naif dalam perusahaan. Jenis diambil. Maklumat mengenai harta itu diambil. Kaedah SetValue dipanggil dan semua orang bergembira. Nilai telah tiba di medan sasaran, semua orang gembira. Orang yang sangat bijak - senior dan ketua pasukan - menulis sambungan mereka untuk membantah, berdasarkan pemetaan "sejagat" pelaksanaan yang naif dari satu jenis ke yang lain. Intipatinya biasanya ini: kami mengambil semua medan, mengambil semua sifat, mengulanginya: jika nama jenis ahli sepadan, kami melaksanakan SetValue. Dari semasa ke semasa kami mendapat pengecualian kerana kesilapan di mana kami tidak menemui beberapa harta dalam salah satu jenis, tetapi di sini terdapat jalan keluar yang meningkatkan prestasi. Cuba tangkap.

Saya telah melihat orang mencipta semula penghurai dan pemeta tanpa bersenjata sepenuhnya dengan maklumat tentang cara mesin yang datang sebelum mereka berfungsi. Saya telah melihat orang menyembunyikan pelaksanaan naif mereka di sebalik strategi, di belakang antara muka, di sebalik suntikan, seolah-olah ini akan memaafkan bacchanalia berikutnya. Saya mengangkat hidung saya pada kesedaran sedemikian. Sebenarnya, saya tidak mengukur kebocoran prestasi sebenar, dan, jika boleh, saya hanya menukar pelaksanaan kepada yang lebih "optimum" jika saya boleh mendapatkannya. Oleh itu, ukuran pertama yang dibincangkan di bawah sangat mengelirukan saya.

Saya rasa ramai di antara anda, membaca Richter atau ahli ideologi lain, telah menemui kenyataan yang benar-benar adil bahawa refleksi dalam kod adalah fenomena yang mempunyai kesan yang sangat negatif terhadap prestasi aplikasi.

Memanggil refleksi memaksa CLR untuk melalui perhimpunan untuk mencari yang mereka perlukan, menarik metadata mereka, menghuraikannya, dsb. Di samping itu, refleksi semasa melintasi urutan membawa kepada peruntukan sejumlah besar ingatan. Kami sedang menggunakan memori, CLR mendedahkan GC dan pembekuan bermula. Ia sepatutnya perlahan, percayalah. Jumlah memori yang besar pada pelayan pengeluaran moden atau mesin awan tidak menghalang kelewatan pemprosesan yang tinggi. Malah, lebih banyak ingatan, lebih besar kemungkinan anda PERHATIAN cara GC berfungsi. Refleksi adalah, secara teori, kain merah tambahan untuknya.

Walau bagaimanapun, kami semua menggunakan bekas IoC dan pemeta tarikh, yang prinsip operasinya juga berdasarkan pantulan, tetapi biasanya tiada soalan tentang prestasinya. Tidak, bukan kerana pengenalan kebergantungan dan pengabstrakan daripada model konteks terhad luaran sangat diperlukan sehingga kita perlu mengorbankan prestasi dalam apa jua keadaan. Segala-galanya lebih mudah - ia tidak banyak menjejaskan prestasi.

Hakikatnya ialah rangka kerja yang paling biasa yang berdasarkan teknologi pantulan menggunakan segala macam helah untuk bekerja dengannya dengan lebih optimum. Biasanya ini adalah cache. Biasanya ini ialah Ungkapan dan perwakilan yang disusun daripada pokok ungkapan. Automapper yang sama mengekalkan kamus kompetitif yang memadankan jenis dengan fungsi yang boleh menukar satu sama lain tanpa memanggil refleksi.

Bagaimana ini dicapai? Pada asasnya, ini tidak berbeza dengan logik yang digunakan oleh platform itu sendiri untuk menjana kod JIT. Apabila kaedah dipanggil buat kali pertama, ia disusun (dan, ya, proses ini tidak pantas); pada panggilan berikutnya, kawalan dipindahkan ke kaedah yang telah disusun, dan tidak akan ada penarikan prestasi yang ketara.

Dalam kes kami, anda juga boleh menggunakan kompilasi JIT dan kemudian menggunakan gelagat yang disusun dengan prestasi yang sama seperti rakan AOTnya. Ungkapan akan membantu kami dalam kes ini.

Prinsip yang dimaksudkan boleh dirumuskan secara ringkas seperti berikut:
Anda harus cache hasil akhir refleksi sebagai perwakilan yang mengandungi fungsi yang disusun. Ia juga masuk akal untuk menyimpan semua objek yang diperlukan dengan maklumat jenis dalam medan jenis anda, pekerja, yang disimpan di luar objek.

Terdapat logik dalam ini. Akal sehat memberitahu kita bahawa jika sesuatu boleh disusun dan dicache, maka ia harus dilakukan.

Memandang ke hadapan, harus dikatakan bahawa cache dalam bekerja dengan refleksi mempunyai kelebihannya, walaupun anda tidak menggunakan kaedah yang dicadangkan untuk menyusun ungkapan. Sebenarnya, di sini saya hanya mengulangi tesis penulis artikel yang saya rujuk di atas.

Sekarang mengenai kod. Mari kita lihat contoh yang berdasarkan kesakitan saya baru-baru ini yang terpaksa saya hadapi dalam pengeluaran serius institusi kredit yang serius. Semua entiti adalah rekaan supaya tiada siapa meneka.

Terdapat beberapa intipati. Biar ada Kenalan. Terdapat huruf dengan badan piawai, dari mana parser dan hydrator mencipta kenalan yang sama ini. Sepucuk surat tiba, kami membacanya, menghuraikannya ke dalam pasangan nilai kunci, mencipta kenalan dan menyimpannya dalam pangkalan data.

Ia adalah asas. Katakan kenalan mempunyai sifat Nama Penuh, Umur dan Telefon Kenalan. Data ini dihantar dalam surat. Perniagaan ini juga mahukan sokongan agar dapat menambahkan kunci baharu dengan pantas untuk memetakan sifat entiti kepada pasangan dalam badan surat. Sekiranya seseorang membuat kesilapan dalam templat atau jika sebelum keluaran adalah perlu untuk melancarkan pemetaan daripada rakan kongsi baharu dengan segera, menyesuaikan diri dengan format baharu. Kemudian kita boleh menambah korelasi pemetaan baharu sebagai pembetulan data yang murah. Iaitu, contoh kehidupan.

Kami melaksanakan, membuat ujian. Berfungsi.

Saya tidak akan memberikan kod tersebut: terdapat banyak sumber, dan ia tersedia di GitHub melalui pautan di penghujung artikel. Anda boleh memuatkannya, menyeksa mereka tanpa dapat dikenali dan mengukurnya, kerana ia akan menjejaskan kes anda. Saya hanya akan memberikan kod dua kaedah templat yang membezakan hydrator, yang sepatutnya pantas, daripada hydrator, yang sepatutnya perlahan.

Logiknya adalah seperti berikut: kaedah templat menerima pasangan yang dijana oleh logik penghurai asas. Lapisan LINQ ialah penghurai dan logik asas penghidrat, yang membuat permintaan kepada konteks pangkalan data dan membandingkan kunci dengan pasangan daripada penghurai (untuk fungsi ini terdapat kod tanpa LINQ untuk perbandingan). Seterusnya, pasangan dihantar ke kaedah penghidratan utama dan nilai pasangan ditetapkan kepada sifat sepadan entiti.

β€œCepat” (Awalan Cepat dalam penanda aras):

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

Seperti yang dapat kita lihat, koleksi statik dengan sifat penetap digunakan - lambdas tersusun yang memanggil entiti penetap. Dibuat oleh kod berikut:

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

Secara umum ia adalah jelas. Kami melintasi sifat, membuat perwakilan untuk mereka yang memanggil penetap dan menyimpannya. Kemudian kami telefon apabila perlu.

β€œPerlahan” (Awalan Perlahan dalam penanda aras):

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

Di sini kami segera memintas sifat dan memanggil SetValue terus.

Untuk kejelasan dan sebagai rujukan, saya melaksanakan kaedah naif yang menulis nilai pasangan korelasi mereka terus ke dalam medan entiti. Awalan – Manual.

Sekarang mari kita ambil BenchmarkDotNet dan periksa prestasinya. Dan tiba-tiba... (spoiler - ini bukan keputusan yang betul, butiran ada di bawah)

Artikel yang tidak berjaya tentang mempercepatkan refleksi

Apa yang kita lihat di sini? Kaedah yang berjaya menanggung awalan Cepat ternyata lebih perlahan dalam hampir semua laluan berbanding kaedah dengan awalan Perlahan. Ini benar untuk kedua-dua peruntukan dan kelajuan kerja. Sebaliknya, pelaksanaan pemetaan yang cantik dan elegan menggunakan kaedah LINQ bertujuan untuk ini di mana mungkin, sebaliknya, sangat mengurangkan produktiviti. Perbezaannya adalah tertib. Trend tidak berubah dengan bilangan hantaran yang berbeza. Satu-satunya perbezaan adalah dalam skala. Dengan LINQ ia adalah 4 - 200 kali lebih perlahan, terdapat lebih banyak sampah pada skala yang lebih kurang sama.

UPDATED

Saya tidak mempercayai pandangan saya, tetapi yang lebih penting, rakan sekerja kami tidak mempercayai sama ada pandangan saya atau kod saya - Dmitry Tikhonov 0x1000000. Setelah menyemak semula penyelesaian saya, dia dengan cemerlang menemui dan menunjukkan ralat yang saya terlepas disebabkan oleh beberapa perubahan dalam pelaksanaan, dari awal hingga akhir. Selepas membetulkan pepijat yang ditemui dalam persediaan Moq, semua keputusan jatuh ke tempatnya. Mengikut keputusan ujian semula, arah aliran utama tidak berubah - LINQ masih menjejaskan prestasi lebih daripada refleksi. Walau bagaimanapun, adalah bagus bahawa kerja dengan kompilasi Ungkapan tidak dilakukan dengan sia-sia, dan hasilnya dapat dilihat dalam peruntukan dan masa pelaksanaan. Pelancaran pertama, apabila medan statik dimulakan, secara semula jadi lebih perlahan untuk kaedah "pantas", tetapi kemudian keadaan berubah.

Berikut adalah keputusan ujian semula:

Artikel yang tidak berjaya tentang mempercepatkan refleksi

Kesimpulan: apabila menggunakan refleksi dalam perusahaan, tidak ada keperluan khusus untuk menggunakan helah - LINQ akan memakan produktiviti lebih banyak. Walau bagaimanapun, dalam kaedah beban tinggi yang memerlukan pengoptimuman, anda boleh menyimpan refleksi dalam bentuk pemula dan penyusun wakil, yang kemudiannya akan memberikan logik "cepat". Dengan cara ini anda boleh mengekalkan kedua-dua fleksibiliti refleksi dan kelajuan aplikasi.

Kod penanda aras tersedia di sini. Sesiapa sahaja boleh menyemak semula kata-kata saya:
HabraReflectionTests

PS: kod dalam ujian menggunakan IoC, dan dalam penanda aras ia menggunakan binaan eksplisit. Hakikatnya ialah dalam pelaksanaan akhir saya memotong semua faktor yang boleh menjejaskan prestasi dan membuat keputusan bising.

PPS: Terima kasih kepada pengguna Dmitry Tikhonov @0x1000000 kerana menemui kesilapan saya dalam menyediakan Moq, yang menjejaskan pengukuran pertama. Jika ada pembaca yang cukup karma, sila like. Lelaki itu berhenti, lelaki itu membaca, lelaki itu menyemak semula dan menunjukkan kesilapan. Saya rasa ini patut dihormati dan simpati.

PPPS: terima kasih kepada pembaca yang teliti yang sampai ke bahagian bawah gaya dan reka bentuk. Saya untuk keseragaman dan kemudahan. Diplomasi pembentangan meninggalkan banyak yang diingini, tetapi saya mengambil kira kritikan itu. Saya meminta peluru itu.

Sumber: www.habr.com

Tambah komen