PRAKTIK TERBAIK
Kode Ramah Kompiler: Kata Kunci Tersegel di .NET C#
Mengapa & Kapan Kata Kunci Tersegel dapat meningkatkan kinerja di .NET C#
Apa artinya menulis kode yang ramah kompiler?
Kode .NET apa pun melewati lebih dari satu fase hingga akhirnya mencapai kode mesin. Karena banyak faktor yang terlibat dalam proses ini, mungkin ada banyak detail yang kita lewatkan saat pertama kali menulis kode.
Namun, semakin jelas dan deterministik kode yang kita tulis, semakin banyak kompiler yang dapat membantu kita dan menghasilkan kode mesin yang dioptimalkan.
Pada artikel ini, kita akan membahas salah satu contoh cara yang dapat kita gunakan untuk membantu compiler mengoptimalkan kode kita. Cara ini adalah; menggunakan Kata Kunci Tersegel.
Cukup dengan pembahasannya dan mari kita lihat contohnya…
Pemeriksaan latar belakang
Jika Anda seorang pengembang .NET, bahkan seorang pemula, Anda harus tahu sekarang bahwa ada kata kunci dalam kerangka .NET yang disebut sealed
.
Kata kunci ini dapat digunakan dalam definisi kelas dan artinya kelas tersebut tidak dapat diwarisi oleh kelas lain mana pun. Ini terlihat seperti ini:
public sealed class MyClass {}
Atau bahkan dalam deklarasi metode dan itu berarti bahwa metode tersebut tidak dapat ditimpa -lagi- oleh metode lain mana pun di kelas anak. Dengan kata lain, ini memutus rangkaian penggantian metode pada tingkat penggunaannya. Ini terlihat seperti ini:
public sealed override void MyMethod() {}
Oleh karena itu, yang dapat kita pahami dari sini adalah ketika kita menggunakan kata kunci sealed
, kita sebenarnya menjanjikan compiler bahwa kita tidak mempunyai niat untuk mewarisi suatu kelas atau mengganti suatu metode.
Karena itu, sekarang mari kita lihat apakah itu berarti apa pun bagi kompiler.
Mari kita mulai dengan beberapa kode dasar yang akan digunakan sepanjang penjelasan.
Kelas Dasar
public class BaseClass { public virtual void DoSomething() { } public void DoSomethingElse() { } }
Ini adalah kelas dasar yang akan kita gunakan sebagai induk teratas.
Di kelas ini, kami telah mendefinisikan anggota berikut:
public virtual void DoSomething()
metode.public void DoSomethingElse()
metode.
Kelasku
public class MyClass : BaseClass { public override void DoSomething() { } }
Ini adalah kelas yang akan mewarisi dari BaseClass
tetapi tanpa menggunakan kata kunci sealed
dalam definisinya.
Di kelas ini, kami mengganti metode DoSomething
yang diwarisi dari kelas induk BaseClass
.
Kelas Tersegel Saya
public sealed class MySealedClass : BaseClass { public override void DoSomething() { } }
Ini adalah kelas yang akan mewarisi dari BaseClass
tetapi kali ini kita menggunakan kata kunci sealed
dalam definisinya.
Di kelas ini, kami mengganti metode DoSomething
yang diwarisi dari kelas induk BaseClass
.
Sekarang, mari kita lanjutkan dan lihat apakah akan ada perbedaan -dari sudut pandang kompiler- antara penggunaan kelas MyClass
dan MySealedClass
.
Memanggil Metode Virtual
Untuk memvalidasi apakah akan ada perbedaan -dari sudut pandang compiler- antara pemanggilan metode virtual pada kelas MyClass
dan MySealedClass
, kita akan membuat proyek Benchmark.
[MemoryDiagnoser(false)] public class Benchmarking { private readonly int NumberOfTrials = 10; private MyClass _myClassObject = new MyClass(); private MySealedClass _mySealedClassObject = new MySealedClass(); [Benchmark] public void CallingVirtualMethodOnMyClass() { for (var i = 0; i < NumberOfTrials; i++) { _myClassObject.DoSomething(); } } [Benchmark] public void CallingVirtualMethodOnMySealedClass() { for (var i = 0; i < NumberOfTrials; i++) { _mySealedClassObject.DoSomething(); } } }
Sekarang, dengan menjalankan proyek Benchmark ini kita akan mendapatkan hasil sebagai berikut.
Seperti yang bisa kita lihat di sini, performa pemanggilan metode virtual pada kelas tersegel jauh lebih baik daripada memanggilnya pada kelas tidak tersegel.
Tapi kenapa?!!! Biarkan aku memberitahu Anda.
Di Kelas Tidak Tersegel
Saat memanggil metode virtual pada objek yang dibuat dari kelas MyClass
, pada saat itu kompiler tidak mengetahui apakah ada kode yang menginisialisasi ulang objek _myClassObject
dengan instance baru dari kelas anak kelas MyClass
atau tidak. Asumsi ini valid karena kelas MyClass
tidak tersegel dan ini berarti dapat diwariskan.
Berdasarkan asumsi tersebut, kompiler tidak dapat memutuskan -pada waktu kompilasi- apakah implementasi sebenarnya dari metode DoSomething
akan menjadi implementasi yang disediakan oleh kelas MyClass
atau kelas turunan lainnya.
Dengan demikian, kompiler akan menulis beberapa instruksi -untuk dieksekusi saat runtime- untuk memeriksa pada saat mengeksekusi metode DoSomething
, implementasi mana yang tepat. Hal ini pasti akan memerlukan lebih banyak pemrosesan dan waktu.
Catatan: seperti yang Anda perhatikan, kompiler akan mencurigai bahwa beberapa kode dapat menginisialisasi ulang objek. Anda mungkin berpikir bahwa menandai bidang sebagai readonly
akan menyelesaikan masalah tetapi sebenarnya tidak karena objek masih dapat diinisialisasi ulang di dalam konstruktor.
Di Kelas Tertutup
Saat memanggil metode virtual pada objek yang dibuat dari kelas MySealedClass
, pada saat itu kompiler tidak mengetahui apakah ada kode yang menginisialisasi ulang objek _mySealedClassObject
dengan instance baru atau tidak. Namun, kompiler yakin bahwa bahkan jika ini terjadi, instance tersebut akan tetap berada di kelas MySealedClass
sebagaimana adanya sealed
dan ini berarti bahwa instance tersebut tidak akan pernah memiliki kelas turunan apa pun.
Berdasarkan hal tersebut, kompiler akan memutuskan -pada waktu kompilasi- implementasi sebenarnya dari metode DoSomething
. Ini pastinya jauh lebih cepat daripada menunggu waktu proses.
Memanggil Metode Non-Virtual
Untuk memvalidasi apakah akan ada perbedaan -dari sudut pandang compiler- antara pemanggilan metode non-virtual pada kelas MyClass
dan MySealedClass
, kita akan membuat proyek Benchmark .
[MemoryDiagnoser(false)] public class Benchmarking { private readonly int NumberOfTrials = 10; private BaseClass _baseClassObject = new BaseClass(); private MyClass _myClassObject = new MyClass(); private MySealedClass _mySealedClassObject = new MySealedClass(); private MyClass[] _myClassObjectsArray = new MyClass[1]; private MySealedClass[] _mySealedClassObjectsArray = new MySealedClass[1]; [Benchmark] public void CallingNonVirtualMethodOnMyClass() { for (var i = 0; i < NumberOfTrials; i++) { _myClassObject.DoSomethingElse(); } } [Benchmark] public void CallingNonVirtualMethodOnMySealedClass() { for (var i = 0; i < NumberOfTrials; i++) { _mySealedClassObject.DoSomethingElse(); } } }
Sekarang, dengan menjalankan proyek Benchmark ini kita akan mendapatkan hasil sebagai berikut.
Seperti yang bisa kita lihat di sini, performa pemanggilan metode non-virtual pada kelas tersegel lebih baik daripada pemanggilan metode non-virtual pada kelas tak tersegel.
Namun, tidak ada bukti ilmiah mengapa hal ini terjadi dan menjalankan kembali proyek benchmark yang sama mungkin akan memberikan hasil yang sebaliknya.
Oleh karena itu, kemungkinan besar perbedaan ini disebabkan oleh kerangka benchmarking itu sendiri karena perbedaannya terlalu kecil sehingga dapat diabaikan.
Pengecekan Tipe
Untuk memvalidasi apakah akan ada perbedaan -dari sudut pandang compiler- antara memeriksa jenis objek menggunakan operator is
pada kelas MyClass
dan MySealedClass
, kita akan membuat Benchmark proyek.
[MemoryDiagnoser(false)] public class Benchmarking { private readonly int NumberOfTrials = 10; private BaseClass _baseClassObject = new BaseClass(); [Benchmark] public bool ObjectTypeIsMyClass() { for (var i = 0; i < NumberOfTrials; i++) { var x = _baseClassObject is MyClass; } return true; } [Benchmark] public bool ObjectTypeIsMySealedClass() { for (var i = 0; i < NumberOfTrials; i++) { var x = _baseClassObject is MySealedClass; } return true; } }
Sekarang, dengan menjalankan proyek Benchmark ini kita akan mendapatkan hasil sebagai berikut.
Seperti yang dapat kita lihat di sini, kinerja pemeriksaan tipe objek pada kelas yang disegel lebih baik daripada memanggilnya pada kelas yang tidak disegel.
Tapi kenapa?!!! Biarkan aku memberitahu Anda.
Di Kelas Tidak Tersegel
Saat memeriksa apakah tipe objeknya adalah kelas MyClass
, kompiler perlu memeriksa apakah objek tersebut bertipe kelas MyClass
atau salah satu kelas turunannya.
Dengan demikian, ini menghasilkan lebih banyak instruksi dan tentunya lebih banyak pemrosesan dan waktu.
Di Kelas Tertutup
Saat memeriksa apakah tipe objeknya adalah kelas MySealedClass
, kompiler perlu memeriksa apakah objek tersebut hanya bertipe kelas MySealedClass
, tidak ada yang lain. Ini karena kelas MySealedClass
disegel dan ini berarti kelas tersebut tidak akan pernah memiliki kelas turunan.
Dengan demikian, hal ini menyebabkan lebih sedikit instruksi dan tentu saja lebih sedikit proses dan waktu.
Tipe Transmisi
Untuk memvalidasi apakah akan ada perbedaan -dari sudut pandang compiler- antara mentransmisikan objek menggunakan operator as
pada kelas MyClass
dan MySealedClass
, kita akan membuat Benchmark proyek.
[MemoryDiagnoser(false)] public class Benchmarking { private readonly int NumberOfTrials = 10; private BaseClass _baseClassObject = new BaseClass(); [Benchmark] public void ObjectTypeAsMyClass() { for (var i = 0; i < NumberOfTrials; i++) { var x = _baseClassObject as MyClass; } } [Benchmark] public void ObjectTypeAsMySealedClass() { for (var i = 0; i < NumberOfTrials; i++) { var x = _baseClassObject as MySealedClass; } } }
Sekarang, dengan menjalankan proyek Benchmark ini kita akan mendapatkan hasil sebagai berikut.
Seperti yang dapat kita lihat di sini, kinerja casting objek pada kelas yang tersegel lebih baik daripada memanggilnya pada kelas yang tidak tersegel.
Tapi kenapa?!!! Biarkan aku memberitahu Anda.
Di Kelas Tidak Tersegel
Saat mentransmisikan objek sebagai kelas MyClass
, kompiler perlu memeriksa apakah objek tersebut bertipe kelas MyClass
atau salah satu kelas turunannya.
Dengan demikian, ini menghasilkan lebih banyak instruksi dan tentunya lebih banyak pemrosesan dan waktu.
Di Kelas Tertutup
Saat mentransmisikan objek sebagai kelas MySealedClass
, kompiler perlu memeriksa apakah objek tersebut hanya bertipe kelas MySealedClass
, tidak ada yang lain. Ini karena kelas MySealedClass
disegel dan ini berarti kelas tersebut tidak akan pernah memiliki kelas turunan.
Dengan demikian, hal ini menyebabkan lebih sedikit instruksi dan tentu saja lebih sedikit proses dan waktu.
Menyimpan Objek Dalam Array
Untuk memvalidasi apakah akan ada perbedaan -dari sudut pandang compiler- antara menyimpan objek dalam arraypada kelas MyClass
dan MySealedClass
, kita akan membuat proyek Benchmark .
[MemoryDiagnoser(false)] public class Benchmarking { private readonly int NumberOfTrials = 10; private MyClass _myClassObject = new MyClass(); private MySealedClass _mySealedClassObject = new MySealedClass(); private MyClass[] _myClassObjectsArray = new MyClass[1]; private MySealedClass[] _mySealedClassObjectsArray = new MySealedClass[1]; [Benchmark] public void StoringValuesInMyClassArray() { for (var i = 0; i < NumberOfTrials; i++) { _myClassObjectsArray[0] = _myClassObject; } } [Benchmark] public void StoringValuesInMySealedClassArray() { for (var i = 0; i < NumberOfTrials; i++) { _mySealedClassObjectsArray[0] = _mySealedClassObject; } } }
Sekarang, dengan menjalankan proyek Benchmark ini kita akan mendapatkan hasil sebagai berikut.
Seperti yang dapat kita lihat di sini, kinerja menyimpan objek dalam array pada kelas yang tersegel lebih baik daripada memanggilnya pada kelas yang tidak tersegel.
Tapi kenapa?!!! Biarkan aku memberitahu Anda.
Sebelum membahas detailnya, izinkan saya mengingatkan Anda terlebih dahulu tentang satu poin penting; array adalah kovarian.
Ini berarti jika kita mendefinisikan kelas-kelas berikut:
public class A {} public class B : A {}
Maka kode berikut akan valid:
A[] arrayOfA = new B[5];
Selain itu, kita dapat menyetel item di dalam arrayOfA
ke instance B
sebagai berikut:
arrayOfA[0] = new B();
Karena itu, sekarang mari kita lanjutkan ke topik utama kita.
Di Kelas Tidak Tersegel
Saat mengatur item di dalam array _myClassObjectsArray
, kompiler perlu memeriksa apakah instance _myClassObject
yang kita gunakan bertipe kelas MyClass
atau salah satu kelas turunannya.
Dengan demikian, ini menghasilkan lebih banyak instruksi dan tentunya lebih banyak pemrosesan dan waktu.
Di Kelas Tertutup
Saat mengatur item di dalam array _mySealedClassObjectsArray
, kompiler perlu memeriksa apakah instance _mySealedClassObject
yang kita gunakan hanya bertipe kelas MySealedClass
, tidak ada yang lain. Ini karena kelas MySealedClass
disegel dan ini berarti kelas tersebut tidak akan pernah memiliki kelas turunan.
Dengan demikian, hal ini menyebabkan lebih sedikit instruksi dan tentu saja lebih sedikit proses dan waktu.
Deteksi Kegagalan Dini
Selain peningkatan kinerja yang dapat kita peroleh dengan menggunakan kata kunci sealed
, kita juga dapat menghindari beberapa kegagalan runtime. Mari saya tunjukkan sebuah contoh.
Jika kita menulis kode berikut:
public void Run(MyClass obj) { _ = _baseClassObject as IMyInterface; }
Kompiler -pada waktu desain- tidak akan menampilkan peringatan atau kesalahan apa pun karena sebenarnya obj
dapat bertipe kelas MyClass
atau salah satu kelas turunannya. Oleh karena itu, kompiler perlu menunggu waktu proses untuk melakukan pemeriksaan terakhir.
Yang pasti, jika pada saat runtime tipe obj
yang sebenarnya ternyata tidak mengimplementasikan IMyInterface
, hal ini akan menyebabkan pengecualian runtime.
Namun jika kita menulis kode berikut:
public void Run(MySealedClass obj) { _ = _baseClassObject as IMyInterface; }
Kompiler akan menampilkan kesalahan (CS0039) pada waktu desain karena obj
hanya dapat bertipe kelas MySealedClass
, tidak ada yang lain. Oleh karena itu, kompiler dapat langsung memeriksa apakah kelas MySealedClass
mengimplementasikan IMyInterface
atau tidak.
Jadi, ini berarti bahwa penggunaan kata kunci yang disegel memungkinkan kompiler melakukan perubahan statis yang tepat pada waktu desain.
Pikiran Terakhir
Saya selalu merekomendasikan penggunaan kata kunci yang disegel bila memungkinkan.
Hal ini bukan hanya untuk peningkatan kinerja yang mungkin Anda peroleh tetapi juga karena ini merupakan praktik terbaik dari sudut pandang desain sejauh Microsoft benar-benar berpikir untuk membuat semua kelas disegel secara default.
Akhirnya, saya harap Anda menikmati membaca artikel ini seperti saya menikmati menulisnya.
Semoga konten ini bermanfaat bagi Anda. Jika Anda ingin mendukung:
▶ Jika Anda belum menjadi anggota Medium, Anda dapat menggunakan tautan referensi saya sehingga saya dapat memperoleh sebagian biaya Anda dari Medium, Anda tidak perlu membayar tambahan apa pun.
▶ Berlangganan Buletin saya untuk mendapatkan praktik terbaik, tutorial, petunjuk, kiat, dan banyak hal keren lainnya langsung ke kotak masuk Anda.
Sumber Daya Lainnya
Ini adalah sumber daya lain yang mungkin berguna bagi Anda.