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 #

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

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

  1. Baris 19 (number=10): satu eksekusi hanya sepersekian milidetik, sehingga timeit mengukur sepuluh eksekusi sekaligus; pembagian t / number di baris berikutnya mengembalikannya ke biaya per eksekusi.
  2. 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:

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

text
loop       : 61.5 ms
vectorized : 0.412 ms
vectorized 149x lebih cepat

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

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  :", 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:

text
data  : float64 8000000 byte
bobot : float64 8000000 byte
iterasi loop      : 1000000
pemanggilan ufunc : 1
elemen hasil filter : 499793
nbytes hasil filter : 3998344 byte

Mari kita baca:

  1. Baris 6-7: input 16 MB secara total; angka ini deterministik dan menjelaskan mengapa filter_vectorized membutuhkan sekitar 6 ms — ia menyalin sekitar 4 MB ke array baru.
  2. Baris 8-9: satu juta iterasi Python berbanding satu pemanggilan ufunc. Ketimpangan jumlah pemanggilan inilah inti dari vectorization (bab Pengenalan Vectorization).
  3. 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:

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

text
True
float64
float64

Penjelasannya pendek:

  1. Baris 6 (np.array_equal(...)): np.dot dan operator @ menghasilkan bit yang sama pada array 1-D — keduanya setara sesuai bab Dot Product.
  2. 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.