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:

SkenarioVersi loopVersi vectorizedKarakter
Jumlah seluruh elemenakumulasi total += nilainp.sumreduksi murni
Dot productakumulasi a[i] * b[i]np.dotreduksi dengan perkalian (bab Dot Product)
Boolean filteringappend nilai di atas ambangdata[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:

python
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:

text
float64 float64
8000000 8000000
[0.77395605 0.43887844 0.85859792]
0.5 0.4998

Mari kita bedah:

  1. 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.
  2. Baris 6-7 (nbytes): tiap array 8.000.000 byte (8 MB), sesuai 1.000.000 x 8 byte float64; total input 16 MB.
  3. 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:

python
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:

text
loop       : 500026.476174
vectorized : 500026.476174
isclose    : True

Penjelasan tiap bagian:

  1. Baris 7-11 (jumlah_loop): loop Python mengakumulasi satu juta kali; tiap iterasi mengeksekusi bytecode, membaca satu scalar dari array, lalu menambahkannya ke total. Biaya itulah yang akan kita ukur di bagian berikutnya.
  2. Baris 13-14 (jumlah_vectorized): satu panggilan np.sum menyerahkan seluruh pekerjaan ke loop C.
  3. Baris 16-18: kedua total tampil sama pada enam desimal, tetapi pemeriksaannya memakai np.isclose, bukan ==. Alasannya: np.sum merangkum 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:

python
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:

text
loop       : 249818.492985
vectorized : 249818.492985
isclose    : True

Yang berbeda dari skenario pertama:

  1. Baris 7-11 (dot_loop): perkalian dan akumulasi bergantian dieksekusi di Python; tiap a[i] dan b[i] melewati lapisan pengindeksan NumPy.
  2. Baris 13-14 (dot_vectorized): np.dot menyerahkan pekerjaan ke rutinitas BLAS yang memakai instruksi SIMD — kandidat rasio terbesar nantinya.
  3. 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:

python
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:

text
499793 499793
True

Tiga catatannya:

  1. Baris 7-12 (filter_loop): list Python menampung nilai di atas ambang 0.5, lalu np.array mengubahnya menjadi array; panjangnya 499.793 elemen.
  2. Baris 14-15 (filter_vectorized): boolean indexing menyaring sekaligus di level C dan menghasilkan array dengan isi yang sama.
  3. Baris 17-18 (np.array_equal): True berarti 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:

  1. timeit.repeat, bukan sekali ukur: setiap skenario diukur beberapa kali (repeat=5) dan hasilnya berupa list durasi, bukan satu angka tunggal.
  2. number disesuaikan dengan durasi: versi loop diukur dengan number=1 karena satu eksekusinya sudah ratusan milidetik; versi vectorized dengan number=10 supaya durasi yang sangat pendek tidak tenggelam dalam ketelitian timer.
  3. Statistik ringkas: yang dilaporkan adalah nilai terkecil atau median, karena durasi tercepat paling dekat dengan biaya sesungguhnya.
  4. 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.