Pengembang go yang telah menulis tolok ukur menggunakan testing
Paket mungkin telah menemukan beberapa jebakannya. GO 1.24 memperkenalkan cara baru untuk menulis tolok ukur yang mudah digunakan, tetapi pada saat yang sama jauh lebih kuat: testing.B.Loop
.
Secara tradisional, tolok ukur pergi ditulis menggunakan loop dari 0 ke b.N
:
func Benchmark(b *testing.B) {
for range b.N {
... code to measure ...
}
}
Menggunakan b.Loop
Sebaliknya adalah perubahan sepele:
func Benchmark(b *testing.B) {
for b.Loop() {
... code to measure ...
}
}
testing.B.Loop
memiliki banyak manfaat:
- Ini mencegah optimasi kompiler yang tidak diinginkan dalam loop benchmark.
- Secara otomatis mengecualikan kode pengaturan dan pembersihan dari waktu tolok ukur.
- Kode tidak dapat secara tidak sengaja bergantung pada jumlah total iterasi atau iterasi saat ini.
Ini semua adalah kesalahan mudah untuk dilakukan b.N
-Sato tolok ukur gaya yang secara diam -diam menghasilkan hasil benchmark palsu. Sebagai bonus tambahan, b.Loop
-Sato tolok ukur gaya bahkan lengkap dalam waktu yang lebih singkat!
Mari kita jelajahi keuntungannya testing.B.Loop
dan bagaimana menggunakannya secara efektif.
Masalah Loop Benchmark Lama
Sebelum GO 1.24, sementara struktur dasar tolok ukur sederhana, tolok ukur yang lebih canggih membutuhkan lebih banyak perawatan:
func Benchmark(b *testing.B) {
... setup ...
b.ResetTimer() // if setup may be expensive
for range b.N {
... code to measure ...
... use sinks or accumulation to prevent dead-code elimination ...
}
b.StopTimer() // if cleanup or reporting may be expensive
... cleanup ...
... report ...
}
Jika pengaturan atau pembersihan tidak sepele, pengembang perlu mengelilingi loop benchmark dengan ResetTimer
dan/atau StopTimer
panggilan. Ini mudah dilupakan, dan bahkan jika pengembang ingat mereka mungkin diperlukan, mungkin sulit untuk menilai apakah pengaturan atau pembersihan “cukup mahal” untuk memerlukannya.
Tanpa ini, testing
Paket hanya dapat mengatur waktu seluruh fungsi benchmark. Jika fungsi benchmark menghilangkannya, kode pengaturan dan pembersihan akan dimasukkan dalam pengukuran waktu keseluruhan, secara diam -diam memiringkan hasil benchmark akhir.
Ada perangkap lain yang lebih halus yang membutuhkan pemahaman yang lebih dalam: (sumber contoh)
func isCond(b byte) bool {
if b%3 == 1 && b%7 == 2 && b%17 == 11 && b%31 == 9 {
return true
}
return false
}
func BenchmarkIsCondWrong(b *testing.B) {
for range b.N {
isCond(201)
}
}
Dalam contoh ini, pengguna mungkin mengamati isCond
mengeksekusi dalam waktu sub-nanosecond. CPU cepat, tapi tidak secepat itu! Hasil yang tampaknya anomali ini berasal dari fakta bahwa isCond
Dikatakan, dan karena hasilnya tidak pernah digunakan, kompiler menghilangkannya sebagai kode mati. Akibatnya, tolok ukur ini tidak mengukur isCond
sama sekali; Ini mengukur berapa lama waktu yang dibutuhkan untuk tidak melakukan apa pun.
Dalam hal ini, hasil sub-nanosecond adalah bendera merah yang jelas, tetapi dalam tolok ukur yang lebih kompleks, eliminasi kode mati parsial dapat menyebabkan hasil yang terlihat masuk akal tetapi masih tidak mengukur apa yang dimaksudkan.
Bagaimana testing.B.Loop
Membantu
Tidak seperti b.N
-Tentang Benchmark, testing.B.Loop
mampu melacak ketika pertama kali dipanggil dalam tolok ukur ketika iterasi akhir berakhir. Itu b.ResetTimer
di awal loop dan b.StopTimer
pada akhirnya diintegrasikan ke dalam testing.B.Loop
menghilangkan kebutuhan untuk mengelola timer benchmark secara manual untuk kode pengaturan dan pembersihan.
Selanjutnya, kompiler go sekarang mendeteksi loop di mana kondisinya hanya panggilan testing.B.Loop
dan mencegah eliminasi kode mati dalam loop. Dalam GO 1.24, ini diimplementasikan dengan melarang inlining ke dalam tubuh loop seperti itu, tetapi kami berencana untuk memperbaiki ini di masa depan.
Fitur bagus lainnya testing.B.Loop
adalah pendekatan ramp-up sekali tembak. Dengan a b.N
-Tentang Benchmark, Paket Pengujian harus memanggil fungsi tolok ukur beberapa kali dengan nilai yang berbeda dari b.N
meningkat sampai waktu yang diukur mencapai ambang batas.
Sebaliknya, b.Loop
dapat dengan mudah menjalankan loop benchmark sampai mencapai ambang waktu, dan hanya perlu memanggil fungsi benchmark sekali. Secara internal, b.Loop
Masih menggunakan proses ramp-up untuk mengamortisasi overhead pengukuran, tetapi ini tersembunyi dari penelepon dan bisa lebih efisien.
Kendala tertentu dari b.N
-Ding Loop masih berlaku untuk b.Loop
-Ding Loop. Tetap menjadi tanggung jawab pengguna untuk mengelola timer dalam loop benchmark, bila perlu: (contoh sumber)
func BenchmarkSortInts(b *testing.B) {
ints := make([]int, N)
for b.Loop() {
b.StopTimer()
fillRandomInts(ints)
b.StartTimer()
slices.Sort(ints)
}
}
Dalam contoh ini, untuk membandingkan kinerja penyortiran di tempat slices.Sort
array yang diinisialisasi secara acak diperlukan untuk setiap iterasi. Pengguna harus tetap mengelola timer secara manual dalam kasus seperti itu.
Selain itu, masih perlu ada satu loop seperti itu di badan fungsi benchmark (a b.N
-Ding loop tidak dapat hidup berdampingan dengan a b.Loop
-Ding Loop), dan setiap iterasi loop harus melakukan hal yang sama.
Kapan harus digunakan
Itu testing.B.Loop
Metode sekarang merupakan cara yang disukai untuk menulis tolok ukur:
func Benchmark(b *testing.B) {
... setup ...
for b.Loop() {
// optional timer control for in-loop setup/cleanup
... code to measure ...
}
... cleanup ...
}
testing.B.Loop
Menawarkan pembandingan yang lebih cepat, lebih akurat, dan lebih intuitif.
Ucapan Terima Kasih
Terima kasih banyak kepada semua orang di komunitas yang memberikan umpan balik tentang masalah proposal dan melaporkan bug saat fitur ini dirilis! Saya juga berterima kasih kepada Eli Bendersky atas ringkasan blognya yang bermanfaat. Dan akhirnya terima kasih banyak kepada Austin Clements, Cherry Mui dan Michael Pratt atas ulasan mereka, pekerjaan yang bijaksana tentang opsi desain dan peningkatan dokumentasi. Terima kasih atas kontribusi Anda!
Kredit: JUNYANG SHAO
Foto oleh Will Paterson di Unsplash
Artikel ini tersedia di