Menghindari salinan

Operasi in-place, parameter out, dan praalokasi.

Daftar isi

Topik ini membahas teknik menjaga penggunaan memori tetap hemat saat mengolah array, yaitu menghindari salinan yang tidak perlu. Pembahasan mencakup perbedaan operasi biasa dan operasi in-place, parameter out pada ufunc, praalokasi penyangga hasil, alat verifikasi np.shares_memory, serta kondisi yang membuat operasi in-place berbahaya. Setelah membacanya, alokasi memori sementara dapat dikurangi tanpa menambah risiko merusak data.

Perbedaan x + 1 dan x += 1#

Pernyataan x + 1 membangun array baru: NumPy mengalokasikan penyangga berukuran sama dengan x, mengisinya dengan hasil, lalu mengembalikan penyangga itu. Pernyataan x += 1 menulis hasilnya langsung ke penyangga milik x tanpa alokasi baru. Pola kedua disebut operasi in-place, dan padanannya tersedia untuk aritmetika dasar, yaitu +=, -=, *=, dan /=.

Bukti berikut menggunakan np.shares_memory untuk menunjukkan apakah dua array berbagi penyangga yang sama.

python
import numpy as np

x = np.array([1.0, 2.0, 3.0])
y = x + 1.0
print(np.shares_memory(x, y))

x2 = np.array([1.0, 2.0, 3.0])
y2 = x2
y2 += 1.0
print(np.shares_memory(x2, y2))
print(x2)
text Keluaran
False
True
[2. 3. 4.]

Keluaran pertama melaporkan False karena y berdiri di penyangga sendiri. Keluaran kedua melaporkan True, dan baris ketiganya menunjukkan akibatnya: penambahan pada y2 ikut mengubah x2, karena keduanya menunjuk penyangga yang sama. Inilah makna in-place yang sesungguhnya, bukan sekadar gaya penulisan yang lebih pendek.

Penyangga sementara dari x + 1 tidak gratis. Selain waktu alokasi, NumPy harus menulis hasilnya sekali lagi ke memori sebelum penyangga lama diambil oleh pemulung memori (garbage collector). Untuk array sejuta elemen float64, sementara tersebut berukuran 8 megabyte, dan ukurannya tumbuh sebanding data.

Parameter out dan praalokasi#

Setiap ufunc NumPy menyediakan parameter out, yaitu array yang menjadi tujuan penulisan hasil. Dengan out, penyangga tujuan disiapkan sekali, lalu diisi ulang pada setiap pemanggilan. Pola ini disebut praalokasi, dan hasil akhirnya identik dengan operasi biasa.

python
import numpy as np

x = np.array([1.0, 2.0, 3.0])
hasil = np.empty(3)
np.add(x, 1.0, out=hasil)
print(hasil)

buffer = np.empty(2)
for baris in np.array([[1.0, 2.0], [3.0, 4.0]]):
    np.multiply(baris, 10.0, out=buffer)
    print(buffer.copy())
text Keluaran
[2. 3. 4.]
[10. 20.]
[30. 40.]

Keluaran menunjukkan dua hal: hasil np.add(x, 1.0, out=hasil) bernilai sama dengan operasi biasa, dan penyangga buffer yang sama terisi ulang dengan benar untuk setiap baris data. Alokasi hanya terjadi satu kali di awal, sedangkan tanpa praalokasi setiap baris membangun penyangga hasilnya sendiri.

Parameter out juga menerima array yang sama dengan masukannya, sehingga np.add(x, 1.0, out=x) ekuivalen dengan x += 1. Bentuk ini diperlukan ketika fungsinya tidak memiliki padanan operator, misalnya np.exp, karena fungsi semacam itu tidak punya bentuk +=.

Besaran keuntungan in-place#

Blok berikut mengukur penambahan satu juta elemen float64 dengan dua cara. Sesuai pola pengukuran modul ini, blok pengukur waktu tidak disertai keluaran.

python
# tidak-dijalankan
# hasilnya berubah pada setiap eksekusi, sehingga tidak ada blok Keluaran
import time
import numpy as np

x = np.arange(1_000_000, dtype=np.float64)

mulai = time.perf_counter()
y = x + 1.0
selesai = time.perf_counter()
waktu_alokasi = selesai - mulai

mulai = time.perf_counter()
x += 1.0
selesai = time.perf_counter()
waktu_inplace = selesai - mulai

print(f"alokasi baru : {waktu_alokasi*1000:.2f} ms")
print(f"in-place     : {waktu_inplace*1000:.2f} ms")

Pada pengujian berulang di lingkungan penulisan, versi ber-alokasi memerlukan sekitar 1,8 hingga 2,0 milidetik, sedangkan versi in-place selesai dalam sekitar 0,45 hingga 0,5 milidetik. Keuntungannya kira-kira empat kali untuk operasi sekecil ini, dan pola yang sama berlaku ketika operasi semacam itu diulang ribuan kali di dalam siklus pelatihan model. Lonjakannya memang tidak sebesar lonjakan vektorisasi, karena kedua versi sama-sama menjalankan perulangan C, dan yang dihemat di sini adalah alokasi serta satu kali penulisan ulang memori.

Keuntungan semacam ini paling berarti pada operasi yang dipanggil sangat sering, misalnya pembaruan parameter di dalam siklus pelatihan, karena selisih kecil per pemanggilan terakumulasi menjadi selisih besar. Alokasi berulang juga memberi beban tambahan pada pemulung memori, karena penyangga sementara berukuran besar bermunculan lalu menunggu dikumpulkan. Operasi in-place menyingkirkan kedua beban itu sekaligus, dengan syarat penyangganya memang boleh ditimpa.

Kapan in-place berbahaya#

Dua kondisi berikut mengubah operasi in-place dari hemat menjadi merusak. Keduanya deterministik dan dapat dibuktikan dengan kode pendek, sehingga gejalanya mudah dikenali.

View yang dipakai bersama#

python
import numpy as np

data = np.array([1.0, 2.0, 3.0, 4.0])
dua_awal = data[:2]
print(np.shares_memory(dua_awal, data))
dua_awal += 100.0
print(data)
text Keluaran
True
[101. 102.   3.   4.]

dua_awal hanyalah view dua elemen pertama dari data, tetapi penambahan padanya menggeser isi data secara langsung. Apabila data dipakai di tempat lain, nilai yang dibaca darinya sudah berubah tanpa pemberitahuan. Perbaikannya mengikuti maksud: salin dulu dengan .copy() apabila data asli harus dipertahankan, atau gunakan operasi non in-place apabila hasil baru yang diinginkan.

Dtype yang tidak sama#

Operator in-place tidak mengubah dtype arraynya, sedangkan hasil operasinya bisa bertipe lebih luas. Apabila keduanya bertentangan, NumPy menolak operasinya dengan error, dan penyangga arraynya tetap utuh.

python
import numpy as np

x = np.array([1, 2, 3], dtype=np.int64)
try:
    x += 0.5
except TypeError as e:
    print(f"{type(e).__name__}: {e}")
text Keluaran
UFuncTypeError: Cannot cast ufunc 'add' output from dtype('float64') to dtype('int64') with casting rule 'same_kind'

Error muncul karena hasil x + 0.5 bertipe float64, sedangkan penyangga x hanya menampung bilangan bulat. Aturan pengecorannya adalah same_kind, yaitu tipe tujuan harus sejenis dengan tipe hasil. Perbaikannya mengikuti maksud: ubah array menjadi float64 terlebih dahulu melalui .astype, atau pakai versi non in-place yang menghasilkan array baru bertipe luas.

Kesalahan umum#

out= dengan bentuk yang salah#

Parameter out menuntut penyangga tujuan yang cocok dengan bentuk hasil. Ketidakcocokan memunculkan error broadcasting, dan pesannya menyinggung operand keluaran, sehingga arah errornya terasa berbeda dari broadcasting biasa.

python
import numpy as np

x = np.array([1.0, 2.0, 3.0])
target = np.empty(2)
try:
    np.add(x, 1.0, out=target)
except ValueError as e:
    print(f"{type(e).__name__}: {e}")
text Keluaran
ValueError: operands could not be broadcast together with shapes (3,) () (2,)

Perbaikannya adalah menyiapkan penyangga dengan bentuk yang sama dengan hasil, misalnya np.empty(3) untuk contoh di atas. Sebelum menyerahkan penyangga ke out, periksa bentuk hasil yang diharapkan terlebih dahulu, karena bentuk hasil mengikuti aturan broadcasting atas kedua masukan.

Memberikan out= ke array sementara#

Ekspresi yang ditulis sebagai nilai out dievaluasi lebih dahulu. Apabila ekspresi itu membuat array baru, hasil operasi justru ditulis ke array sementara yang tidak dimiliki siapa pun setelah baris tersebut selesai.

python
import numpy as np

x = np.array([1, 2, 3], dtype=np.int64)
hasil = np.add(x, 1.0, out=x.astype(np.float64))
print(x)
print(hasil)
print(np.shares_memory(x, hasil))
text Keluaran
[1 2 3]
[2. 3. 4.]
False

Nilai x tidak berubah dan hasil tidak berbagi memori dengan x, karena penyangga tujuannya adalah salinan sementara yang dibuat oleh x.astype. Pola yang benar adalah menyiapkan penyangga tujuan pada variabel tersendiri, lalu memberikan variabel itu ke out. Dengan begitu, array hasil tetap hidup selama diperlukan dan dapat diisi ulang berkali-kali.

Ringkasan#

  • x + 1 membangun penyangga baru, sedangkan x += 1 dan np.add(x, 1.0, out=x) menulis ke penyangga yang sudah ada.
  • Parameter out beserta praalokasi memungkinkan satu penyangga diisi ulang untuk banyak operasi berulang.
  • Pada pengujian di lingkungan penulisan, penambahan satu juta elemen secara in-place selesai dalam sekitar 0,5 milidetik berbanding sekitar 1,8 milidetik untuk versi ber-alokasi, atau sekitar empat kali lebih cepat.
  • In-place berbahaya pada view yang dipakai bersama dan pada dtype yang tidak kompatibel, sehingga setiap mutasi layak diverifikasi dengan np.shares_memory.

Referensi#