Baseline loop sudah tercatat; kini giliran sisi NumPy. Fungsi yang diukur persis pasangannya di bagian pertama — data sama, seed sama, hanya implementasinya yang berbeda. Kesetaraan hasilnya sudah diverifikasi, sehingga satu-satunya variabel yang tersisa dalam pengukuran adalah kecepatan.
Mengukur Ketiga Versi Vectorized #
# notest
import numpy as np
import timeit
rng = np.random.default_rng(42)
data = rng.random(1_000_000)
bobot = rng.random(1_000_000)
def jumlah_vectorized(data):
return np.sum(data)
def dot_vectorized(a, b):
return np.dot(a, b)
def filter_vectorized(data, ambang):
return data[data > ambang]
for nama, aksi in [
("jumlah_vectorized", lambda: jumlah_vectorized(data)),
("dot_vectorized", lambda: dot_vectorized(data, bobot)),
("filter_vectorized", lambda: filter_vectorized(data, 0.5)),
]:
waktu = timeit.repeat(aksi, number=10, repeat=5)
per_eksekusi = [t / number for t in waktu]
print(f"{nama}: {[f'{t * 1000:.3f} ms' for t in per_eksekusi]}")Output yang Dihasilkan (contoh — angka waktu berbeda di tiap komputer):
jumlah_vectorized: ['0.502 ms', '0.459 ms', '0.465 ms', '0.463 ms', '0.461 ms']
dot_vectorized: ['0.335 ms', '0.319 ms', '0.321 ms', '0.325 ms', '0.328 ms']
filter_vectorized: ['6.153 ms', '7.932 ms', '6.022 ms', '6.028 ms', '5.860 ms']Dua perbedaan penting dari blok pengukuran loop:
- Baris 19 (
number=10): satu eksekusi hanya sepersekian milidetik, sehinggatimeitmengukur sepuluh eksekusi sekaligus; pembagiant / numberdi baris berikutnya mengembalikannya ke biaya per eksekusi. - Baris 20-21: format
f"{t * 1000:.3f} ms"memakai tiga desimal karena nilainya berada di bawah 1 ms.
Dari contoh di atas: np.sum sekitar 0,46 ms, np.dot sekitar 0,33 ms, dan boolean indexing sekitar 6 ms. Perhatikan bahwa filter jauh lebih berat daripada dua reduksi lainnya — ia harus mengalokasikan dan mengisi array baru, bukan sekadar merangkum angka menjadi satu skalar.
Menghitung Rasio Percepatan #
Angka dua kubu baru bermakna ketika dibagi. Rasio dihitung dari durasi loop dibagi durasi vectorized; blok berikut mengukur keduanya dalam satu script untuk skenario pertama:
# notest
import numpy as np
import timeit
rng = np.random.default_rng(42)
data = rng.random(1_000_000)
def jumlah_loop(data):
total = 0.0
for nilai in data:
total += nilai
return total
def jumlah_vectorized(data):
return np.sum(data)
t_loop = min(timeit.repeat(lambda: jumlah_loop(data), number=1, repeat=5))
t_vec = min(timeit.repeat(lambda: jumlah_vectorized(data), number=10, repeat=5)) / 10
rasio = t_loop / t_vec
print(f"loop : {t_loop * 1000:.1f} ms")
print(f"vectorized : {t_vec * 1000:.3f} ms")
print(f"vectorized {rasio:.0f}x lebih cepat")Output yang Dihasilkan (contoh — angka waktu berbeda di tiap komputer):
loop : 61.5 ms
vectorized : 0.412 ms
vectorized 149x lebih cepatPada contoh tersebut rasionya sekitar 150x. Sepanjang sesi pengukuran penulis, rasio ketiga skenario bergerak di kisaran: 150-180x untuk penjumlahan, 650-700x untuk dot product, dan 18-23x untuk boolean filtering — secara keseluruhan, puluhan hingga ratusan kali lebih cepat. Angka pastinya akan berbeda di mesin pembaca, tetapi urutan besarannya tidak.
Pemeriksaan Pendukung yang Deterministik #
Angka waktu tidak bisa dijadikan assertion karena tidak pernah sama persis dua kali dijalankan. Karena itu klaim yang bisa diuji otomatis dipisahkan dari klaim yang hanya bisa dicontohkan. Berikut pendamping deterministiknya — besaran memori dan jumlah operasi yang tampil sebagai angka pasti:
import numpy as np
rng = np.random.default_rng(42)
data = rng.random(1_000_000)
bobot = rng.random(1_000_000)
print("data :", data.dtype, data.nbytes, "byte")
print("bobot :", bobot.dtype, bobot.nbytes, "byte")
print("iterasi loop : 1000000")
print("pemanggilan ufunc : 1")
terpilih = data[data > 0.5]
print("elemen hasil filter :", terpilih.size)
print("nbytes hasil filter :", terpilih.nbytes, "byte")Output yang Dihasilkan:
data : float64 8000000 byte
bobot : float64 8000000 byte
iterasi loop : 1000000
pemanggilan ufunc : 1
elemen hasil filter : 499793
nbytes hasil filter : 3998344 byteMari kita baca:
- Baris 6-7: input 16 MB secara total; angka ini deterministik dan menjelaskan mengapa
filter_vectorizedmembutuhkan sekitar 6 ms — ia menyalin sekitar 4 MB ke array baru. - Baris 8-9: satu juta iterasi Python berbanding satu pemanggilan ufunc. Ketimpangan jumlah pemanggilan inilah inti dari vectorization (bab Pengenalan Vectorization).
- Baris 10-12: hasil filter berisi 499.793 elemen (3.998.344 byte) — data pembanding untuk interpretasi hasil nanti.
Satu pemeriksaan kecil lagi memastikan dua ejaan dot product di NumPy memang jalur yang sama, karena keduanya kita pakai bergantian sepanjang proyek:
import numpy as np
rng = np.random.default_rng(42)
a = rng.random(1_000)
b = rng.random(1_000)
hasil = np.dot(a, b)
print(np.array_equal(hasil, a @ b))
print(type(hasil).__name__)
print(hasil.dtype)Output yang Dihasilkan:
True
float64
float64Penjelasannya pendek:
- Baris 6 (
np.array_equal(...)):np.dotdan operator@menghasilkan bit yang sama pada array 1-D — keduanya setara sesuai bab Dot Product. - Baris 7-8: hasil dot product adalah scalar NumPy bertipe
np.float64, bukan array — penting diingat saat menyusun fungsi pengukuran yang mengembalikan satu angka.
Semua alat ukur dan pemeriksaan pendamping siap. Bagian terakhir menyusun keduanya menjadi tabel, interpretasi, dan laporan.