Mengapa array mengungguli list

Memori berdampingan, vektorisasi, dan mental model kinerja array.

Daftar isi

Topik ini menjelaskan alasan array NumPy mengungguli list Python untuk pekerjaan numerik. Penjelasan bergerak dari cara list menyimpan objek yang tersebar di memori, cara ndarray menyimpan angka mentah secara berdampingan, sampai vektorisasi sebagai mental model kinerja. Setelah membacanya, keputusan untuk memilih array dan menulis operasi tanpa perulangan memiliki dasar yang dapat dijelaskan, bukan sekadar kebiasaan.

Memori list: kumpulan objek tersebar#

Sebagai struktur data umum, list tidak menyimpan angka secara langsung. Struktur ini menyimpan rangkaian penunjuk memori (pointer), dan setiap penunjuk mengarah ke satu objek Python lengkap yang letaknya dapat tersebar di memori. Satu objek bilangan bulat bukan sekadar angkanya, melainkan bundel yang memuat tipe, penghitung referensi, dan ukurannya sendiri. Biaya pembungkus itulah yang jarang terbayangkan ketika list ditulis begitu saja.

Ukuran pembungkus tersebut dapat diperiksa dengan fungsi sys.getsizeof dari modul sys.

python
import numpy as np
import sys

print(sys.getsizeof(10))
print(sys.getsizeof([1, 2, 3]))
text Keluaran
28
88

Pada lingkungan yang digunakan di sini, yaitu Python 3.12 untuk mesin 64 bit, satu objek bilangan bulat kecil memerlukan 28 byte, sedangkan list yang memuat tiga bilangan memerlukan 88 byte untuk slot penunjuknya saja. Objek bilangan yang ditunjuk menempati ruang tambahan di luar angka 88 itu, sekitar 28 byte untuk masing-masing, sehingga ketiga bilangan tersebut memerlukan total sekitar 172 byte. Python sebenarnya berbagi pakai sebagian objek bilangan kecil, tetapi ukuran per objeknya tetap seperti tercetak. Angka pastinya dapat berbeda antar versi Python, sedangkan polanya tidak berubah, yaitu setiap elemen membawa pembungkus dan pembungkus itu tersebar alih-alih berdampingan.

Memori array: buffer berdampingan#

Pada ndarray, jalan yang dipilih berbeda. Angka disimpan sebagai nilai mentah dalam satu buffer memori yang berdampingan (contiguous), tanpa pembungkus objek per elemen. Seluruh elemen wajib bertipe sama, sehingga ukuran setiap slot diketahui pasti dan posisi angka berikutnya dapat ditebak dari ukuran itu.

Rincian ukurannya dapat diperiksa pada array kecil berikut.

python
import numpy as np

array = np.array([1, 2, 3], dtype=np.int64)
print(array.dtype)
print(array.itemsize)
print(array.nbytes)
text Keluaran
int64
8
24

Atribut itemsize melaporkan ukuran satu elemen, yaitu 8 byte untuk tipe int64, sedangkan nbytes melaporkan total data mentahnya, yaitu 24 byte untuk tiga elemen. Angka 24 itu murni isi datanya, dan kebutuhan total array hanya menambahkan konstanta kecil untuk pembukuan objeknya. Sebagai perbandingan kasar, versi list untuk ketiga bilangan yang sama memerlukan sekitar 172 byte, sekitar tujuh kali lipat dari 24 byte. Selisih ini berasal semata dari tata letak, karena isi logisnya sama persis.

Homogenitas dipertahankan dengan aturan yang tegas. Apabila anggota campur saat array dibuat, NumPy memilih satu tipe umum yang mencakup semuanya, seperti berikut.

python
import numpy as np

array = np.array([1, 2.5])
print(array)
print(array.dtype)
text Keluaran
[1.  2.5]
float64

Bilangan bulat 1 ikut naik menjadi pecahan karena tipe umum yang terpilih adalah float64. Tidak ada satu elemen pun yang bertipe berbeda, sehingga setiap slot berukuran sama. Perilaku ini adalah alasan mengapa array dapat dirapikan dalam memori, sekaligus pengingat untuk memeriksa dtype setelah membuat array dari data yang campur.

Vektorisasi sebagai mental model kinerja#

Vektorisasi adalah kebiasaan menyatakan operasi untuk seluruh array dalam satu pernyataan, dan pada topik ini vektorisasi dipandang sebagai mental model kinerja, bukan sekadar gaya menulis. Dua keuntungan bekerja bersamaan. Pertama, buffer berdampingan membuat satu perulangan C dapat membaca memori secara berurutan tanpa melompat mengikuti penunjuk. Kedua, satu pemanggilan fungsi menggantikan jutaan iterasi yang semula dilakukan interpreter.

Kode berikut menyandingkan kedua gaya pada dua skala yang berbeda.

python
import numpy as np

# perulangan Python: instruksi interpreter dijalankan untuk setiap elemen
array_kecil = np.arange(5)
hasil_list = []
for nilai in array_kecil:
    hasil_list.append(nilai * 2)
print(hasil_list)

# operasi array: satu pemanggilan fungsi untuk seluruh buffer
array_besar = np.arange(5_000_000)
hasil_array = array_besar * 2
print(hasil_array[:5])
print(hasil_array[-5:])
text Keluaran
[np.int64(0), np.int64(2), np.int64(4), np.int64(6), np.int64(8)]
[0 2 4 6 8]
[9999990 9999992 9999994 9999996 9999998]

Bagian pertama keluaran memperlihatkan harga yang dibayar perulangan Python, yaitu setiap elemen dikeluarkan sebagai satu objek skalar NumPy, terlihat dari tulisan np.int64(...) di dalam list hasil. Perulangan yang sama, apabila diterapkan pada array lima juta elemen, mengulang pekerjaan itu lima juta kali. Bagian kedua keluaran berasal dari satu pemanggilan array_besar * 2 atas lima juta elemen, tanpa satu pun iterasi di level Python. Pada percobaan sederhana di lingkungan penulisan artikel ini, versi array menyelesaikan pekerjaan yang sama belasan kali lebih cepat untuk satu operasi, dan selisihnya membesar hingga ratusan kali apabila setiap elemen memerlukan beberapa operasi sekaligus.

Kesalahan umum#

Kedua kesalahan berikut tidak menghasilkan pesan error sama sekali, sehingga lebih sulit dikenali. Kode berjalan dan keluarannya tampak benar, tetapi keunggulan array tidak diperoleh atau kesimpulan yang ditarik menjadi keliru. Pemeriksaan berkala atas pola kode dan ukuran data mencegah keduanya terulang.

Membawa pola list ke dalam array#

Kesalahan yang sering muncul saat berpindah kebiasaan adalah membungkus operasi array dalam list comprehension. Hasilnya tampak benar, tetapi perulangan Python per elemen ikut hidup kembali.

python
import numpy as np

array = np.arange(5)

# pola list yang dibawa ke array: perulangan Python kembali muncul
dengan_perulangan = np.array([x * 2 for x in array])
print(dengan_perulangan)

# vektorisasi: operasi array menggantikan perulangan
tervektorisasi = array * 2
print(tervektorisasi)
text Keluaran
[0 2 4 6 8]
[0 2 4 6 8]

Kedua hasil identik nilainya, tetapi versi pertama membangun list Python, mengeluarkan objek skalar satu per satu, lalu menyusun array baru darinya. Keunggulan array hilang tanpa ada pesan error yang mengingatkan. Perbaikannya adalah menuliskan operasinya langsung pada array, seperti pada baris kedua.

Mengukur keunggulan array pada data yang terlalu kecil#

Keunggulan vektorisasi tampil pada data yang besar. Untuk array beranggota beberapa elemen saja, biaya tetap pembuatan objek array dapat melebihi penghematannya, sehingga hasil pengukuran kecil terlihat setara atau bahkan lebih lambat. Kesalahannya bukan pada pilihan array, melainkan pada ukuran data yang tidak mewakili pekerjaan nyata. Uji keunggulan dengan ukuran data yang mendekati kasus sehari-hari sebelum menarik kesimpulan.

Ringkasan#

  • Struktur list menyimpan penunjuk ke objek Python yang tersebar, dan setiap objek bilangan membawa pembungkus berukuran puluhan byte.
  • Objek ndarray menyimpan angka mentah secara berdampingan dengan satu dtype, sehingga tiga bilangan int64 hanya memerlukan 24 byte.
  • Vektorisasi menggantikan perulangan interpreter per elemen dengan satu pemanggilan fungsi yang membaca buffer secara berurutan.
  • Tulis operasi langsung pada array, dan uji keunggulannya pada ukuran data yang mewakili pekerjaan nyata.

Referensi#