Setelah ekspresi di-vektorisasi, langkah optimasi berikutnya adalah mengatur lalu lintas memorinya: seberapa banyak array sementara yang dibuat, dan ke mana hasil ditulis. Pada array kecil biaya ini tak terasa, tetapi pada array sejuta elemen setiap array sementara berarti puluhan megabyte alokasi tambahan.
Bab ini membahas tiga tuas yang saling melengkapi: parameter out untuk menulis hasil ke buffer yang sudah ada, operator in-place, dan np.einsum yang merangkai perhitungan menjadi satu pass. Sesuai pesan Chapter Mengukur Waktu Eksekusi, setiap tuas diakhiri dengan ajakan mengukur ulang.
Parameter out: Menulis Hasil ke Buffer yang Ada #
Hampir semua ufunc NumPy menerima parameter out: array tujuan tempat hasil ditulis. Dengan out, dua operasi berurutan dapat memakai satu buffer yang sama tanpa membuat array baru:
import numpy as np
a = np.array([1.0, 2.0, 3.0])
b = np.array([10.0, 20.0, 30.0])
hasil = np.zeros(3)
np.multiply(a, 2.0, out=hasil)
np.add(hasil, b, out=hasil)
print(hasil)Output yang Dihasilkan:
[12. 24. 36.]Bedah baris demi baris:
- Baris 5 (
np.zeros(3)): buffer hasil disiapkan sekali di awal — inilah pola preallocation. - Baris 6 (
np.multiply(a, 2.0, out=hasil)): hasila * 2ditulis langsung kehasil; tanpaout, baris ini akan membuat array sementara baru. - Baris 7 (
np.add(hasil, b, out=hasil)):hasilberperan ganda sebagai input sekaligus output. Ufunc menangani kasus ini dengan benar karena operasinya elemen demi elemen pada posisi yang sama.
Ekspresi a * 2 + b menghasilkan nilai yang sama, tetapi menyisakan dua array sementara; versi out tidak menyisakan satu pun.
Operator In-place #
Operator in-place (+=, *=, dan seterusnya) adalah bentuk ringkas dari pola out: hasil ditulis kembali ke array asal, dan identitas objeknya tidak berubah:
import numpy as np
a = np.arange(5, dtype=np.float64)
sebelum = a
a *= 2
a += 0.5
print(a)
print("masih array yang sama:", np.shares_memory(a, sebelum))Output yang Dihasilkan:
[0.5 2.5 4.5 6.5 8.5]
masih array yang sama: TrueCara membacanya:
- Baris 4 (
sebelum = a): nama kedua dibuat untuk membuktikan identitas objek, bukan menyalin datanya. - Baris 5-6:
a *= 2dana += 0.5memperbarui isi blok yang sama;np.shares_memorymengembalikanTrue, berarti tidak ada array baru yang lahir.
einsum: Satu Pass untuk Rangkaian Operasi #
Ketika perhitungan berupa kontraksi sumbu — perkalian lalu penjumlahan sepanjang axis — np.einsum mengerjakannya dalam satu pass tanpa array sementara antara:
import numpy as np
a = np.arange(6, dtype=np.float64).reshape(2, 3)
v = np.array([1.0, 0.5, 2.0])
manual = (a * v).sum(axis=1)
einsum = np.einsum("ij,j->i", a, v)
print(manual)
print(einsum)
print(np.array_equal(manual, einsum))Output yang Dihasilkan:
[ 4.5 15. ]
[ 4.5 15. ]
TrueCara membacanya:
- Baris 4 (
manual): cara dua langkah —a * vmembuat matriks sementara 2x3, lalusum(axis=1)membuat hasil baru lagi. - Baris 5 (
"ij,j->i"): notasi einsum menyatakan kontrak: indeksjdiulang pada kedua operand lalu hilangkan, sehingga penjumlahan sepanjang kolom langsung terjadi di dalam satu panggilan. - Baris 8: hasilnya identik untuk kasus ini. Untuk rangkaian besar,
np.einsum(..., optimize=True)menyusun urutan kontraksi terbaik secara otomatis; notasi lengkapnya dibahas pada Chapter einsum.
Menekan Array Temporer #
Manfaat menghilangkan array sementara dapat diukur: satu ekspresi a * 2.0 + b * 3.0 mengalokasikan tiga array sementara setiap dipanggil, sedangkan versi ber-buffer tidak mengalokasikan apa pun setelah persiapan:
# notest
import numpy as np
import timeit
a = np.arange(1_000_000, dtype=np.float64)
b = np.arange(1_000_000, dtype=np.float64) * 2.0
tmp1 = np.empty_like(a)
tmp2 = np.empty_like(a)
hasil = np.empty_like(a)
def dengan_temporer():
return a * 2.0 + b * 3.0
def dengan_buffer():
np.multiply(a, 2.0, out=tmp1)
np.multiply(b, 3.0, out=tmp2)
np.add(tmp1, tmp2, out=hasil)
return hasil
t_temp = min(timeit.repeat(dengan_temporer, number=20, repeat=5)) / 20
t_buf = min(timeit.repeat(dengan_buffer, number=20, repeat=5)) / 20
print(f"dengan array temporer : {t_temp * 1000:.2f} ms")
print(f"dengan buffer out= : {t_buf * 1000:.2f} ms")Output yang Dihasilkan:
# (ilustrasi — angka waktu berbeda di tiap komputer)
dengan array temporer : 3.15 ms
dengan buffer out= : 2.43 msPenghematan sekitar seperempat waktu berasal dari alokasi dan salinan yang dihindari — angkanya berbeda di tiap komputer, tetapi arah penghematannya konsisten. Perhatikan juga polanya: buffer tmp1, tmp2, dan hasil disiapkan sekali di luar, sehingga panggilan berulang tidak menyentuh alokator memori sama sekali.
Checklist Optimasi Operasi Array #
Urutan kerja yang sehat, dari yang paling murah:
- Vektorisasi dulu: hilangkan loop
foryang isinya aritmetika (Chapter Vectorization vs Loop). - Preallocate: siapkan
np.emptyuntuk hasil yang diisi berulang, alih-alih menumbuhkan list lalu mengonversinya. - Gunakan
outatau in-place untuk array besar yang diproses berulang; satukan buffer agar tidak ada alokasi per panggilan. - Rangkai kontraksi dengan einsum bila pola perhitungannya cocok.
- Ukur ulang dengan
timeit— pertahankan perubahan hanya bila rasionya layak terhadap kompleksitas yang ditambah.