Cukup sering di SO saya mendapati diri saya melakukan benchmarking pada potongan-potongan kecil kode untuk melihat implementasi mana yang tercepat.
Cukup sering saya melihat komentar bahwa kode pembandingan tidak memperhitungkan jitting atau pengumpul sampah.
Saya memiliki fungsi pembandingan sederhana berikut yang perlahan saya kembangkan:
static void Profile(string description, int iterations, Action func) {
// warm up
func();
// clean up
GC.Collect();
var watch = new Stopwatch();
watch.Start();
for (int i = 0; i < iterations; i++) {
func();
}
watch.Stop();
Console.Write(description);
Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
}
Pemakaian:
Profile("a descriptions", how_many_iterations_to_run, () =>
{
// ... code being profiled
});
Apakah penerapan ini memiliki kekurangan? Apakah cukup baik untuk menunjukkan bahwa implementasi X lebih cepat daripada implementasi Y melalui iterasi Z? Dapatkah Anda memikirkan cara untuk meningkatkan ini?
EDIT Cukup jelas bahwa pendekatan berbasis waktu (sebagai lawan dari iterasi), lebih disukai, apakah ada yang memiliki implementasi di mana pemeriksaan waktu tidak mempengaruhi kinerja?
c#
.net
performance
profiling
Sam Saffron
sumber
sumber
Jawaban:
Berikut adalah fungsi yang dimodifikasi: seperti yang direkomendasikan oleh komunitas, silakan ubah ini menjadi wiki komunitas.
Pastikan Anda menyusun dalam Rilis dengan pengoptimalan diaktifkan, dan menjalankan tes di luar Visual Studio . Bagian terakhir ini penting karena JIT menjalankan pengoptimalannya dengan debugger terpasang, bahkan dalam mode Rilis.
sumber
Finalisasi belum tentu selesai sebelum
GC.Collect
dikembalikan. Finalisasi diantrekan dan kemudian dijalankan pada thread terpisah. Utas ini masih bisa aktif selama pengujian Anda, memengaruhi hasil.Jika Anda ingin memastikan bahwa finalisasi telah selesai sebelum memulai pengujian, Anda mungkin ingin memanggil
GC.WaitForPendingFinalizers
, yang akan memblokir hingga antrian finalisasi dihapus:sumber
GC.Collect()
sekali lagi?Collect
adalah untuk memastikan objek yang "diselesaikan" juga dikumpulkan.Jika Anda ingin menghilangkan interaksi GC, Anda mungkin ingin menjalankan panggilan 'pemanasan' setelah panggilan GC.Collect, bukan sebelumnya. Dengan cara itu Anda tahu .NET sudah memiliki cukup memori yang dialokasikan dari OS untuk set kerja fungsi Anda.
Perlu diingat bahwa Anda membuat panggilan metode non-inline untuk setiap iterasi, jadi pastikan Anda membandingkan hal-hal yang Anda uji dengan badan kosong. Anda juga harus menerima bahwa Anda hanya dapat mengatur waktu dengan andal hal-hal yang beberapa kali lebih lama daripada panggilan metode.
Selain itu, tergantung pada jenis hal yang Anda buat profil, Anda mungkin ingin melakukan pengaturan waktu berdasarkan waktu tertentu daripada untuk sejumlah iterasi - ini cenderung mengarah ke angka yang lebih mudah dibandingkan tanpa harus memiliki jangka waktu yang sangat singkat untuk penerapan terbaik dan / atau jangka panjang untuk yang terburuk.
sumber
Saya sama sekali tidak akan melewatkan delegasi:
Kode contoh yang mengarah ke penggunaan penutupan:
Jika Anda tidak mengetahui tentang penutupan, lihat metode ini di .NET Reflector.
sumber
IDisposable
.Saya pikir masalah yang paling sulit diatasi dengan metode benchmarking seperti ini adalah memperhitungkan kasus-kasus edge dan hal-hal yang tidak terduga. Misalnya - "Bagaimana cara kerja dua cuplikan kode di bawah beban CPU yang tinggi / penggunaan jaringan / perontokan disk / dll." Mereka bagus untuk pemeriksaan logika dasar untuk melihat apakah algoritme tertentu bekerja secara signifikan lebih cepat daripada yang lain. Tetapi untuk menguji sebagian besar kinerja kode dengan benar, Anda harus membuat pengujian yang mengukur hambatan spesifik dari kode tertentu itu.
Saya masih mengatakan bahwa menguji blok kecil kode sering kali memiliki sedikit laba atas investasi dan dapat mendorong penggunaan kode yang terlalu rumit daripada kode sederhana yang dapat dipelihara. Menulis kode yang jelas yang dapat dipahami oleh pengembang lain, atau saya sendiri dalam 6 bulan ke depan, dapat memahami dengan cepat akan memiliki lebih banyak manfaat kinerja daripada kode yang sangat dioptimalkan.
sumber
Saya akan menelepon
func()
beberapa kali untuk pemanasan, bukan hanya satu.sumber
Saran untuk peningkatan
Mendeteksi apakah lingkungan eksekusi baik untuk pembandingan (seperti mendeteksi jika debugger terpasang atau jika pengoptimalan jit dinonaktifkan yang akan menghasilkan pengukuran yang salah).
Mengukur bagian-bagian kode secara independen (untuk melihat dengan tepat di mana letak hambatannya).
Mengenai # 1:
Untuk mendeteksi apakah debugger dilampirkan, baca properti
System.Diagnostics.Debugger.IsAttached
(Ingat juga untuk menangani kasus di mana debugger awalnya tidak terpasang, tetapi dilampirkan setelah beberapa waktu).Untuk mendeteksi apakah pengoptimalan jit dinonaktifkan, baca properti
DebuggableAttribute.IsJITOptimizerDisabled
dari rakitan yang relevan:Mengenai # 2:
Ini bisa dilakukan dengan banyak cara. Salah satu caranya adalah dengan mengizinkan beberapa delegasi untuk disuplai dan kemudian mengukur delegasi tersebut secara individu.
Mengenai # 3:
Ini juga dapat dilakukan dengan banyak cara, dan kasus penggunaan yang berbeda akan menuntut solusi yang sangat berbeda. Jika tolok ukur dipanggil secara manual, maka menulis ke konsol mungkin baik-baik saja. Namun jika benchmark dilakukan secara otomatis oleh sistem build, maka menulis ke konsol mungkin tidak begitu baik.
Salah satu cara untuk melakukannya adalah dengan mengembalikan hasil benchmark sebagai objek dengan tipe kuat yang dapat dengan mudah digunakan dalam konteks yang berbeda.
Etimo.Benchmarks
Pendekatan lain adalah dengan menggunakan komponen yang ada untuk melakukan benchmark. Sebenarnya, di perusahaan saya, kami memutuskan untuk merilis alat benchmark kami ke domain publik. Pada intinya, ia mengelola pengumpul sampah, jitter, pemanasan, dll, seperti yang disarankan beberapa jawaban lain di sini. Ia juga memiliki tiga fitur yang saya sarankan di atas. Ini mengelola beberapa masalah yang dibahas dalam blog Eric Lippert .
Ini adalah contoh keluaran dimana dua komponen dibandingkan dan hasilnya dituliskan ke konsol. Dalam hal ini, dua komponen yang dibandingkan disebut 'KeyedCollection' dan 'MultiplyIndexedKeyedCollection':
Ada paket NuGet , contoh paket NuGet, dan kode sumbernya tersedia di GitHub . Ada juga postingan blog .
Jika Anda sedang terburu-buru, saya sarankan Anda mendapatkan paket sampel dan cukup memodifikasi delegasi sampel sesuai kebutuhan. Jika Anda tidak sedang terburu-buru, ada baiknya membaca postingan blog untuk memahami detailnya.
sumber
Anda juga harus menjalankan "pemanasan" sebelum pengukuran sebenarnya untuk mengecualikan waktu yang dihabiskan oleh compiler JIT untuk mengubah kode Anda.
sumber
Bergantung pada kode yang Anda tolok ukur dan platform yang menjalankannya, Anda mungkin perlu memperhitungkan bagaimana penyelarasan kode memengaruhi kinerja . Untuk melakukannya mungkin akan memerlukan pembungkus luar yang menjalankan pengujian beberapa kali (dalam domain aplikasi terpisah atau proses?), Beberapa kali pertama memanggil "kode padding" untuk memaksanya dikompilasi JIT, sehingga menyebabkan kode menjadi dibandingkan untuk disejajarkan secara berbeda. Hasil tes yang lengkap akan memberikan pengaturan waktu kasus terbaik dan terburuk untuk berbagai penyelarasan kode.
sumber
Jika Anda mencoba menghilangkan dampak Pengumpulan Sampah dari benchmark lengkap, apakah itu layak untuk disetel
GCSettings.LatencyMode
?Jika tidak, dan Anda ingin dampak sampah yang dibuat
func
menjadi bagian dari tolok ukur, bukankah sebaiknya Anda juga memaksa pengumpulan di akhir pengujian (di dalam pengatur waktu)?sumber
Masalah mendasar dari pertanyaan Anda adalah asumsi bahwa satu pengukuran dapat menjawab semua pertanyaan Anda. Anda perlu mengukur beberapa kali untuk mendapatkan gambaran situasi yang efektif dan terutama dalam bahasa pengumpulan sampah seperti C #.
Jawaban lain memberikan cara yang baik untuk mengukur kinerja dasar.
Namun, pengukuran tunggal ini tidak memperhitungkan pengumpulan sampah. Profil yang tepat juga menjelaskan kinerja kasus terburuk dari pengumpulan sampah yang tersebar di banyak panggilan (nomor ini tidak berguna karena VM dapat berhenti tanpa pernah mengumpulkan sampah yang tersisa tetapi masih berguna untuk membandingkan dua implementasi yang berbeda dari
func
.)Dan Anda mungkin juga ingin mengukur kinerja kasus terburuk dari pengumpulan sampah untuk metode yang hanya dipanggil sekali.
Tetapi yang lebih penting daripada merekomendasikan pengukuran tambahan yang mungkin spesifik ke profil adalah gagasan bahwa seseorang harus mengukur beberapa statistik yang berbeda dan bukan hanya satu jenis statistik.
sumber