Broadcasting terasa nyaman sehingga mudah terlupa satu konsekuensinya: hasil operasi aritmetika selalu berupa array baru rapat sebesar bentuk hasil. Pola (n, 1) berjumpa (1, n) adalah contoh klasiknya — kedua operand ringan, tetapi hasilnya matriks n × n. Bila yang kita perlukan hanya elemen pada diagonal, sisanya miliaran byte yang menganggur.
Bab ini melatih cara membaca tanda bahaya dan tiga alternatif yang lebih hemat:
- Menghitung biaya memori sebelum menulis operasi.
- Mengganti matriks perantara dengan
np.einsum. - Memakai operasi in-place dan perhitungan per baris.
Menghitung biaya memori sebelum menulis kode #
Tanda bahaya pertama adalah bentuk hasil. Untuk n = 10000, bentuk (n, 1) dan (1, n) diperluas menjadi (10000, 10000) — seratus juta elemen float64. Perhitungannya dapat ditebak dulu tanpa membuat array:
import numpy as np
n = 10_000
itemsize = np.dtype(np.float64).itemsize
bentuk_hasil = np.broadcast_shapes((n, 1), (1, n))
print("bentuk hasil broadcast:", bentuk_hasil)
print("jumlah elemen :", n * n)
print("total memori (byte) :", n * n * itemsize)
print("total memori (MB) :", n * n * itemsize / 1_000_000)Output yang Dihasilkan:
bentuk hasil broadcast: (10000, 10000)
jumlah elemen : 100000000
total memori (byte) : 800000000
total memori (MB) : 800.0- Baris 5 (
np.broadcast_shapes): bentuk hasil dihitung dari metadata saja — tanpa satu byte pun dialokasikan untuk array hasil. - Baris 7-10:
10000 × 10000elemen dikalikan 8 byte per elemen menghasilkan tepat800000000byte, atau 800 MB, untuk satu operasi saja.
Angka tersebut menjelaskan mengapa operasi seperti ini tiba-tiba memenuhi memori mesin: kedua operand hanya memakan puluhan kilobyte, tetapi hasil aritmetikanya ratusan megabyte.
Alternatif einsum untuk kebutuhan diagonal #
Bila dari matriks (n, n) itu yang diperlukan hanya pasangan elemen seposisi — elemen (i, i) — maka seluruh kolom luar tidak pernah dipakai. np.einsum menghitung hanya pasangan itu:
import numpy as np
a = np.array([1., 2., 3., 4.])
b = np.array([5., 6., 7., 8.])
matriks = a[:, np.newaxis] * b[np.newaxis, :]
diagonal = np.einsum("i,i->i", a, b)
print(matriks.shape)
print(diagonal)
print(np.array_equal(diagonal, np.diag(matriks)))Output yang Dihasilkan:
(4, 4)
[ 5. 12. 21. 32.]
True- Baris 5 (
matriks): pola(4, 1) * (1, 4)menghasilkan matriks(4, 4)penuh — padan = 10000inilah 800 MB. - Baris 6 (
np.einsum("i,i->i")): notasii,i->imenyatakan “kalikan elemen seposisi saja”, menghasilkan array(n,)— sekitar 80 KB padan = 10000, sepuluh ribu kali lebih kecil. - Baris 8 (
np.array_equal):Truemembuktikan hasileinsumidentik dengan diagonal matriks penuh.
Mengubah operasi menjadi in-place #
Sumber kedua pemborosan adalah pola a = a * 2, yang membuat array hasil baru sementara array lama menunggu dibersihkan. Operator gabungan seperti *= menulis ulang buffer yang sama:
import numpy as np
x = np.arange(1_000_001, dtype=np.float64)
id_awal = id(x)
x = x * 2
print("a = a * 2 membuat array baru :", id(x) != id_awal)
y = np.arange(1_000_001, dtype=np.float64)
id_awal = id(y)
y *= 2
print("a *= 2 mengubah di tempat :", id(y) == id_awal)Output yang Dihasilkan:
a = a * 2 membuat array baru : True
a *= 2 mengubah di tempat : True- Baris 4-6 (
x = x * 2):id(x)berubah — pernyataan tersebut mengalokasikan array baru berukuran 8 MB dan mengganti ikatan namax. - Baris 8-10 (
y *= 2):id(y)tetap — operasi in-place menulis ulang buffer lama tanpa alokasi baru. - Baris 7 dan 11: perbandingan
idsebelum-sesudah menjadi bukti perilaku kedua pola.
Menghitung per baris bila hasil akhirnya kecil #
Sumber ketiga adalah matriks perantara besar yang tujuannya hanya menghasilkan array kecil. Total penjualan setelah potongan per cabang berbentuk (n,) — matriks (n, m) tidak perlu terbentuk utuh:
import numpy as np
penjualan = np.array([[10., 12., 9.],
[8., 11., 13.],
[15., 7., 6.]])
potongan = np.array([1., 2., 0.5])
total_penuh = (penjualan - potongan).sum(axis=1)
total_baris = np.empty(penjualan.shape[0])
for i in range(penjualan.shape[0]):
total_baris[i] = (penjualan[i] - potongan).sum()
print(total_penuh)
print(total_baris)
print("hasil sama:", np.array_equal(total_penuh, total_baris))Output yang Dihasilkan:
[27.5 28.5 24.5]
[27.5 28.5 24.5]
hasil sama: True- Baris 7 (
total_penuh): operasi broadcasting membentuk matriks perantara(3, 3)sebelum diratakan per baris. - Baris 9-11 (loop per baris): tiap baris diproses satu per satu — perantara hanya berukuran
(m,), dan hasil akhirnya identik. - Baris 13 (
np.array_equal):Truemenegaskan kedua jalur menghasilkan angka yang sama.
Jalan per baris memang lebih lambat untuk data kecil — pada bab Loop Python vs Vectorization kita melihat biayanya. Keputusan yang seimbang: biarkan broadcasting bekerja bila hasilnya muat di memori, dan beralih ke per baris (atau per potongan) bila perantara membengkak.
| Pola | Perantara yang terbentuk | Alternatif hemat |
|---|---|---|
(n, 1) * (1, n) padahal hanya diagonal | matriks n × n | np.einsum("i,i->i") |
a = a * 2 pada array besar | salinan baru sebesar a | in-place: a *= 2 atau out=a |
matriks (n, m) demi hasil (n,) | matriks (n, m) | loop per baris / per potongan |