Hampir semua diskusi performa NumPy berujung pada dua pertanyaan yang sama: lebih cepat seberapa banyak, dan untuk operasi apa? Bab Vectorization vs Loop sudah menunjukkan arah jawabannya; proyek ini mengukurnya secara terencana. Rancangan yang adil menetapkan tiga hal lebih dulu — skenario yang dibandingkan, ukuran data yang dipakai, dan cara pengukurannya — sebelum satu angka waktu pun dicatat.
Tiga Skenario yang Dibandangkan #
Kita memilih tiga operasi yang paling sering dipakai, dengan karakter yang sengaja dibedakan:
| Skenario | Versi loop | Versi vectorized | Karakter |
|---|---|---|---|
| Jumlah seluruh elemen | akumulasi total += nilai | np.sum | reduksi murni |
| Dot product | akumulasi a[i] * b[i] | np.dot | reduksi dengan perkalian (bab Dot Product) |
| Boolean filtering | append nilai di atas ambang | data[data > ambang] | pembentukan array baru (bab Boolean Indexing) |
Ketiganya bekerja pada array berukuran satu juta elemen float64 — cukup besar sehingga biaya operasinya jelas terlihat, cukup kecil untuk memori mesin biasa:
import numpy as np
rng = np.random.default_rng(42)
data = rng.random(1_000_000)
bobot = rng.random(1_000_000)
print(data.dtype, bobot.dtype)
print(data.nbytes, bobot.nbytes)
print(data[:3])
print(round(data.mean(), 4), round(bobot.mean(), 4))Output yang Dihasilkan:
float64 float64
8000000 8000000
[0.77395605 0.43887844 0.85859792]
0.5 0.4998Mari kita bedah:
- Baris 3-5 (
default_rng(42)): generator ber-seed menghasilkan data yang sama persis setiap dijalankan — syarat benchmark yang bisa diulang orang lain dengan hasil identik. - Baris 6-7 (
nbytes): tiap array 8.000.000 byte (8 MB), sesuai 1.000.000 x 8 bytefloat64; total input 16 MB. - Baris 8-9: tiga nilai pertama dan rerata sekitar 0,5 menegaskan distribusi uniform 0-1 yang tercetak stabil.
Fungsi Kompetitor dari Kedua Kubu #
Skenario jumlah terlebih dulu — satu fungsi loop, satu fungsi vectorized, langsung diikuti pemeriksaan bahwa keduanya setia pada hasil yang sama:
import numpy as np
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)
print(f"loop : {jumlah_loop(data):.6f}")
print(f"vectorized : {jumlah_vectorized(data):.6f}")
print("isclose :", np.isclose(jumlah_loop(data), jumlah_vectorized(data)))Output yang Dihasilkan:
loop : 500026.476174
vectorized : 500026.476174
isclose : TruePenjelasan tiap bagian:
- Baris 7-11 (
jumlah_loop): loop Python mengakumulasi satu juta kali; tiap iterasi mengeksekusi bytecode, membaca satu scalar dari array, lalu menambahkannya ketotal. Biaya itulah yang akan kita ukur di bagian berikutnya. - Baris 13-14 (
jumlah_vectorized): satu panggilannp.summenyerahkan seluruh pekerjaan ke loop C. - Baris 16-18: kedua total tampil sama pada enam desimal, tetapi pemeriksaannya memakai
np.isclose, bukan==. Alasannya:np.summerangkum dengan penjumlahan berpasangan (pairwise summation) yang urutannya berbeda dari akumulasi berurutan, sehingga selisih keduanya sekitar 2e-8 — bukan nol, meskipun tak terlihat pada tampilan enam desimal.
Skenario dot product mengikuti pola yang sama:
import numpy as np
rng = np.random.default_rng(42)
data = rng.random(1_000_000)
bobot = rng.random(1_000_000)
def dot_loop(a, b):
total = 0.0
for i in range(len(a)):
total += a[i] * b[i]
return total
def dot_vectorized(a, b):
return np.dot(a, b)
print(f"loop : {dot_loop(data, bobot):.6f}")
print(f"vectorized : {dot_vectorized(data, bobot):.6f}")
print("isclose :", np.isclose(dot_loop(data, bobot), dot_vectorized(data, bobot)))Output yang Dihasilkan:
loop : 249818.492985
vectorized : 249818.492985
isclose : TrueYang berbeda dari skenario pertama:
- Baris 7-11 (
dot_loop): perkalian dan akumulasi bergantian dieksekusi di Python; tiapa[i]danb[i]melewati lapisan pengindeksan NumPy. - Baris 13-14 (
dot_vectorized):np.dotmenyerahkan pekerjaan ke rutinitas BLAS yang memakai instruksi SIMD — kandidat rasio terbesar nantinya. - Baris 16-18: hasil keduanya setara dalam toleransi
np.isclose; perbedaan urutan penjumlahan kembali menjadi alasan kita tidak memakai==.
Terakhir, skenario boolean filtering — satu-satunya yang menghasilkan array baru, sehingga verifikasinya boleh memakai kesamaan ketat:
import numpy as np
rng = np.random.default_rng(42)
data = rng.random(1_000_000)
def filter_loop(data, ambang):
hasil = []
for nilai in data:
if nilai > ambang:
hasil.append(nilai)
return np.array(hasil)
def filter_vectorized(data, ambang):
return data[data > ambang]
terpilih_loop = filter_loop(data, 0.5)
terpilih_vec = filter_vectorized(data, 0.5)
print(len(terpilih_loop), len(terpilih_vec))
print(np.array_equal(terpilih_loop, terpilih_vec))Output yang Dihasilkan:
499793 499793
TrueTiga catatannya:
- Baris 7-12 (
filter_loop): list Python menampung nilai di atas ambang 0.5, lalunp.arraymengubahnya menjadi array; panjangnya 499.793 elemen. - Baris 14-15 (
filter_vectorized): boolean indexing menyaring sekaligus di level C dan menghasilkan array dengan isi yang sama. - Baris 17-18 (
np.array_equal):Trueberarti kedua array identik bit-per-bit — wajar, karena keduanya menyalin elemen yang sama tanpa aritmetika apa pun.
Keputusan Desain Pengukuran #
Sebelum masuk ke pengukuran, kita mengunci beberapa keputusan agar angkanya bisa dipercaya:
timeit.repeat, bukan sekali ukur: setiap skenario diukur beberapa kali (repeat=5) dan hasilnya berupa list durasi, bukan satu angka tunggal.numberdisesuaikan dengan durasi: versi loop diukur dengannumber=1karena satu eksekusinya sudah ratusan milidetik; versi vectorized dengannumber=10supaya durasi yang sangat pendek tidak tenggelam dalam ketelitian timer.- Statistik ringkas: yang dilaporkan adalah nilai terkecil atau median, karena durasi tercepat paling dekat dengan biaya sesungguhnya.
- Kebenaran dulu, kecepatan kemudian: semua fungsi sudah lolos pemeriksaan kesetaraan di atas; mengukur fungsi yang salah hanya mempercepat kekeliruan.
Rancangan terkunci, fungsi teruji. Bagian berikutnya merekam baseline dari sisi loop.