Topik ini membahas profiling (pengukuran performa terhadap program yang berjalan), yaitu kegiatan mengukur terlebih dahulu sebelum mengoptimalkan kode. Pembahasan mencakup pola pengukuran yang dapat diulang, perbandingan dua algoritma lewat jumlah operasinya, tanda kompleksitas kuadratik, serta hotspot (bagian kode yang paling banyak menyita waktu) yang umum ditemui pada kode C#. Setelah topik ini diselesaikan, keputusan optimasi dapat didasarkan pada angka yang terukur dan bukan pada dugaan.
Ukur dahulu, optimalkan kemudian#
Dugaan tentang letak kode yang lambat sering kali keliru, karena bagian yang terlihat berat pada kode sumber bisa saja jarang dijalankan, sedangkan perulangan kecil yang dipanggil beribu kali justru menjadi penyita waktu terbesar. Profiling membalik urutan kerjanya: program diukur dahulu, bagian tersibuk ditentukan dari data, dan optimasi baru dikerjakan pada bagian itu. Angka waktu mentah pun perlu dibaca dengan hati-hati, karena hasilnya bergantung mesin, beban proses lain, dan kompilasi JIT yang berlangsung saat program berjalan.
Perkakas bawaan .NET menangani pengukuran tersebut. Perintah dotnet-counters menampilkan metrik ringkas seperti penggunaan CPU dan memori, perintah dotnet-trace merekam jejak eksekusi untuk dianalisis, sedangkan benchmark (pengukuran berulang dengan aturan statistik) yang serius memakai pustaka khusus seperti BenchmarkDotNet. Untuk pengamatan kasar di dalam kode, kelas Stopwatch mengukur waktu berjalan sebuah blok.
// tidak-dijalankan
using System.Diagnostics;
// Angka waktu yang dihasilkan bergantung mesin, sehingga tidak ditempel sebagai keluaran.
Stopwatch stopwatch = Stopwatch.StartNew();
ProsesPengukuran(); // bagian kode yang sedang diukur
stopwatch.Stop();
Console.WriteLine($"{stopwatch.ElapsedMilliseconds} ms");Satu angka dari Stopwatch hanya petunjuk awal. Nilainya berubah antarjalankan dan antarmesin, sehingga angka waktu tidak pernah dipakai sebagai keluaran yang pasti, dan kesimpulan yang serius membutuhkan pengukuran berulang dari alat benchmark.
Membandingkan algoritma lewat jumlah operasi#
Karena angka waktu bersifat bergantung mesin, perbandingan antaralgoritma membutuhkan satuan yang deterministik. Satuan itu adalah jumlah operasi, misalnya berapa kali dua nilai dibandingkan atau berapa kali sebuah struktur data diperiksa. Dua algoritma yang menyelesaikan pekerjaan yang sama dapat dibandingkan secara adil dengan menghitung operasinya, dan angka tersebut sama di semua komputer. Program berikut menghitung operasi dua cara mendeteksi nilai ganda pada data seribu elemen, yaitu perbandingan berpasangan dan pemeriksaan dengan HashSet<int>.
// Dua algoritma dengan hasil sama, jumlah operasi berbeda jauh.
int[] data = new int[1000];
for (int i = 0; i < data.Length; i++)
{
data[i] = i + 1;
}
data[998] = 1000;
data[999] = 1000; // nilai ganda sengaja diletakkan di posisi terakhir
// cara kuadratik: membandingkan setiap pasangan nilai
int perbandingan = 0;
bool ditemukanKuadratik = false;
for (int i = 0; i < data.Length && !ditemukanKuadratik; i++)
{
for (int j = i + 1; j < data.Length; j++)
{
perbandingan++;
if (data[i] == data[j])
{
ditemukanKuadratik = true;
break;
}
}
}
// cara linear: HashSet mencatat nilai yang sudah terlihat
HashSet<int> terlihat = new HashSet<int>();
int pemeriksaan = 0;
bool ditemukanLinear = false;
foreach (int nilai in data)
{
pemeriksaan++;
if (!terlihat.Add(nilai))
{
ditemukanLinear = true;
break;
}
}
Console.WriteLine($"Panjang data : {data.Length}");
Console.WriteLine($"Hasil cara kuadratik : {ditemukanKuadratik}");
Console.WriteLine($"Perbandingan cara kuadratik : {perbandingan}");
Console.WriteLine($"Hasil cara linear : {ditemukanLinear}");
Console.WriteLine($"Pemeriksaan cara linear : {pemeriksaan}");Panjang data : 1000
Hasil cara kuadratik : True
Perbandingan cara kuadratik : 499500
Hasil cara linear : True
Pemeriksaan cara linear : 1000Kedua cara menemukan nilai ganda yang sama, tetapi jumlah kerjanya berbeda hampir lima ratus kali lipat. Cara kuadratik melakukan 499500 perbandingan karena setiap elemen dibandingkan dengan seluruh elemen setelahnya, sedangkan cara linear hanya melakukan 1000 pemeriksaan karena HashSet<int> menjawab pertanyaan “apakah nilai ini sudah terlihat” dalam satu langkah. Angka-angka itu tidak bergantung pada kecepatan komputer, sehingga perbandingannya dapat dipertahankan di mana pun program dijalankan. Catatan keadilannya, satu pemanggilan Add memang lebih mahal daripada satu perbandingan bilangan bulat, tetapi selisih biaya satuan tidak mungkin menutup jarak 499500 berbanding 1000.
Tanda kompleksitas kuadratik#
Jumlah operasi juga membantu mengenali pola pertumbuhan tanpa mengukur waktu. Perbandingan berpasangan pada data berpanjang melakukan perbandingan, yang termasuk kelas , sedangkan pemeriksaan satu kali per elemen termasuk kelas . Program berikut menghitung jumlah perbandingan berpasangan untuk beberapa panjang data.
// Tanda kuadratik: panjang data digandakan, jumlah operasi naik empat kali lipat.
int[] panjangData = [100, 200, 400, 800];
foreach (int panjang in panjangData)
{
int operasi = panjang * (panjang - 1) / 2;
Console.WriteLine($"panjang {panjang,4} : {operasi,6} perbandingan");
}panjang 100 : 4950 perbandingan
panjang 200 : 19900 perbandingan
panjang 400 : 79800 perbandingan
panjang 800 : 319600 perbandinganTabel keluarannya memperlihatkan aturan praktis yang berguna saat membaca hasil profiling. Apabila panjang data digandakan dan jumlah operasi naik sekitar empat kali lipat, algoritma tersebut kemungkinan besar berada di kelas , dan lipatannya akan terasa pada data yang jauh lebih besar. Algoritma seperti pengurutan bawaan tumbuh jauh lebih lambat, sedangkan tumbuh sebanding dengan datanya. Menyadari pola ini pada tahap perancangan lebih murah daripada menemukannya pada hasil profiling.
Hotspot umum pada kode C##
Hasil profiling pada aplikasi nyata berulang kali menunjuk pada beberapa pola yang sama. Mengenali polanya mempercepat membaca laporan profiling, karena keempat pola berikut sering muncul sebagai hotspot pada kode C#:
- Perangkaian string di dalam perulangan. Setiap operasi
+pada string menyalin seluruh isinya ke string baru, sehingga perulangan seribu kali menyalin data seribu kali. KelasStringBuildermembangun teks pada satu buffer (wilayah memori sementara) dan menjadi pilihan baku untuk perangkaian yang berulang. - Enumerasi ulang kueri LINQ. Kueri LINQ dievaluasi setiap kali dienumerasi, sehingga kueri yang sama yang dipakai tiga kali dihitung tiga kali. Pemanggilan
ToList()atauToArray()sekali di awal lebih murah bila hasilnya dipakai berulang. - Boxing pada jalur panas. Memasukkan nilai bertipe nilai ke dalam variabel
objectatau koleksi non-generik membungkus nilainya di heap, dan boxing yang terjadi di dalam perulangan menimbulkan alokasi berulang. - Alokasi kecil yang terus menerus. Pembuatan objek baru di dalam perulangan mempercepat penuhnya generation termuda GC, sehingga koleksi berjalan lebih sering dan menyita waktu. Menggunakan ulang objek atau buffer mengurangi beban itu.
Keempat pola tersebut tidak selalu salah, karena pada kode yang jarang dijalankan, kerapian kode lebih berharga daripada performanya. Penilaiannya tetap berangkat dari hasil profiling pada jalur kode yang benar-benar sibuk.
Kesalahan umum#
- Mengoptimalkan bagian yang tidak diukur. Optimasi tanpa pengukuran sering memindahkan kompleksitas, bukan menghilangkannya, dan dapat merusak keterbacaan tanpa keuntungan nyata. Ukur dahulu, catat angkanya, lalu bandingkan sebelum dan sesudah perubahan.
- Membaca satu angka
Stopwatchsebagai kesimpulan. Nilainya bergantung mesin dan kondisi proses lain, sehingga satu jalanan tidak cukup. Ulangi pengukuran berkali-kali atau gunakan alat benchmark yang menyediakan aturan statistiknya. - Membandingkan dua algoritma dengan beban yang tidak setara. Menghitung operasi hanya adil apabila kedua sisi menyelesaikan pekerjaan yang sama pada data yang sama. Satu operasi yang mahal juga tidak disamakan dengan satu operasi yang murah, sehingga biaya satuan setiap operasi tetap disebutkan dalam prosa.
Ringkasan#
- Profiling mendahului optimasi karena dugaan letak kode lambat sering keliru, dan hotspot ditentukan dari hasil pengukuran.
- Angka waktu bergantung mesin, sehingga perbandingan antaralgoritma yang adil memakai jumlah operasi sebagai satuan deterministik.
- Panjang data digandakan dan operasi naik empat kali lipat adalah tanda algoritma , sebagaimana 1000 menjadi 499500 perbandingan pada data seribu elemen.
- Hotspot yang sering muncul pada kode C# adalah perangkaian string berulang, enumerasi ulang kueri LINQ, boxing, dan alokasi kecil yang terus menerus.
Referensi#
- Diagnostics tools overview — Microsoft — Daftar perkakas diagnostik .NET untuk profiling dan analisis performa.
- dotnet-counters diagnostic tool — Microsoft — Perintah untuk mengamati metrik performa program .NET secara langsung.
- Stopwatch Class — Microsoft
— Dokumentasi kelas
Stopwatchuntuk pengukuran waktu yang teliti. - BenchmarkDotNet — Pustaka benchmark open source untuk .NET dengan aturan statistik yang ketat.