Artikel gagal tentang mempercepat refleksi

Langsung saja saya jelaskan judul artikelnya. Rencana awalnya adalah memberikan nasehat yang baik dan terpercaya tentang cara mempercepat penggunaan refleksi dengan menggunakan contoh yang sederhana namun realistis, namun saat benchmarking ternyata refleksi tidak selambat yang saya kira, LINQ lebih lambat dari pada mimpi buruk saya. Namun pada akhirnya ternyata saya juga melakukan kesalahan dalam pengukurannya... Detail kisah hidup ini ada di bawah potongan dan di komentar. Karena contoh tersebut cukup lumrah dan diterapkan pada prinsipnya seperti yang biasa dilakukan di suatu perusahaan, ternyata contoh tersebut cukup menarik, menurut saya, demonstrasi kehidupan: dampaknya terhadap kecepatan subjek utama artikel tersebut adalah tidak terlihat karena logika eksternal: Moq, Autofac, EF Core dan "tali" lainnya.

Saya mulai bekerja berdasarkan kesan artikel ini: Mengapa Refleksi lambat

Seperti yang Anda lihat, penulis menyarankan untuk menggunakan delegasi yang dikompilasi daripada memanggil metode tipe refleksi secara langsung sebagai cara terbaik untuk mempercepat aplikasi. Tentu saja ada emisi IL, tapi saya ingin menghindarinya, karena ini adalah cara yang paling memakan waktu untuk melakukan tugas, yang penuh dengan kesalahan.

Mengingat saya selalu mempunyai pendapat serupa tentang kecepatan refleksi, saya tidak bermaksud mempertanyakan kesimpulan penulis.

Saya sering menjumpai penggunaan refleksi yang naif dalam perusahaan. Jenisnya diambil. Informasi tentang properti diambil. Metode SetValue dipanggil dan semua orang bersukacita. Nilainya sudah sampai di bidang sasaran, semua senang. Orang-orang yang sangat pintar - senior dan pemimpin tim - menulis ekstensi mereka ke objek, mendasarkan pada implementasi pemetaan “universal” yang naif dari satu jenis ke jenis lainnya. Intinya biasanya ini: kita mengambil semua bidang, mengambil semua properti, mengulanginya: jika nama anggota tipe cocok, kita menjalankan SetValue. Dari waktu ke waktu kami menemukan pengecualian karena kesalahan di mana kami tidak menemukan beberapa properti di salah satu tipe, namun bahkan di sini ada jalan keluar yang meningkatkan kinerja. Coba tangkap.

Saya telah melihat orang-orang menemukan kembali parser dan mapper tanpa memiliki informasi lengkap tentang cara kerja mesin yang ada sebelum mereka. Saya telah melihat orang-orang menyembunyikan implementasi naif mereka di balik strategi, di balik antarmuka, di balik suntikan, seolah-olah ini akan menjadi alasan untuk terjadinya bacchanalia berikutnya. Aku menutup hidungku pada kesadaran seperti itu. Faktanya, saya tidak mengukur kebocoran kinerja sebenarnya, dan, jika memungkinkan, saya hanya mengubah implementasinya ke implementasi yang lebih “optimal” jika saya bisa melakukannya. Oleh karena itu, pengukuran pertama yang dibahas di bawah ini benar-benar membingungkan saya.

Saya rasa banyak dari Anda, yang membaca Richter atau ahli ideologi lainnya, telah menemukan pernyataan yang cukup adil bahwa refleksi dalam kode adalah fenomena yang memiliki dampak yang sangat negatif terhadap kinerja aplikasi.

Memanggil refleksi memaksa CLR melalui rakitan untuk menemukan yang mereka perlukan, mengambil metadatanya, menguraikannya, dll. Selain itu, refleksi saat melintasi urutan menyebabkan alokasi memori dalam jumlah besar. Kami menggunakan memori, CLR mengungkap GC dan jalur dimulai. Ini akan terasa lambat, percayalah. Memori dalam jumlah besar pada server produksi modern atau mesin cloud tidak mencegah penundaan pemrosesan yang tinggi. Faktanya, semakin banyak memori, semakin besar kemungkinan Anda MEMPERHATIKAN cara kerja GC. Refleksi, secara teori, merupakan kain merah ekstra baginya.

Namun, kita semua menggunakan wadah IoC dan pemetaan tanggal, yang prinsip pengoperasiannya juga didasarkan pada refleksi, namun biasanya tidak ada pertanyaan tentang kinerjanya. Tidak, bukan karena pengenalan ketergantungan dan abstraksi dari model konteks terbatas eksternal sangat diperlukan sehingga kita harus mengorbankan kinerja dalam hal apa pun. Semuanya lebih sederhana - ini tidak terlalu memengaruhi kinerja.

Faktanya adalah kerangka kerja paling umum yang didasarkan pada teknologi refleksi menggunakan segala macam trik untuk bekerja dengannya secara lebih optimal. Biasanya ini adalah cache. Biasanya ini adalah Ekspresi dan delegasi yang dikompilasi dari pohon ekspresi. Pemetaan otomatis yang sama memelihara kamus kompetitif yang mencocokkan tipe dengan fungsi yang dapat mengonversi satu sama lain tanpa memanggil refleksi.

Bagaimana hal ini dicapai? Pada dasarnya, ini tidak berbeda dengan logika yang digunakan platform itu sendiri untuk menghasilkan kode JIT. Ketika suatu metode dipanggil untuk pertama kalinya, metode tersebut dikompilasi (dan, ya, proses ini tidak cepat); pada panggilan berikutnya, kontrol ditransfer ke metode yang sudah dikompilasi, dan tidak akan ada penurunan kinerja yang signifikan.

Dalam kasus kami, Anda juga dapat menggunakan kompilasi JIT dan kemudian menggunakan perilaku yang dikompilasi dengan kinerja yang sama dengan rekan-rekan AOT-nya. Ekspresi akan membantu kami dalam kasus ini.

Prinsip yang dimaksud dapat dirumuskan secara singkat sebagai berikut:
Anda harus menyimpan hasil akhir refleksi sebagai delegasi yang berisi fungsi yang dikompilasi. Masuk akal juga untuk menyimpan cache semua objek yang diperlukan dengan informasi tipe di bidang tipe Anda, pekerja, yang disimpan di luar objek.

Ada logika dalam hal ini. Akal sehat memberi tahu kita bahwa jika sesuatu dapat dikompilasi dan di-cache, maka hal itu harus dilakukan.

Ke depan, harus dikatakan bahwa cache dalam bekerja dengan refleksi memiliki kelebihan, bahkan jika Anda tidak menggunakan metode kompilasi ekspresi yang diusulkan. Sebenarnya di sini saya hanya mengulang tesis penulis artikel yang saya rujuk di atas.

Sekarang tentang kodenya. Mari kita lihat contoh yang didasarkan pada penderitaan saya baru-baru ini yang harus saya hadapi dalam produksi serius di sebuah lembaga kredit yang serius. Semua entitas adalah fiktif sehingga tidak ada yang bisa menebaknya.

Ada beberapa esensi. Biarlah ada Kontak. Ada huruf dengan badan standar, dari mana parser dan hidrator membuat kontak yang sama. Sebuah surat tiba, kami membacanya, menguraikannya menjadi pasangan nilai kunci, membuat kontak, dan menyimpannya dalam database.

Itu dasar. Katakanlah sebuah kontak memiliki properti Nama Lengkap, Usia dan Telepon Kontak. Data ini dikirimkan melalui surat. Bisnis juga menginginkan dukungan untuk dapat dengan cepat menambahkan kunci baru untuk memetakan properti entitas secara berpasangan di badan surat. Jika seseorang salah ketik pada template atau jika sebelum rilis perlu segera meluncurkan pemetaan dari mitra baru, beradaptasi dengan format baru. Kemudian kita dapat menambahkan korelasi pemetaan baru sebagai perbaikan data yang murah. Itulah contoh kehidupan.

Kami menerapkan, membuat tes. Bekerja.

Saya tidak akan memberikan kodenya: ada banyak sumber, dan tersedia di GitHub melalui tautan di akhir artikel. Anda dapat memuatnya, menyiksanya hingga tidak dapat dikenali lagi, dan mengukurnya, karena hal ini akan berdampak pada kasus Anda. Saya hanya akan memberikan kode dua metode template yang membedakan hydrator yang seharusnya cepat dengan hydrator yang seharusnya lambat.

Logikanya adalah sebagai berikut: metode templat menerima pasangan yang dihasilkan oleh logika parser dasar. Lapisan LINQ adalah parser dan logika dasar hidrator, yang membuat permintaan ke konteks database dan membandingkan kunci dengan pasangan dari parser (untuk fungsi ini ada kode tanpa LINQ sebagai perbandingan). Selanjutnya, pasangan diteruskan ke metode hidrasi utama dan nilai pasangan ditetapkan ke properti entitas yang sesuai.

“Fast” (Awalan Fast di benchmark):

 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 bisa kita lihat, koleksi statis dengan properti penyetel digunakan - lambda terkompilasi yang memanggil entitas penyetel. Dibuat dengan kode 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 sudah jelas. Kami melintasi properti, membuat delegasi untuk mereka yang memanggil setter, dan menyimpannya. Lalu kami menelepon bila diperlukan.

“Lambat” (Awalan Lambat dalam tolok ukur):

        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 kita segera melewati properti dan memanggil SetValue secara langsung.

Untuk kejelasan dan sebagai referensi, saya menerapkan metode naif yang menulis nilai pasangan korelasinya langsung ke bidang entitas. Awalan – Panduan.

Sekarang mari kita gunakan BenchmarkDotNet dan periksa kinerjanya. Dan tiba-tiba... (spoiler - ini bukan hasil yang benar, detailnya ada di bawah)

Artikel gagal tentang mempercepat refleksi

Apa yang kita lihat di sini? Metode yang menggunakan awalan Fast ternyata lebih lambat di hampir semua lintasan dibandingkan metode dengan awalan Lambat. Hal ini berlaku baik untuk alokasi maupun kecepatan kerja. Di sisi lain, implementasi pemetaan yang indah dan elegan menggunakan metode LINQ yang dimaksudkan untuk hal ini, sebaliknya, sangat mengurangi produktivitas. Perbedaannya terletak pada urutannya. Trennya tidak berubah dengan jumlah operan yang berbeda. Perbedaannya hanya pada skala. Dengan LINQ, kecepatannya 4 - 200 kali lebih lambat, terdapat lebih banyak sampah pada skala yang kira-kira sama.

UPDATED

Saya tidak mempercayai mata saya, tetapi yang lebih penting, kolega kami tidak mempercayai mata saya atau kode saya - Dmitry Tikhonov 0x1000000. Setelah memeriksa ulang solusi saya, dia dengan cemerlang menemukan dan menunjukkan kesalahan yang saya lewatkan karena sejumlah perubahan dalam implementasi, dari awal hingga akhir. Setelah memperbaiki bug yang ditemukan di pengaturan Moq, semua hasil sesuai dengan tempatnya. Berdasarkan hasil pengujian ulang, tren utama tidak berubah - LINQ masih lebih mempengaruhi kinerja daripada refleksi. Namun, alangkah baiknya jika pekerjaan kompilasi Ekspresi tidak dilakukan dengan sia-sia, dan hasilnya terlihat baik dalam alokasi maupun waktu eksekusi. Peluncuran pertama, ketika bidang statis diinisialisasi, secara alami lebih lambat untuk metode "cepat", tetapi kemudian situasinya berubah.

Berikut hasil tes ulangnya:

Artikel gagal tentang mempercepat refleksi

Kesimpulan: saat menggunakan refleksi dalam suatu perusahaan, tidak ada kebutuhan khusus untuk menggunakan trik - LINQ akan menghabiskan lebih banyak produktivitas. Namun, dalam metode beban tinggi yang memerlukan optimasi, Anda dapat menyimpan refleksi dalam bentuk inisialisasi dan kompiler delegasi, yang kemudian akan memberikan logika “cepat”. Dengan cara ini Anda dapat menjaga fleksibilitas refleksi dan kecepatan aplikasi.

Kode benchmark tersedia di sini. Siapa pun dapat memeriksa ulang kata-kata saya:
Tes Refleksi Habra

PS: kode dalam pengujian menggunakan IoC, dan dalam benchmark menggunakan konstruksi eksplisit. Faktanya adalah bahwa dalam implementasi akhir saya menghilangkan semua faktor yang dapat mempengaruhi kinerja dan membuat hasilnya berisik.

PPS: Terima kasih kepada pengguna Dmitry Tikhonov @0x1000000 untuk menemukan kesalahan saya dalam menyiapkan Moq, yang memengaruhi pengukuran pertama. Jika ada di antara pembaca yang memiliki karma yang cukup, silakan menyukainya. Laki-laki itu berhenti, laki-laki itu membaca, laki-laki itu memeriksa ulang dan menunjukkan kesalahannya. Saya pikir ini layak untuk dihormati dan disimpati.

PPPS: terima kasih kepada pembaca teliti yang telah memahami gaya dan desain. Saya mendukung keseragaman dan kenyamanan. Diplomasi presentasinya menyisakan banyak hal yang diinginkan, tetapi saya mempertimbangkan kritik tersebut. Saya meminta proyektilnya.

Sumber: www.habr.com

Tambah komentar