Saya telah menemukan banyak tips optimasi yang mengatakan bahwa Anda harus menandai kelas Anda sebagai disegel untuk mendapatkan manfaat kinerja tambahan.
Saya menjalankan beberapa tes untuk memeriksa diferensial kinerja dan tidak menemukannya. Apakah saya melakukan sesuatu yang salah? Apakah saya melewatkan kasus di mana kelas tertutup akan memberikan hasil yang lebih baik?
Adakah yang menjalankan tes dan melihat perbedaan?
Bantu aku belajar :)
.net
optimization
frameworks
performance
Vaibhav
sumber
sumber
Jawaban:
JITter kadang-kadang akan menggunakan panggilan non-virtual ke metode dalam kelas tertutup karena tidak ada cara mereka dapat diperpanjang lebih lanjut.
Ada aturan yang kompleks mengenai jenis panggilan, virtual / nonvirtual, dan saya tidak tahu semuanya jadi saya tidak bisa menguraikannya untuk Anda, tetapi jika Anda mencari kelas disegel dan metode virtual Anda bisa menemukan beberapa artikel tentang topik tersebut.
Perhatikan bahwa segala jenis manfaat kinerja yang akan Anda peroleh dari tingkat optimasi ini harus dianggap sebagai pilihan terakhir, selalu optimalkan pada tingkat algoritmik sebelum Anda mengoptimalkan pada tingkat kode.
Berikut ini satu tautan yang menyebutkan ini: Rambling pada kata kunci yang disegel
sumber
Jawabannya adalah tidak, kelas yang disegel tidak berkinerja lebih baik daripada yang tidak tersegel.
Masalahnya muncul pada
call
vscallvirt
kode op IL.Call
lebih cepat daripadacallvirt
, dancallvirt
terutama digunakan ketika Anda tidak tahu apakah objek telah subkelas. Jadi orang berasumsi bahwa jika Anda menyegel sebuah kelas, semua kode op akan berubah daricalvirts
menjadicalls
dan akan lebih cepat.Sayangnya
callvirt
melakukan hal-hal lain yang membuatnya berguna juga, seperti memeriksa referensi nol. Ini berarti bahwa bahkan jika suatu kelas disegel, referensi mungkin masih nol dan dengan demikian acallvirt
diperlukan. Anda dapat menyiasatinya (tanpa perlu menyegel kelas), tetapi itu menjadi sedikit sia-sia.Structs digunakan
call
karena mereka tidak dapat disubklasifikasikan dan tidak pernah null.Lihat pertanyaan ini untuk informasi lebih lanjut:
Panggilan dan panggilan telepon
sumber
call
digunakan adalah: dalam situasinew T().Method()
, untukstruct
metode, untuk panggilan non-virtual kevirtual
metode (sepertibase.Virtual()
), atau untukstatic
metode. Di tempat lain menggunakancallvirt
.Pembaruan: Pada .NET Core 2.0 dan .NET Desktop 4.7.1, CLR sekarang mendukung devirtualization. Itu dapat mengambil metode dalam kelas tertutup dan mengganti panggilan virtual dengan panggilan langsung - dan itu juga bisa melakukan ini untuk kelas tidak tertutup jika bisa mengetahui aman untuk melakukannya.
Dalam kasus seperti itu (kelas tertutup yang CLR tidak bisa mendeteksi aman untuk didevirtualise), kelas tertutup sebenarnya harus menawarkan semacam manfaat kinerja.
Yang mengatakan, saya tidak akan berpikir itu layak dikhawatirkan kecuali Anda sudah membuat profil kode dan menentukan bahwa Anda berada di jalur panas yang disebut jutaan kali, atau sesuatu seperti itu:
https://blogs.msdn.microsoft.com/dotnet/2017/06/29/performance-improvements-in-ryujit-in-net-core-and-net-framework/
Jawaban asli:
Saya membuat program pengujian berikut ini, dan kemudian mendekompilasinya menggunakan Reflector untuk melihat kode MSIL apa yang dipancarkan.
Dalam semua kasus, kompiler C # (Visual studio 2010 dalam konfigurasi build Rilis) memancarkan MSIL identik, yaitu sebagai berikut:
Alasan yang sering dikutip bahwa orang mengatakan disegel memberikan manfaat kinerja adalah bahwa kompiler tahu kelas tidak diganti, dan dengan demikian dapat menggunakan
call
alih-alihcallvirt
karena tidak harus memeriksa virtual, dll. Seperti terbukti di atas, ini bukan benar.Pikiran saya berikutnya adalah bahwa meskipun MSIL identik, mungkin kompiler JIT memperlakukan kelas yang disegel berbeda?
Saya menjalankan rilis build di bawah visual studio debugger dan melihat output x86 yang di-decompile. Dalam kedua kasus, kode x86 identik, dengan pengecualian nama kelas dan alamat memori fungsi (yang tentu saja harus berbeda). Ini dia
Saya kemudian berpikir mungkin berjalan di bawah debugger menyebabkannya melakukan optimasi yang kurang agresif?
Saya kemudian menjalankan rilis mandiri yang dapat dieksekusi di luar lingkungan debugging apa pun, dan menggunakan WinDBG + SOS untuk menerobos setelah program selesai, dan melihat dissasembly dari kode x86 yang dikompilasi JIT.
Seperti yang dapat Anda lihat dari kode di bawah ini, ketika menjalankan di luar debugger, kompiler JIT lebih agresif, dan itu telah memasukkan
WriteIt
metode langsung ke pemanggil. Namun yang penting adalah bahwa itu identik ketika memanggil kelas disegel vs non-disegel. Tidak ada perbedaan apa pun antara kelas tertutup atau tidak.Ini dia ketika memanggil kelas normal:
Vs kelas tersegel:
Bagi saya, ini memberikan bukti kuat bahwa tidak mungkin ada peningkatan kinerja antara metode panggilan pada kelas disegel vs non-disegel ... Saya pikir saya senang sekarang :-)
sumber
callvirt
ketika metode ini tidak virtual untuk memulai?callvirt
untuk metode yang disegel karena masih harus memeriksa objek sebelum memohon pemanggilan metode, dan begitu Anda memperhitungkannya, Anda mungkin juga hanya perlu gunakancallvirt
. Untuk menghapuscallvirt
dan hanya melompat secara langsung, mereka perlu memodifikasi C # untuk memungkinkan((string)null).methodCall()
seperti C ++, atau mereka perlu membuktikan secara statis bahwa objek itu tidak nol (yang dapat mereka lakukan, tetapi tidak repot-repot)Seperti yang saya tahu, tidak ada jaminan manfaat kinerja. Tetapi ada kemungkinan untuk mengurangi penalti kinerja dalam kondisi tertentu dengan metode tertutup. (kelas tertutup membuat semua metode harus disegel.)
Tetapi tergantung pada implementasi kompiler dan lingkungan eksekusi.
Detail
Banyak CPU modern menggunakan struktur pipa panjang untuk meningkatkan kinerja. Karena CPU jauh lebih cepat daripada memori, CPU harus mengambil kode dari memori untuk mempercepat pipa. Jika kode tidak siap pada waktu yang tepat, pipa akan menganggur.
Ada kendala besar yang disebut pengiriman dinamis yang mengganggu pengoptimalan 'pengambilan awal' ini. Anda dapat memahami ini hanya sebagai percabangan bersyarat.
CPU tidak dapat mengambil kode berikutnya untuk dieksekusi dalam kasus ini karena posisi kode berikutnya tidak diketahui sampai kondisi teratasi. Jadi ini membuat bahaya menyebabkan idle pipeline. Dan penalti kinerja oleh idle sangat besar secara teratur.
Hal serupa terjadi jika metode override. Kompiler dapat menentukan metode yang tepat untuk mengganti metode panggilan saat ini, tetapi terkadang tidak mungkin. Dalam hal ini, metode yang tepat hanya dapat ditentukan saat runtime. Ini juga merupakan kasus pengiriman dinamis, dan, alasan utama bahasa yang diketik secara dinamis umumnya lebih lambat daripada bahasa yang diketik secara statis.
Beberapa CPU (termasuk chip Intel x86 baru-baru ini) menggunakan teknik yang disebut eksekusi spekulatif untuk memanfaatkan pipa bahkan pada situasi. Hanya mengambil satu dari jalur eksekusi. Tetapi hit rate dari teknik ini tidak begitu tinggi. Dan kegagalan spekulasi menyebabkan kios pipa yang juga membuat penalti kinerja yang sangat besar. (Ini sepenuhnya dengan implementasi CPU. Beberapa CPU mobile dikenal tidak melakukan optimasi untuk menghemat energi)
Pada dasarnya, C # adalah bahasa yang dikompilasi secara statis. Tapi tidak selalu. Saya tidak tahu persis kondisi dan ini sepenuhnya tergantung pada implementasi kompiler. Beberapa kompiler dapat menghilangkan kemungkinan pengiriman dinamis dengan mencegah penggantian metode jika metode ditandai sebagai
sealed
. Kompiler bodoh mungkin tidak. Ini adalah manfaat kinerja darisealed
.Jawaban ini ( Mengapa lebih cepat memproses array yang diurutkan dari array yang tidak disortir? ) Menggambarkan prediksi cabang jauh lebih baik.
sumber
<off-topic-rant>
Saya benci kelas yang disegel. Bahkan jika manfaat kinerja sangat mencengangkan (yang saya ragu), mereka menghancurkan model berorientasi objek dengan mencegah penggunaan kembali melalui warisan. Misalnya, kelas Thread disegel. Meskipun saya dapat melihat bahwa seseorang mungkin ingin agar thread seefisien mungkin, saya juga dapat membayangkan skenario di mana dapat mensubklasifikasikan Thread akan memiliki manfaat besar. Penulis kelas, jika Anda harus menutup kelas Anda karena alasan "kinerja", harap sediakan antarmuka setidaknya agar kami tidak perlu membungkus dan mengganti di mana pun kami membutuhkan fitur yang Anda lupa.
Contoh: SafeThread harus membungkus kelas Thread karena Thread disegel dan tidak ada antarmuka IThread; SafeThread secara otomatis menjebak pengecualian yang tidak ditangani pada utas, sesuatu yang benar-benar hilang dari kelas Utas. [dan tidak, peristiwa pengecualian tidak ditangani tidak mengambil pengecualian tidak tertangani di utas sekunder].
</off-topic-rant>
sumber
Menandai kelas
sealed
seharusnya tidak memiliki dampak kinerja.Ada beberapa kasus di mana
csc
mungkin harus memancarkancallvirt
opcode alih-alihcall
opcode. Namun, sepertinya kasus-kasus itu jarang terjadi.Dan sepertinya bagi saya bahwa JIT harus dapat memancarkan panggilan fungsi non-virtual yang sama untuk
callvirt
itucall
, jika ia tahu bahwa kelas tidak memiliki subclass (belum). Jika hanya ada satu implementasi dari metode ini, tidak ada gunanya memuat alamatnya dari suatu vtable — cukup panggil satu implementasi secara langsung. Dalam hal ini, JIT bahkan dapat menyamakan fungsi.Ini sedikit berjudi pada bagian JIT, karena jika subclass yang kemudian dimuat, JIT akan harus membuang kode mesin dan mengkompilasi kode lagi, memancarkan panggilan virtual yang nyata. Dugaan saya adalah ini tidak sering terjadi dalam praktek.
(Dan ya, perancang VM sangat agresif mengejar kemenangan kecil ini.)
sumber
Kelas tertutup harus memberikan peningkatan kinerja. Karena kelas yang disegel tidak dapat diturunkan, anggota virtual apa pun dapat diubah menjadi anggota non-virtual.
Tentu saja, kita berbicara keuntungan yang sangat kecil. Saya tidak akan menandai kelas sebagai tersegel hanya untuk mendapatkan peningkatan kinerja kecuali jika profil mengungkapkannya sebagai masalah.
sumber
call
alih-alihcallvirt
... Saya suka jenis referensi non-nol karena banyak alasan lain juga ... sigh :-(Saya menganggap kelas "tersegel" sebagai kasus normal dan saya SELALU punya alasan untuk menghilangkan kata kunci "tersegel".
Alasan terpenting bagi saya adalah:
a) Pemeriksaan waktu kompilasi yang lebih baik (casting ke antarmuka yang tidak diimplementasikan akan terdeteksi pada waktu kompilasi, tidak hanya pada saat runtime)
dan, alasan utama:
b) Pelanggaran kelas saya tidak mungkin seperti itu
Saya berharap Microsoft akan membuat "menyegel" standar, bukan "membuka segel".
sumber
@Vaibhav, tes seperti apa yang Anda lakukan untuk mengukur kinerja?
Saya kira kita harus menggunakan Rotor dan menelusuri ke dalam CLI dan memahami bagaimana kelas yang disegel akan meningkatkan kinerja.
sumber
kelas yang disegel akan setidaknya sedikit lebih cepat, tetapi kadang-kadang bisa lebih cepat ... jika Pengoptimal JIT dapat inline panggilan yang seharusnya panggilan virtual. Jadi, di mana ada metode yang sering disebut cukup kecil untuk diuraikan, pasti mempertimbangkan menyegel kelas.
Namun, alasan terbaik untuk menyegel kelas adalah untuk mengatakan "Saya tidak merancang ini untuk diwarisi dari, jadi saya tidak akan membiarkan Anda terbakar dengan menganggap itu dirancang untuk jadi, dan saya tidak akan untuk membakar diri dengan terkunci ke dalam implementasi karena saya membiarkan Anda berasal darinya. "
Saya tahu beberapa di sini mengatakan mereka membenci kelas tersegel karena mereka ingin kesempatan untuk berasal dari apa pun ... tapi itu SERINGAN bukan pilihan yang paling bisa dipertahankan ... karena mengekspos kelas terhadap derivasi akan mengunci Anda lebih banyak daripada tidak mengekspos semua bahwa. Mirip dengan mengatakan "Saya benci kelas yang memiliki anggota pribadi ... Saya sering tidak dapat membuat kelas melakukan apa yang saya inginkan karena saya tidak memiliki akses." Enkapsulasi itu penting ... penyegelan adalah salah satu bentuk enkapsulasi.
sumber
callvirt
(pemanggilan metode virtual) sebagai contoh metode pada kelas yang disegel, karena ia masih harus melakukan pemeriksaan objek-nol pada mereka. Mengenai inlining, CLR JIT dapat (dan memang) sebaris metode virtual panggilan untuk kelas disegel dan non-disegel ... jadi ya. Pertunjukan adalah mitos.Untuk benar-benar melihatnya, Anda perlu menganalisis codec JIT-Compiled (yang terakhir).
Kode C #
Kode MIL
JIT- Kode yang Dikompilasi
Sementara penciptaan objek adalah sama, instruksi dieksekusi untuk memanggil metode kelas disegel dan diturunkan / basis sedikit berbeda. Setelah memindahkan data ke register atau RAM (instruksi gerak), gunakan metode yang disegel, jalankan perbandingan antara dword ptr [ecx], ecx (instruksi cmp) dan kemudian panggil metode tersebut sementara kelas turunan / basis mengeksekusi langsung metode tersebut. .
Menurut laporan yang ditulis oleh Torbj¨orn Granlund, Latency instruksi dan throughput untuk prosesor AMD dan Intel x86 , kecepatan instruksi berikut dalam Intel Pentium 4 adalah:
Tautan : https://gmplib.org/~tege/x86-timing.pdf
Ini berarti bahwa, idealnya , waktu yang diperlukan untuk memanggil metode tertutup adalah 2 siklus sedangkan waktu yang diperlukan untuk memanggil metode kelas turunan atau dasar adalah 3 siklus.
Optimalisasi dari kompiler telah membuat perbedaan antara kinerja yang disegel dan yang tidak disegel dikategorikan sangat rendah sehingga kita berbicara tentang lingkaran prosesor dan karena alasan ini tidak relevan untuk sebagian besar aplikasi.
sumber
Jalankan kode ini dan Anda akan melihat bahwa kelas yang disegel 2 kali lebih cepat:
output: Kelas Tertutup: 00: 00: 00.1897568 Kelas NonSealed: 00: 00: 00.3826678
sumber