Laporan benchmark

Menyusun bukti kinerja sebelum dan sesudah optimasi.

Daftar isi

Topik ini menutup proyek analisis kisi suhu ruangan dengan laporan benchmark, yaitu pengukuran kinerja yang disusun sebagai bukti. Pembahasan mencakup metodologi pengukuran yang jujur, hasil khas dari eksekusi nyata pada dua ukuran kisi, verifikasi kebenaran sebagai bagian tetap laporan, dan tabel perbandingan yang merangkum waktu, jumlah baris kode, serta risiko setiap versi. Setelah topik ini selesai, syarat keenam definisi selesai terpenuhi dan proyek memiliki bukti kinerja yang dapat diperiksa ulang.

Metodologi pengukuran#

Kepercayaan sebuah laporan benchmark ditentukan oleh metodologinya sebelum angkanya dibaca. Prinsip yang dipakai di sini ada empat. Pertama, waktu diukur dengan time.perf_counter(), yaitu jam beresolusi tinggi yang memang dirancang untuk mengukur selisih waktu. Kedua, setiap fungsi dipanaskan satu kali (warm-up) sebelum diukur, karena eksekusi pertama memuat pekerjaan sekali jalan yang tidak muncul lagi setelahnya. Ketiga, setiap versi diukur sembilan kali dan yang dilaporkan mediannya, karena median tidak tergeser oleh satu pengukuran yang kebetulan lambat. Keempat, kedua versi mengolah data yang sama persis dari seed 42, sehingga selisih waktunya murni berasal dari gaya komputasinya.

Skrip pengukurannya disimpan sebagai benchmark_suhu.py. Skrip ini tidak diberi blok keluaran pada artikel karena angkanya berubah antar mesin dan antar eksekusi, tetapi isinya dapat dijalankan pembaca pada lingkungannya masing-masing.

python benchmark_suhu.py
# tidak-dijalankan
# skrip pengukuran waktu: keluarannya berubah antar mesin, sehingga artikel tidak menyertakan keluarannya
"""Benchmark proyek kisi suhu: perulangan Python versus kode tervektorisasi.

Setiap tahap dipanaskan sekali, lalu diukur beberapa kali dengan
time.perf_counter. Yang dilaporkan adalah median waktu setiap versi, bukan
satu kali pengukuran terbaik.
"""
import time
import numpy as np


def buat_suhu(jam, sensor):
    rng = np.random.default_rng(42)
    offset = rng.uniform(-1.5, 1.5, size=sensor)
    derau = rng.normal(0, 0.8, size=(jam, sensor))
    baris = np.arange(jam)[:, np.newaxis]
    return np.round(22.0 + 3.0 * np.sin((baris - 6) * np.pi / 12) + offset + derau, 1)


def normalisasi_vektor(suhu):
    return (suhu - suhu.mean(axis=0, keepdims=True)) / suhu.std(axis=0, keepdims=True)


def ringkasan_vektor(suhu):
    return suhu.mean(axis=1)


def jarak_vektor(z):
    selisih = z.T[:, np.newaxis, :] - z.T[np.newaxis, :, :]
    return np.sqrt((selisih ** 2).sum(axis=-1))


def normalisasi_perulangan(suhu):
    n_jam, n_sensor = suhu.shape
    z = np.zeros((n_jam, n_sensor))
    for s in range(n_sensor):
        total = 0.0
        for j in range(n_jam):
            total += suhu[j, s]
        rata_s = total / n_jam
        kuadrat = 0.0
        for j in range(n_jam):
            kuadrat += (suhu[j, s] - rata_s) ** 2
        simpangan_s = (kuadrat / n_jam) ** 0.5
        for j in range(n_jam):
            z[j, s] = (suhu[j, s] - rata_s) / simpangan_s
    return z


def ringkasan_perulangan(suhu):
    n_jam, n_sensor = suhu.shape
    rata = np.zeros(n_jam)
    for j in range(n_jam):
        total = 0.0
        for s in range(n_sensor):
            total += suhu[j, s]
        rata[j] = total / n_sensor
    return rata


def jarak_perulangan(z):
    n_jam, n_sensor = z.shape
    d = np.zeros((n_sensor, n_sensor))
    for i in range(n_sensor):
        for k in range(n_sensor):
            total = 0.0
            for j in range(n_jam):
                total += (z[j, i] - z[j, k]) ** 2
            d[i, k] = total ** 0.5
    return d


def ukur(fungsi, ulangan=9):
    fungsi()
    catatan = []
    for _ in range(ulangan):
        mulai = time.perf_counter()
        fungsi()
        catatan.append(time.perf_counter() - mulai)
    catatan.sort()
    return catatan[len(catatan) // 2]


for jam, sensor in [(24, 8), (168, 24)]:
    suhu = buat_suhu(jam, sensor)
    z = normalisasi_vektor(suhu)
    print(f"kisi {jam} x {sensor} ({jam * sensor} sel)")
    tugas = [
        ("normalisasi", normalisasi_perulangan, normalisasi_vektor, (suhu,)),
        ("ringkasan", ringkasan_perulangan, ringkasan_vektor, (suhu,)),
        ("jarak", jarak_perulangan, jarak_vektor, (z,)),
    ]
    for nama, f_loop, f_vektor, argumen in tugas:
        t_loop = ukur(lambda: f_loop(*argumen))
        t_vek = ukur(lambda: f_vektor(*argumen))
        print(f"  {nama:12} perulangan {t_loop * 1000:9.3f} ms | "
              f"tervektorisasi {t_vek * 1000:8.3f} ms | {t_loop / t_vek:5.1f}x")

Kondisi lingkungan pengukuran dilaporkan apa adanya, yaitu satu sesi Python 3.12 dengan NumPy 2.1.3 pada mesin penulisan dengan beban normal, tanpa penyetelan khusus. Angka pada lingkungan lain akan berbeda, sehingga yang layak dibandingkan dari laporan semacam ini adalah urutan besarnya, bukan angka persisnya.

Hasil pengukuran#

Tabel berikut merangkum waktu khas dari beberapa eksekusi skrip pada lingkungan penulisan, dengan kisi proyek berukuran 192 sel.

TahapPerulanganTervektorisasiSelisih khas
Normalisasi per sensor0.10 ms0.02 mssekitar 5 kali
Ringkasan per jam0.03 ms0.005 mssekitar 5 kali
Jarak antar sensor0.41 ms0.01 ms35 sampai 48 kali

Pada kisi kecil 192 sel, selisih normalisasi dan ringkasan berada di sekitar lima kali, terasa tetapi tidak dramatis, karena waktu versi tervektorisasi masih didominasi overhead tetap seperti pemanggilan fungsi dan penyiaran bentuk, bukan perhitungan datanya. Selisih jarak antar sensor sudah mencapai puluhan kali pada kisi kecil karena versi perulangannya menumpuk tiga perulangan untuk setiap pasangan sensor. Pola normalisasi dan ringkasan berubah ketika kisi diperbesar menjadi 168 jam dan 24 sensor, yaitu 4032 sel atau 21 kali lebih banyak. Pada ukuran tersebut, normalisasi terukur sekitar 1.95 ms berbanding 0.04 ms, ringkasan sekitar 0.46 ms berbanding 0.009 ms, dan jarak sekitar 24.6 ms berbanding 0.52 ms, sehingga selisih ketiga tahap berkumpul di sekitar 50 kali. Biaya tetap versi tervektorisasi tidak tumbuh secepat biaya per elemen versi perulangan, sehingga makin banyak data, makin lebar selisih keduanya.

Perbedaan gaya komputasinya juga dapat dibaca langsung dari kodenya. Versi perulangan melakukan sekitar 576 operasi skalar untuk normalisasi, 192 untuk ringkasan, dan 1536 untuk matriks jarak pada kisi proyek, sedangkan versi tervektorisasi menyelesaikan setiap tahap dengan satu rangkaian pemanggilan array. Angka-angka ini bukan hasil pengukuran, melainkan konsekuensi struktur kode yang waktunya diukur di atas.

Verifikasi kebenaran dalam laporan#

Angka waktu tanpa bukti kebenaran tidak layak dilaporkan, karena versi tercepat tidak berarti apa pun apabila hasilnya salah. Laporan benchmark proyek ini karenanya menyertakan verifikasi yang sama dengan definisi selesai, yaitu perbandingan np.allclose antara kedua versi pada ketiga tahap, dieksekusi pada data yang sama dengan data yang diukur.

python
import numpy as np

JAM, SENSOR = 24, 8
rng = np.random.default_rng(42)
offset = rng.uniform(-1.5, 1.5, size=SENSOR)
derau = rng.normal(0, 0.8, size=(JAM, SENSOR))
jam = np.arange(JAM)[:, np.newaxis]
suhu = np.round(22.0 + 3.0 * np.sin((jam - 6) * np.pi / 12) + offset + derau, 1)

z = (suhu - suhu.mean(axis=0, keepdims=True)) / suhu.std(axis=0, keepdims=True)
ringkas = suhu.mean(axis=1)
selisih = z.T[:, np.newaxis, :] - z.T[np.newaxis, :, :]
jarak = np.sqrt((selisih ** 2).sum(axis=-1))

z_loop = np.zeros((JAM, SENSOR))
for s in range(SENSOR):
    total = 0.0
    for j in range(JAM):
        total += suhu[j, s]
    rata_s = total / JAM
    kuadrat = 0.0
    for j in range(JAM):
        kuadrat += (suhu[j, s] - rata_s) ** 2
    simpangan_s = (kuadrat / JAM) ** 0.5
    for j in range(JAM):
        z_loop[j, s] = (suhu[j, s] - rata_s) / simpangan_s

ringkas_loop = np.zeros(JAM)
for j in range(JAM):
    total = 0.0
    for s in range(SENSOR):
        total += suhu[j, s]
    ringkas_loop[j] = total / SENSOR

jarak_loop = np.zeros((SENSOR, SENSOR))
for i in range(SENSOR):
    for k in range(SENSOR):
        total = 0.0
        for j in range(JAM):
            total += (z[j, i] - z[j, k]) ** 2
        jarak_loop[i, k] = total ** 0.5

print("normalisasi per sensor :", np.allclose(z_loop, z))
print("ringkasan per jam      :", np.allclose(ringkas_loop, ringkas))
print("matriks jarak          :", np.allclose(jarak_loop, jarak))
print("ukuran data suhu       :", suhu.nbytes, "byte")
print("ukuran array selisih   :", selisih.nbytes, "byte")
text Keluaran
normalisasi per sensor : True
ringkasan per jam      : True
matriks jarak          : True
ukuran data suhu       : 1536 byte
ukuran array selisih   : 12288 byte

Ketiga pemeriksaan mengembalikan True, sehingga selisih waktu pada tabel sebelumnya memang membandingkan dua versi yang menghasilkan nilai yang sama. Dua baris terakhir menambahkan bukti memori yang bersifat deterministik, yaitu data proyek menempati 1536 byte, sedangkan array selisih tahap jarak menempati 12288 byte atau delapan kali lipatnya. Besar memori antara layak dilaporkan karena pola broadcasting tiga dimensi menumbuhkan array antara sebesar hasil kali ketiga dimensinya, dan angka ini ikut menentukan apakah pola tersebut masih masuk akal pada data yang lebih besar.

Tabel perbandingan dan risiko#

Rangkuman perbandingan kedua versi disusun dalam satu tabel, dengan jumlah baris kode dihitung dari berkas benchmark_suhu.py, yaitu baris nonkosong setiap fungsi tanpa baris def-nya. Dengan hitungan itu, versi perulangan menyumbang 31 baris dan versi tervektorisasi menyumbang 4 baris, sebagaimana tercantum pada tabel.

AspekVersi perulanganVersi tervektorisasi
Baris kode ketiga tahap314
Jumlah waktu khas ketiga tahap pada kisi proyek0.54 ms0.035 ms
Sumber kesalahan yang umumindeks tertukar dan batas perulangan salahbentuk broadcasting dan memori antara

Kesimpulan teknisnya untuk proyek ini adalah versi tervektorisasi lebih cepat dan lebih ringkas sekaligus, dengan kebenaran yang terverifikasi. Versi perulangan tetap dipertahankan sebagai pembanding kebenaran dan sebagai bentuk yang paling mudah ditulis ketika logika per elemen dikerjakan pertama kali. Apabila polanya tidak dapat diseragamkan atau memori antaranya melebihi batas yang tersedia, bentuk perulangan atau campuran keduanya tetap menjadi pilihan yang sah.

Risiko kedua bentuk berbeda jenisnya. Versi perulangan rawan indeks yang tertukar antara baris dan kolom serta rawan batas perulangan yang meleset satu, dan kerentanannya tumbuh seiring panjang kodenya. Versi tervektorisasi rawan kesalahan bentuk broadcasting yang kadang tidak muncul sebagai error, sebagaimana contoh penukaran sumbu pada tahap implementasi proyek ini, serta konsumsi memori antara yang tumbuh cepat pada pola tiga dimensi. Kedua risiko itu ditangani dengan cara yang sudah dipakai sepanjang proyek, yaitu memeriksa bentuk setiap tahap dan memverifikasi hasilnya dengan np.allclose.

Kesalahan umum#

Kesalahan pertama adalah melaporkan hasil satu kali pengukuran. Satu eksekusi dapat bertepatan dengan gangguan dari proses lain, sehingga angkanya dapat dua kali lipat dari kondisi biasanya. Perbaikannya adalah pemanasan dan pengulangan yang diterapkan metodologi di atas, dengan median sebagai pelaporannya.

Kesalahan kedua adalah memasukkan pembangkitan data ke dalam waktu yang diukur. Pembangkitan data memakan waktu yang sama untuk kedua versi, sehingga menyertainya mengecilkan selisih yang sebenarnya. Perbaikannya adalah membangkitkan data terlebih dahulu, lalu mengukur hanya perhitungan yang hendak dibandingkan, sebagaimana dilakukan benchmark_suhu.py.

Kesalahan ketiga adalah membandingkan angka dari lingkungan yang berbeda, baik antara dua mesin maupun antara dua sesi pada mesin yang sama dengan beban yang berbeda. Angka dari lingkungan yang berbeda tidak sebanding karena kecepatan prosesor, memori, dan beban sistem ikut berperan. Perbaikannya adalah mengukur kedua versi berdampingan pada lingkungan yang sama, lalu melaporkan kondisi lingkungannya bersama angkanya.

Ringkasan#

  • Pengukuran memakai time.perf_counter(), pemanasan sekali, sembilan kali pengulangan, dan median sebagai pelaporannya, pada data yang sama untuk kedua versi.
  • Pada kisi proyek 192 sel, selisih khasnya sekitar lima kali untuk normalisasi dan ringkasan serta puluhan kali untuk jarak, dan melebar ke sekitar 50 kali pada kisi 4032 sel.
  • Setiap angka waktu disertai verifikasi np.allclose yang mengembalikan True, sehingga perbandingannya sah.
  • Versi tervektorisasi unggul pada waktu dan jumlah baris kode, sedangkan versi perulangan tetap berguna sebagai pembanding kebenaran.

Referensi#