Kernel yang benar dan API yang rapi belum menjawab pertanyaan terakhir dari sebuah proyek, yaitu apakah kernel itu benar-benar lebih cepat daripada baseline yang ada. Topik ini membahas pengukuran kinerja secara jujur terhadap baseline NumPy, mulai dari pemeriksaan kesetaraan hasil, pengukuran berulang dengan waktu terbaik dan rata-rata, sampai cara membaca angka yang dihasilkan. Setelah topik ini dibaca, klaim kinerja dapat disertai pengukuran yang dapat diperiksa ulang oleh orang lain dan tidak melebihi apa yang sebenarnya diukur.
Memeriksa kebenaran sebelum mengukur#
Benchmark atas kode yang salah hanya mengukur kecepatan kesalahannya, sehingga kesetaraan hasil wajib diperiksa sebelum pengukuran dimulai. Kernel penjumlahan berparameter dtype dan lebar yang dirancang untuk pustaka statistik dipindahkan apa adanya, kemudian diterapkan pada array NumPy melalui tampilan Span. Kebenarannya diuji terhadap arr.sum() milik NumPy pada data yang sama, karena baseline dan kernel baru harus menghitung pekerjaan yang identik terlebih dahulu.
Program berikut menjalankan pemeriksaan tersebut pada array NumPy yang berisi bilangan 0 sampai 9. Kernel Mojo dan NumPy menghitung jumlah yang sama, lalu selisih kedua hasilnya dicetak. Simpan kode berikut sebagai cek.mojo.
from std.collections.span import Span
from std.python import Python, PythonObject
from std.python.numpy import from_numpy_array
def jumlah[dtype: DType, lebar: Int](data: Span[Scalar[dtype], _]) -> Scalar[dtype]:
var ptr = data.unsafe_ptr()
var total: Scalar[dtype] = Scalar[dtype](0)
var i = 0
while i + lebar <= len(data):
total += ptr.unsafe_load[width=lebar](i).reduce_add()
i += lebar
while i < len(data):
total += data[i]
i += 1
return total
def main() raises:
var np = Python.import_module("numpy")
var arr = np.arange(10.0)
var tampilan = from_numpy_array[DType.float64](arr)
var hasil_mojo = jumlah[DType.float64, 4](tampilan)
print("mojo", hasil_mojo)
print("numpy", arr.sum())
print("selisih", PythonObject(hasil_mojo) - arr.sum())# jalankan pemeriksaan kesetaraan hasil
mojo run cek.mojomojo 45.0
numpy 45.0
selisih 0.0Tiga baris keluaran menyatakan satu kesimpulan, yaitu kernel dan baseline menghasilkan nilai yang identik. Nilai 45.0 muncul dua kali dari sisi yang berbeda, sedangkan selisih 0.0 adalah bukti numeriknya. Pemeriksaan semacam ini murah, tetapi menyelamatkan pengukuran dari kesimpulan yang salah, karena selisih yang bukan nol berarti yang diukur bukan pekerjaan yang sama. Perhatikan juga bahwa hasil Mojo dikonversi kembali dengan PythonObject(hasil_mojo) sebelum dikurangi, sehingga pengurangan berlangsung sepenuhnya di sisi Python.
Mengukur dengan pengulangan di proses yang sama#
Satu kali pengukuran tidak cukup untuk menarik kesimpulan, karena setiap eksekusi membawa derau (noise) dari cache, penjadwalan sistem operasi, dan keadaan prosesor. Program berikut mengukur baseline NumPy dan kernel Mojo bergantian di dalam proses yang sama, masing-masing sepuluh kali, lalu melaporkan waktu terbaik dan rata-rata untuk kedua sisi. Waktu dibaca dengan perf_counter dari std.time, yang mengembalikan pembacaan waktu presisi tinggi dalam satuan detik, sehingga selisih dua pembacaan berurutan adalah durasi pekerjaan di antara keduanya.
Simpan kode berikut sebagai benchmark.mojo. Kedua sisi diukur dengan pola yang persis sama, yaitu pembacaan perf_counter sebelum dan sesudah pekerjaan, akumulasi durasi untuk rata-rata, pencarian durasi terkecil untuk waktu terbaik, serta penjumlahan hasil setiap iterasi untuk pemeriksaan akhir. Data berukuran satu juta elemen disiapkan sekali sebelum pengukuran, sehingga biaya penyiapan tidak ikut terhitung.
from std.time import perf_counter
from std.collections.span import Span
from std.python import Python, PythonObject
from std.python.numpy import from_numpy_array
comptime ULANG = 10
def jumlah[dtype: DType, lebar: Int](data: Span[Scalar[dtype], _]) -> Scalar[dtype]:
var ptr = data.unsafe_ptr()
var total: Scalar[dtype] = Scalar[dtype](0)
var i = 0
while i + lebar <= len(data):
total += ptr.unsafe_load[width=lebar](i).reduce_add()
i += lebar
while i < len(data):
total += data[i]
i += 1
return total
def main() raises:
var np = Python.import_module("numpy")
var arr = np.arange(1000000.0)
var tampilan = from_numpy_array[DType.float64](arr)
# baseline: penjumlahan bawaan numpy
var terbaik_np = 1.0e30
var total_np = 0.0
var akum_np = PythonObject(0.0)
for _ in range(ULANG):
var mulai = perf_counter()
var hasil_np = arr.sum()
var akhir = perf_counter()
var durasi = akhir - mulai
total_np += durasi
if durasi < terbaik_np:
terbaik_np = durasi
akum_np += hasil_np
# kernel: penjumlahan Mojo berlebar 4
var terbaik_mojo = 1.0e30
var total_mojo = 0.0
var akum_mojo: Float64 = 0.0
for _ in range(ULANG):
var mulai = perf_counter()
var hasil_mojo = jumlah[DType.float64, 4](tampilan)
var akhir = perf_counter()
var durasi = akhir - mulai
total_mojo += durasi
if durasi < terbaik_mojo:
terbaik_mojo = durasi
akum_mojo += hasil_mojo
print("elemen", len(tampilan))
print("numpy terbaik", terbaik_np, "rata-rata", total_np / ULANG)
print("mojo terbaik", terbaik_mojo, "rata-rata", total_mojo / ULANG)
print("selisih hasil", PythonObject(akum_mojo) - akum_np)# tidak-dijalankan: angka waktu bervariasi pada setiap eksekusi
mojo run benchmark.mojoelemen 1000000
numpy terbaik 0.0004822190003324067 rata-rata 0.0005232844001511694
mojo terbaik 0.0003936039993277518 rata-rata 0.00040886219994717976
selisih hasil 0.0Keluaran di atas berasal dari satu eksekusi nyata pada sandbox saat topik ini ditulis, dan angka waktunya berubah antar eksekusi maupun antar mesin, sedangkan baris elemen dan selisih hasil 0.0 bersifat tetap. Baris selisih hasil menjumlahkan hasil sepuluh iterasi dari kedua sisi lalu mengurangkannya, dan nol berarti pengukuran tidak mengubah pekerjaan yang dihitung. Dua pembacaan perf_counter mengurung tepat satu panggilan penjumlahan, sehingga tidak ada pekerjaan lain yang ikut terhitung.
Membaca hasil dengan jujur#
Waktu terbaik dan rata-rata menjawab dua pertanyaan yang berbeda. Waktu terbaik mendekati batas bawah pekerjaan, yaitu kondisi paling bersih yang pernah dicapai selama pengukuran, sedangkan rata-rata menggambarkan kinerja yang biasa ditemui pengguna karena memuat iterasi yang terganggu derau. Keduanya dilaporkan berdampingan, sebab laporan yang hanya menampilkan angka terbaik menyembunyikan varians yang nyata.
Pada keluaran di atas, waktu terbaik kernel Mojo lebih kecil daripada waktu terbaik NumPy, dan pola yang sama muncul pada rata-ratanya. Keduanya berdekatan, yaitu berada di bawah satu milidetik untuk satu juta elemen, sehingga kesimpulan yang lebih kuat membutuhkan pengulangan pada lebih banyak eksekusi dan mesin. Cara membaca yang jujur adalah menyatakan rentang yang terlihat, menyertakan kondisi pengukuran, dan menunda keputusan akhir sampai pengukuran yang lebih panjang dilakukan.
Perbandingan di proses yang sama menutup celah kejujuran lain, yaitu perbedaan lingkungan antar pengukuran. Baseline dan kernel berbagi proses, cache, dan keadaan mesin yang sama, sehingga perbedaan yang tersisa lebih mungkin berasal dari pekerjaannya sendiri. Membandingkan angka dari dua mesin yang berbeda justru mengukur perbedaan mesin, bukan perbedaan kode.
Kesalahan umum#
Kesalahan pertama adalah mengukur sekali lalu menyimpulkan. Satu iterasi mudah terganggu derau, dan keluaran pada bagian sebelumnya memperlihatkan bahwa angka berpindah antar eksekusi walaupun pekerjaannya tetap. Perbaikannya sudah terlihat pada program benchmark, yaitu pengulangan dengan pencatatan waktu terbaik dan rata-rata sekaligus.
Kesalahan kedua adalah memasukkan pekerjaan yang bukan sasaran pengukuran ke dalam lingkup waktu, misalnya pembuatan array, pembentukan tampilan, atau pencetakan hasil. Lingkup yang diukur menjadi campuran pekerjaan, dan perbaikannya adalah menyiapkan data sebelum kurungan perf_counter pertama serta mengurung tepat satu pekerjaan di dalamnya. Kesalahan ketiga adalah mengukur baseline dan kernel di proses atau mesin yang berbeda, karena perbedaan lingkungan ikut terhitung ke dalam hasil. Pindahkan kedua sisi ke dalam satu program, ukur bergantian, dan laporkan keduanya dengan format yang sama.
Kesalahan keempat adalah membandingkan ukuran yang tidak setara, misalnya rata-rata Mojo terhadap waktu terbaik NumPy. Kedua ukuran itu menjawab pertanyaan yang berbeda, dan perbandingan silang hampir selalu memihak salah satu sisi. Bandingkan terbaik dengan terbaik dan rata-rata dengan rata-rata, lalu laporkan keduanya sebagaimana adanya.
Ringkasan#
- Pemeriksaan kesetaraan hasil terhadap
arr.sum()wajib dilakukan sebelum pengukuran, karena benchmark atas kode yang salah tidak berarti apa-apa. - Pengukuran yang dapat dipercaya mengulang pekerjaan sepuluh kali di proses yang sama, melaporkan waktu terbaik dan rata-rata, serta mengurung tepat satu pekerjaan dengan
perf_counter. - Baris
selisih hasilmemastikan pengukuran tidak mengubah hasil perhitungan, sedangkan baris waktu bervariasi pada setiap eksekusi. - Klaim kinerja menyertakan keluaran lengkap, kondisi pengukuran, dan rentangnya, bukan satu angka tunggal.
Referensi#
- Parameterized declarations — Mojo Manual — Dokumentasi resmi deklarasi berparameter yang menjadi dasar kernel dtype dan lebar yang diukur.
- Mojo standard library — Mojo
— Referensi fungsi
perf_counterpada modulstd.timedan jembatanstd.python.numpyyang dipakai pengukuran.