Apakah kelas tertutup benar-benar menawarkan Manfaat kinerja?


140

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 :)


9
Saya tidak berpikir kelas tertutup dimaksudkan untuk memberikan peningkatan kinerja. Fakta yang mereka lakukan mungkin bersifat insidentil. Selain itu, buat profil aplikasi Anda setelah Anda refactored untuk menggunakan kelas yang disegel, dan tentukan apakah itu sepadan dengan usaha. Mengunci ekstensibilitas Anda untuk membuat optimasi mikro yang tidak dibutuhkan akan membebani Anda dalam jangka panjang. Tentu saja, jika Anda membuat profil, dan itu memungkinkan Anda mencapai tolok ukur perf Anda (bukan perf untuk kepentingan perf), maka Anda dapat membuat keputusan sebagai sebuah tim jika bernilai uang yang dihabiskan. Jika Anda memiliki kelas yang disegel karena alasan non-perf, maka pertahankan mereka :)
Merlyn Morgan-Graham

1
Sudahkah Anda mencoba dengan refleksi? Saya membaca di suatu tempat bahwa instantiating oleh refleksi lebih cepat dengan kelas tertutup
langsung

Setahu saya tidak ada. Disegel ada untuk alasan yang berbeda - untuk memblokir ekstensibilitas, yang mungkin berguna / dibutuhkan dalam banyak kasus. Optimalisasi kinerja bukanlah tujuan di sini.
TomTom

... tetapi jika Anda berpikir tentang kompiler: jika kelas Anda disegel, Anda tahu alamat metode yang Anda panggil di kelas Anda pada waktu kompilasi. Jika kelas Anda tidak disegel, Anda harus menyelesaikan metode ini saat run-time karena Anda mungkin perlu memanggil override. Itu pasti akan diabaikan, tetapi saya bisa melihat bahwa akan ada beberapa perbedaan.
Dan Puzey

Ya, tapi hal semacam itu tidak berarti manfaat nyata, seperti yang ditunjukkan OP. Perbedaan arsitektur adalah / mungkin jauh lebih relevan.
TomTom

Jawaban:


60

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


2
tautan 'bertele-tele' menarik karena kedengarannya seperti kebaikan teknis tetapi sebenarnya omong kosong. Baca komentar di artikel untuk info lebih lanjut. Ringkasan: 3 alasan yang diberikan adalah Versi, Kinerja, dan Keamanan / Prediktabilitas - [lihat komentar berikutnya]
Steven A. Lowe

1
[lanjutan] Versi hanya berlaku ketika tidak ada subkelas, ya, tapi perluas argumen ini untuk setiap kelas dan tiba-tiba Anda tidak memiliki warisan dan coba tebak, bahasa tidak lagi berorientasi objek (tetapi hanya berbasis objek)! [lihat selanjutnya]
Steven A. Lowe

3
[lanjutan] contoh kinerja adalah lelucon: mengoptimalkan panggilan metode virtual; mengapa kelas yang disegel memiliki metode virtual di tempat pertama karena tidak dapat disubklasifikasikan? Akhirnya, argumen Keamanan / Prediktabilitas jelas bodoh: 'Anda tidak dapat menggunakannya sehingga aman / dapat diprediksi'. LOL!
Steven A. Lowe

16
@ Seven A. Lowe - Saya pikir apa yang Jeffrey Richter coba katakan dengan cara yang agak bundar adalah bahwa jika Anda meninggalkan kelas Anda tanpa segel, Anda perlu memikirkan bagaimana kelas turunan dapat / akan menggunakannya, dan jika Anda tidak memiliki waktu atau kecenderungan untuk melakukan ini dengan benar, kemudian segel karena kemungkinan kecil akan menyebabkan perubahan pada kode orang lain di masa mendatang. Itu sama sekali bukan omong kosong, itu akal sehat yang baik.
Greg Beech

6
Kelas yang disegel mungkin memiliki metode virtual karena mungkin berasal dari kelas yang menyatakannya. Ketika Anda kemudian, kemudian, mendeklarasikan variabel dari kelas turunan tersegel, dan memanggil metode itu, kompilator mungkin memancarkan panggilan langsung ke implementasi yang diketahui, karena ia tahu tidak ada cara bahwa ini mungkin berbeda dari apa yang ada di yang dikenal- at-compile-time vtable untuk kelas itu. Adapun disegel / tidak disegel, itu diskusi yang berbeda, dan saya setuju dengan alasan untuk membuat kelas disegel secara default.
Lasse V. Karlsen

143

Jawabannya adalah tidak, kelas yang disegel tidak berkinerja lebih baik daripada yang tidak tersegel.

Masalahnya muncul pada callvs callvirtkode op IL. Calllebih cepat daripada callvirt, dan callvirtterutama digunakan ketika Anda tidak tahu apakah objek telah subkelas. Jadi orang berasumsi bahwa jika Anda menyegel sebuah kelas, semua kode op akan berubah dari calvirtsmenjadi callsdan akan lebih cepat.

Sayangnya callvirtmelakukan 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 a callvirtdiperlukan. Anda dapat menyiasatinya (tanpa perlu menyegel kelas), tetapi itu menjadi sedikit sia-sia.

Structs digunakan callkarena mereka tidak dapat disubklasifikasikan dan tidak pernah null.

Lihat pertanyaan ini untuk informasi lebih lanjut:

Panggilan dan panggilan telepon


5
AFAIK, keadaan di mana calldigunakan adalah: dalam situasi new T().Method(), untuk structmetode, untuk panggilan non-virtual ke virtualmetode (seperti base.Virtual()), atau untuk staticmetode. Di tempat lain menggunakan callvirt.
porges

1
Uhh ... Saya menyadari ini sudah tua, tapi ini tidak benar ... kemenangan besar dengan kelas tersegel adalah ketika Pengoptimal JIT dapat menyambungkan panggilan ... dalam hal itu, kelas tersegel dapat menjadi kemenangan besar .
Brian Kennedy

5
Kenapa jawaban ini salah. Dari Mono changelog: "Optimalisasi devirtualisasi untuk kelas dan metode yang disegel, meningkatkan kinerja pystone IronPython 2.0 sebesar 4%. Program lain dapat mengharapkan peningkatan serupa [Rodrigo].". Kelas tertutup dapat meningkatkan kinerja, tetapi seperti biasa, itu tergantung pada situasi.
Smilediver

1
@ Smilediver Ini dapat meningkatkan kinerja, tetapi hanya jika Anda memiliki JIT yang buruk (tidak tahu seberapa bagus. JIT NET saat ini - dulu cukup buruk dalam hal itu). Hotspot misalnya akan inline panggilan virtual dan deoptimisasi nanti jika perlu - maka Anda membayar overhead tambahan hanya jika Anda benar-benar subkelas kelas (dan itupun belum tentu).
Voo

1
-1 JIT tidak harus menghasilkan kode mesin yang sama untuk opcode IL yang sama. Pemeriksaan kosong dan panggilan virtual adalah langkah-langkah panggilan terpisah dari orthogonal dan terpisah. Dalam hal tipe tertutup, kompiler JIT masih dapat mengoptimalkan bagian dari callvirt. Hal yang sama berlaku ketika kompiler JIT dapat menjamin bahwa referensi tidak akan nol.
dibuka kembali pada

29

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.

public class NormalClass {
    public void WriteIt(string x) {
        Console.WriteLine("NormalClass");
        Console.WriteLine(x);
    }
}

public sealed class SealedClass {
    public void WriteIt(string x) {
        Console.WriteLine("SealedClass");
        Console.WriteLine(x);
    }
}

public static void CallNormal() {
    var n = new NormalClass();
    n.WriteIt("a string");
}

public static void CallSealed() {
    var n = new SealedClass();
    n.WriteIt("a string");
}

Dalam semua kasus, kompiler C # (Visual studio 2010 dalam konfigurasi build Rilis) memancarkan MSIL identik, yaitu sebagai berikut:

L_0000: newobj instance void <NormalClass or SealedClass>::.ctor()
L_0005: stloc.0 
L_0006: ldloc.0 
L_0007: ldstr "a string"
L_000c: callvirt instance void <NormalClass or SealedClass>::WriteIt(string)
L_0011: ret 

Alasan yang sering dikutip bahwa orang mengatakan disegel memberikan manfaat kinerja adalah bahwa kompiler tahu kelas tidak diganti, dan dengan demikian dapat menggunakan callalih-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

//            var n = new NormalClass();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  sub         esp,8 
00000006  cmp         dword ptr ds:[00585314h],0 
0000000d  je          00000014 
0000000f  call        70032C33 
00000014  xor         edx,edx 
00000016  mov         dword ptr [ebp-4],edx 
00000019  mov         ecx,588230h 
0000001e  call        FFEEEBC0 
00000023  mov         dword ptr [ebp-8],eax 
00000026  mov         ecx,dword ptr [ebp-8] 
00000029  call        dword ptr ds:[00588260h] 
0000002f  mov         eax,dword ptr [ebp-8] 
00000032  mov         dword ptr [ebp-4],eax 
//            n.WriteIt("a string");
00000035  mov         edx,dword ptr ds:[033220DCh] 
0000003b  mov         ecx,dword ptr [ebp-4] 
0000003e  cmp         dword ptr [ecx],ecx 
00000040  call        dword ptr ds:[0058827Ch] 
//        }
00000046  nop 
00000047  mov         esp,ebp 
00000049  pop         ebp 
0000004a  ret 

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 WriteItmetode 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:

Normal JIT generated code
Begin 003c00b0, size 39
003c00b0 55              push    ebp
003c00b1 8bec            mov     ebp,esp
003c00b3 b994391800      mov     ecx,183994h (MT: ScratchConsoleApplicationFX4.NormalClass)
003c00b8 e8631fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c00bd e80e70106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00c2 8bc8            mov     ecx,eax
003c00c4 8b1530203003    mov     edx,dword ptr ds:[3302030h] ("NormalClass")
003c00ca 8b01            mov     eax,dword ptr [ecx]
003c00cc 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00cf ff5010          call    dword ptr [eax+10h]
003c00d2 e8f96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00d7 8bc8            mov     ecx,eax
003c00d9 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c00df 8b01            mov     eax,dword ptr [ecx]
003c00e1 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00e4 ff5010          call    dword ptr [eax+10h]
003c00e7 5d              pop     ebp
003c00e8 c3              ret

Vs kelas tersegel:

Normal JIT generated code
Begin 003c0100, size 39
003c0100 55              push    ebp
003c0101 8bec            mov     ebp,esp
003c0103 b90c3a1800      mov     ecx,183A0Ch (MT: ScratchConsoleApplicationFX4.SealedClass)
003c0108 e8131fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c010d e8be6f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0112 8bc8            mov     ecx,eax
003c0114 8b1538203003    mov     edx,dword ptr ds:[3302038h] ("SealedClass")
003c011a 8b01            mov     eax,dword ptr [ecx]
003c011c 8b403c          mov     eax,dword ptr [eax+3Ch]
003c011f ff5010          call    dword ptr [eax+10h]
003c0122 e8a96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0127 8bc8            mov     ecx,eax
003c0129 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c012f 8b01            mov     eax,dword ptr [ecx]
003c0131 8b403c          mov     eax,dword ptr [eax+3Ch]
003c0134 ff5010          call    dword ptr [eax+10h]
003c0137 5d              pop     ebp
003c0138 c3              ret

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 :-)


Ada alasan mengapa Anda memposting jawaban ini dua kali alih-alih menutup yang lain sebagai duplikat? Mereka terlihat seperti duplikat yang valid bagi saya meskipun ini bukan area saya.
Flexo

1
Bagaimana jika metodenya lebih panjang (lebih dari 32 byte kode IL) untuk menghindari inlining dan melihat operasi panggilan apa yang akan digunakan? Jika itu sebaris, Anda tidak melihat panggilan jadi tidak bisa menilai efek.
ygoe

Saya bingung: mengapa ada callvirtketika metode ini tidak virtual untuk memulai?
freakish

@freakish Saya tidak ingat di mana saya melihat ini, tapi saya membaca bahwa CLR digunakan callvirtuntuk metode yang disegel karena masih harus memeriksa objek sebelum memohon pemanggilan metode, dan begitu Anda memperhitungkannya, Anda mungkin juga hanya perlu gunakan callvirt. Untuk menghapus callvirtdan 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)
Orion Edwards

1
Props untuk pergi ke upaya untuk menggali sampai ke tingkat kode mesin, tapi sangat hati-hati membuat pernyataan seperti 'ini memberikan bukti kuat bahwa ada tidak bisa menjadi perbaikan kinerja'. Apa yang telah Anda tunjukkan adalah bahwa untuk satu skenario tertentu tidak ada perbedaan dalam output asli. Ini adalah satu titik data, dan seseorang tidak dapat menganggapnya umum untuk semua skenario. Sebagai permulaan, kelas Anda tidak mendefinisikan metode virtual, jadi panggilan virtual tidak diperlukan sama sekali.
Matt Craig

24

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.

// Value of `v` is unknown,
// and can be resolved only at runtime.
// CPU cannot know which code to prefetch.
// Therefore, just prefetch any one of a() or b().
// This is *speculative execution*.
int v = random();
if (v==1) a();
else b();

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 dari sealed.


Jawaban ini ( Mengapa lebih cepat memproses array yang diurutkan dari array yang tidak disortir? ) Menggambarkan prediksi cabang jauh lebih baik.


1
CPU kelas Pentium mengambil pengiriman langsung secara tidak langsung. Kadang-kadang pengalihan fungsi pointer lebih cepat daripada jika (tidak dapat dilakukan) karena alasan ini.
Joshua

2
Satu keuntungan dari fungsi non virtual atau disegel adalah mereka dapat digarisbawahi dalam lebih banyak situasi.
CodesInChaos

4

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


35
Saya tidak menyegel kelas saya karena alasan kinerja. Saya menyegelnya untuk alasan desain. Merancang warisan adalah sulit, dan upaya itu akan sia-sia sebagian besar waktu. Saya sepenuhnya setuju tentang menyediakan antarmuka - itu solusi yang jauh lebih unggul untuk membuka segel kelas.
Jon Skeet

6
Enkapsulasi umumnya merupakan solusi yang lebih baik daripada pewarisan. Untuk mengambil contoh utas spesifik Anda, menjebak pengecualian utas mendobrak Prinsip Pergantian Liskov karena Anda telah mengubah perilaku terdokumentasi dari kelas Utas, jadi meskipun Anda dapat mengambil darinya, tidak masuk akal untuk mengatakan bahwa Anda dapat menggunakan SafeThread di mana saja Anda bisa menggunakan Utas. Dalam hal ini, Anda akan lebih baik untuk merangkum Thread ke kelas lain yang memiliki perilaku terdokumentasi berbeda, yang dapat Anda lakukan. Terkadang segala sesuatu disegel untuk kebaikan Anda sendiri.
Greg Beech

1
@ [Greg Beech]: opini, bukan fakta - bisa mewarisi dari Thread untuk memperbaiki kekhilafan dalam desainnya BUKAN hal yang buruk ;-) Dan saya pikir Anda melebih-lebihkan LSP - properti provabable q (x) di kasus ini adalah 'perkecualian yang tidak ditangani menghancurkan program' yang bukan merupakan "properti yang diinginkan" :-)
Steven A. Lowe

1
Tidak, tetapi saya sudah memiliki bagian kode jelek di mana saya memungkinkan pengembang lain untuk menyalahgunakan barang-barang saya dengan tidak menyegelnya atau dengan membiarkan kasus sudut. Sebagian besar kode saya saat ini adalah pernyataan dan hal-hal terkait kontrak lainnya. Dan aku cukup terbuka tentang fakta bahwa aku melakukan ini hanya untuk menyebalkan.
Turing Lengkap

2
Karena kita sedang melakukan off-topik rants di sini, hanya seperti Anda benci kelas disegel, aku benci pengecualian tertelan. Tidak ada yang lebih buruk daripada ketika sesuatu berjalan titsup tetapi program terus berjalan. JavaScript adalah favorit saya. Anda membuat perubahan pada beberapa kode dan tiba-tiba mengklik tombol tidak melakukan apa-apa. Bagus! ASP.NET dan UpdatePanel adalah satu lagi; serius, jika handler tombol saya melempar, itu adalah masalah besar dan perlu di-CRASH, jadi saya tahu ada sesuatu yang perlu diperbaiki! Sebuah tombol yang tidak melakukan apa-apa lebih berguna daripada sebuah tombol yang menampilkan layar crash!
Roman Starkov

4

Menandai kelas sealedseharusnya tidak memiliki dampak kinerja.

Ada beberapa kasus di mana cscmungkin harus memancarkan callvirtopcode alih-alih callopcode. Namun, sepertinya kasus-kasus itu jarang terjadi.

Dan sepertinya bagi saya bahwa JIT harus dapat memancarkan panggilan fungsi non-virtual yang sama untuk callvirtitu call, 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.)


3

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.


Mereka seharusnya , tetapi tampaknya tidak. Jika CLR memiliki konsep tipe referensi non-nol, maka kelas yang disegel benar-benar akan lebih baik, karena kompiler dapat memancarkan callalih-alih callvirt... Saya suka jenis referensi non-nol karena banyak alasan lain juga ... sigh :-(
Orion Edwards

3

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".


Saya percaya "menghilangkan" (untuk tidak) harus "memancarkan" (untuk menghasilkan)?
user2864740

2

@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.

SSCLI (Rotor)
SSCLI: Sumber Bersama Infrastruktur Bahasa Umum

Common Language Infrastructure (CLI) adalah standar ECMA yang menjelaskan inti dari .NET Framework. Shared Source CLI (SSCLI), juga dikenal sebagai Rotor, adalah arsip terkompresi dari kode sumber untuk implementasi ECI CLI dan spesifikasi bahasa ECMA C # yang bekerja, teknologi di jantung arsitektur Microsoft .NET.


Tes termasuk membuat hirarki kelas, dengan beberapa metode yang melakukan kerja dummy (manipulasi string kebanyakan). Beberapa metode ini virtual. Mereka saling menelepon di sana-sini. Kemudian memanggil metode ini 100, 10.000, dan 100000 kali ... dan mengukur waktu yang telah berlalu. Kemudian jalankan ini setelah menandai kelas sebagai disegel. dan mengukur lagi. Tidak ada perbedaan pada mereka.
Vaibhav

2

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.


Kompiler C # akan tetap menggunakan 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.
Orion Edwards

1

Untuk benar-benar melihatnya, Anda perlu menganalisis codec JIT-Compiled (yang terakhir).

Kode C #

public sealed class Sealed
{
    public string Message { get; set; }
    public void DoStuff() { }
}
public class Derived : Base
{
    public sealed override void DoStuff() { }
}
public class Base
{
    public string Message { get; set; }
    public virtual void DoStuff() { }
}
static void Main()
{
    Sealed sealedClass = new Sealed();
    sealedClass.DoStuff();
    Derived derivedClass = new Derived();
    derivedClass.DoStuff();
    Base BaseClass = new Base();
    BaseClass.DoStuff();
}

Kode MIL

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       41 (0x29)
  .maxstack  8
  IL_0000:  newobj     instance void ConsoleApp1.Program/Sealed::.ctor()
  IL_0005:  callvirt   instance void ConsoleApp1.Program/Sealed::DoStuff()
  IL_000a:  newobj     instance void ConsoleApp1.Program/Derived::.ctor()
  IL_000f:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0014:  newobj     instance void ConsoleApp1.Program/Base::.ctor()
  IL_0019:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0028:  ret
} // end of method Program::Main

JIT- Kode yang Dikompilasi

--- C:\Users\Ivan Porta\source\repos\ConsoleApp1\Program.cs --------------------
        {
0066084A  in          al,dx  
0066084B  push        edi  
0066084C  push        esi  
0066084D  push        ebx  
0066084E  sub         esp,4Ch  
00660851  lea         edi,[ebp-58h]  
00660854  mov         ecx,13h  
00660859  xor         eax,eax  
0066085B  rep stos    dword ptr es:[edi]  
0066085D  cmp         dword ptr ds:[5842F0h],0  
00660864  je          0066086B  
00660866  call        744CFAD0  
0066086B  xor         edx,edx  
0066086D  mov         dword ptr [ebp-3Ch],edx  
00660870  xor         edx,edx  
00660872  mov         dword ptr [ebp-48h],edx  
00660875  xor         edx,edx  
00660877  mov         dword ptr [ebp-44h],edx  
0066087A  xor         edx,edx  
0066087C  mov         dword ptr [ebp-40h],edx  
0066087F  nop  
            Sealed sealedClass = new Sealed();
00660880  mov         ecx,584E1Ch  
00660885  call        005730F4  
0066088A  mov         dword ptr [ebp-4Ch],eax  
0066088D  mov         ecx,dword ptr [ebp-4Ch]  
00660890  call        00660468  
00660895  mov         eax,dword ptr [ebp-4Ch]  
00660898  mov         dword ptr [ebp-3Ch],eax  
            sealedClass.DoStuff();
0066089B  mov         ecx,dword ptr [ebp-3Ch]  
0066089E  cmp         dword ptr [ecx],ecx  
006608A0  call        00660460  
006608A5  nop  
            Derived derivedClass = new Derived();
006608A6  mov         ecx,584F3Ch  
006608AB  call        005730F4  
006608B0  mov         dword ptr [ebp-50h],eax  
006608B3  mov         ecx,dword ptr [ebp-50h]  
006608B6  call        006604A8  
006608BB  mov         eax,dword ptr [ebp-50h]  
006608BE  mov         dword ptr [ebp-40h],eax  
            derivedClass.DoStuff();
006608C1  mov         ecx,dword ptr [ebp-40h]  
006608C4  mov         eax,dword ptr [ecx]  
006608C6  mov         eax,dword ptr [eax+28h]  
006608C9  call        dword ptr [eax+10h]  
006608CC  nop  
            Base BaseClass = new Base();
006608CD  mov         ecx,584EC0h  
006608D2  call        005730F4  
006608D7  mov         dword ptr [ebp-54h],eax  
006608DA  mov         ecx,dword ptr [ebp-54h]  
006608DD  call        00660490  
006608E2  mov         eax,dword ptr [ebp-54h]  
006608E5  mov         dword ptr [ebp-44h],eax  
            BaseClass.DoStuff();
006608E8  mov         ecx,dword ptr [ebp-44h]  
006608EB  mov         eax,dword ptr [ecx]  
006608ED  mov         eax,dword ptr [eax+28h]  
006608F0  call        dword ptr [eax+10h]  
006608F3  nop  
        }
0066091A  nop  
0066091B  lea         esp,[ebp-0Ch]  
0066091E  pop         ebx  
0066091F  pop         esi  
00660920  pop         edi  
00660921  pop         ebp  

00660922  ret  

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:

  • mov : memiliki 1 siklus sebagai latensi dan prosesor dapat mempertahankan 2,5 instruksi per siklus jenis ini
  • cmp : memiliki 1 siklus sebagai latensi dan prosesor dapat mempertahankan 2 instruksi per siklus jenis ini

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.


-10

Jalankan kode ini dan Anda akan melihat bahwa kelas yang disegel 2 kali lebih cepat:

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new SealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("Sealed class : {0}", watch.Elapsed.ToString());

        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new NonSealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("NonSealed class : {0}", watch.Elapsed.ToString());

        Console.ReadKey();
    }
}

sealed class SealedClass
{
    public string GetName()
    {
        return "SealedClass";
    }
}

class NonSealedClass
{
    public string GetName()
    {
        return "NonSealedClass";
    }
}

output: Kelas Tertutup: 00: 00: 00.1897568 Kelas NonSealed: 00: 00: 00.3826678


9
Ada beberapa masalah dengan ini. Pertama, Anda tidak mengatur ulang stopwatch antara pengujian pertama dan kedua. Kedua, cara Anda memanggil metode berarti bahwa semua kode op akan berupa panggilan bukan panggilan sehingga jenisnya tidak masalah.
Cameron MacFarland

1
RuslanG, Anda lupa memanggil watch.Reset () setelah menjalankan tes pertama. o_O:]

Ya, kode di atas memiliki bug. Kemudian lagi, siapa yang dapat membual tentang diri sendiri untuk tidak pernah memperkenalkan kesalahan dalam kode? Tetapi jawaban ini memiliki satu hal penting yang lebih baik daripada yang lainnya: Jawaban ini mencoba mengukur efek yang ditanyakan. Dan jawaban ini juga membagikan kode untuk kalian semua yang memeriksa dan [secara massal] downvote (saya bertanya-tanya mengapa perlunya downvoting secara massal?). Berbeda dengan jawaban lain. Itu layak dihormati menurut pendapat saya ... Juga harap dicatat bahwa pengguna tersebut adalah pemula, jadi pendekatan yang sangat tidak ramah juga. Beberapa perbaikan sederhana dan kode ini menjadi berguna bagi kita semua. Perbaiki + bagikan versi tetap, jika Anda berani
Roland Pihlakas

Saya tidak setuju dengan @RolandPihlakas. Seperti yang Anda katakan, "siapa yang bisa membual tentang diri sendiri untuk tidak pernah memperkenalkan kesalahan dalam kode". Jawab -> Tidak, tidak ada. Tapi bukan itu intinya. Intinya, ini adalah informasi yang salah yang dapat menyesatkan programmer baru lainnya. Banyak orang dapat dengan mudah kehilangan bahwa stopwatch tidak disetel ulang. Mereka dapat percaya bahwa informasi tolok ukur itu benar. Bukankah itu lebih berbahaya? Seseorang yang dengan cepat mencari jawaban mungkin bahkan tidak membaca komentar-komentar ini, tetapi dia akan melihat jawaban dan dia mungkin percaya itu.
Emran Hussain
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.