Tata letak memori

Intuisi kontiguitas dan strides.

Daftar isi

Topik ini membahas tata letak memori array NumPy, yaitu cara nilai-nilainya tersusun di dalam memori utama. Pembahasan mencakup atribut strides dan nbytes, perbedaan urutan C dan urutan F, konsekuensi kontiguitas terhadap kelancaran pembacaan data, serta pengembalian array ke urutan C dengan np.ascontiguousarray. Setelah membacanya, angka strides pada hasil inspeksi dapat dibaca dan ditafsirkan tanpa menebak.

Strides dan nbytes: peta byte#

Setiap ndarray menyimpan nilai numeriknya di dalam satu penyangga (buffer) byte, yaitu deretan alamat memori yang berdampingan. Atribut strides menyatakan berapa byte yang harus dilompati untuk berpindah satu elemen pada setiap sumbu, sedangkan nbytes menyatakan ukuran total penyangganya. Keduanya diturunkan dari itemsize, yaitu ukuran satu elemen dalam byte, sehingga nilainya deterministik dan selalu sama untuk array dengan tipe serta bentuk yang sama.

Inspeksi berikut memakai array suhu satu dimensi berisi lima nilai sebagai contoh datanya.

python
import numpy as np

suhu = np.array([27.5, 28.0, 26.5, 27.0, 28.5])
print(suhu.strides)
print(suhu.nbytes)
print(suhu.itemsize)
text Keluaran
(8,)
40
8

Keluarannya konsisten dengan aritmetika sederhana: satu elemen float64 berukuran 8 byte, sehingga strides berisi (8,), dan lima elemen memakan 40 byte. Untuk array berdimensi dua, alamat sebuah elemen dihitung dari dua langkah yang sesuai dengan sumbunya, sebagaimana rumus berikut.

alamat(r,c)=dasar+r⋅s0+c⋅s1 \text{alamat}(r, c) = \text{dasar} + r \cdot s_0 + c \cdot s_1

Notasi s0s_0 dan s1s_1 adalah strides untuk sumbu baris dan kolom. Rumus ini berlaku untuk semua array tanpa kecuali, dan perbedaan urutan memori pada bagian berikutnya hanya mengubah isi s0s_0 serta s1s_1, bukan bentuk rumusnya.

Urutan C dan urutan F#

NumPy mengenal dua urutan penyimpanan kanonis. Urutan C (row-major) menyimpan baris demi baris, sehingga nilai awal satu baris mengikuti akhir baris sebelumnya secara langsung. Urutan F (column-major, mengikuti konvensi Fortran) menyimpan kolom demi kolom. Fungsi reshape dan sebagian besar fungsi pembuat array menghasilkan urutan C secara default, sedangkan np.asfortranarray membuat salinan baru dengan urutan F.

Contoh berikut menumpangkan keduanya pada matriks yang sama, lengkap dengan flag kontiguitasnya. Operasi .T dipakai untuk menunjukkan bahwa transpose hanyalah view.

python
import numpy as np

matriks = np.arange(1, 7, dtype=np.int64).reshape(2, 3)
transpose = matriks.T

print(matriks.strides)
print(transpose.strides)
print(matriks.flags["C_CONTIGUOUS"], matriks.flags["F_CONTIGUOUS"])
print(transpose.flags["C_CONTIGUOUS"], transpose.flags["F_CONTIGUOUS"])
text Keluaran
(24, 8)
(8, 24)
True False
False True

Pada matriks berurutan C, pindah satu baris berarti melompat 24 byte, yaitu tiga elemen, sedangkan pindah satu kolom hanya 8 byte. Transpose tidak menyalin apa pun: penyangganya tetap milik matriks, dan yang berubah hanyalah bentuk serta susunan strides menjadi (8, 24). Karena susunannya tidak lagi memenuhi pola urutan C, flag C_CONTIGUOUS transpose bernilai False, meskipun penyangga dasarnya sama sekali tidak berubah.

Salinan berurutan F dapat dibuat dengan np.asfortranarray, dan urutan memori aslinya dapat dilihat melalui ravel(order="K"), yang membaca penyangga menurut susunan memorinya.

python
import numpy as np

matriks = np.arange(1, 7, dtype=np.int64).reshape(2, 3)
urutan_f = np.asfortranarray(matriks)

print(matriks.strides)
print(urutan_f.strides)
print(matriks.ravel(order="K"))
print(urutan_f.ravel(order="K"))
text Keluaran
(24, 8)
(8, 16)
[1 2 3 4 5 6]
[1 4 2 5 3 6]

Diagram berikut menyandingkan kedua penyangga untuk matriks logis yang sama.

Urutan C (baris)123456matriks logis1234560816243240strides = (24, 8) bytebaris: 24 byte per langkah, kolom: 8 byteUrutan F (kolom)123456matriks logis1425360816243240strides = (8, 16) bytebaris: 8 byte per langkah, kolom: 16 byte
Susunan penyangga matriks 2x3 int64 pada urutan C dan urutan F

Kedua panel memuat matriks logis yang sama, tetapi urutan byte di dalam penyangganya berbeda. Penyangga urutan C berisi 1 sampai 6 secara berurutan, sedangkan penyangga urutan F berisi 1, 4, 2, 5, 3, 6 sesuai kolomnya. Nilai strides pada kedua panel saling bertukar karena rumus alamat harus tetap menghasilkan elemen yang benar dari penyangga yang tersusun berbeda.

Mengapa kontiguitas berpengaruh#

Kontiguitas menentukan seberapa jauh lompatan memori pada setiap langkah perulangan. Pada array berurutan C, membaca sepanjang baris mengunjungi alamat yang berurutan, sedangkan membaca sepanjang kolom melompat sejauh ukuran satu baris pada setiap langkah. Selisihnya sudah terlihat pada matriks kecil di atas, yaitu 24 byte berbanding 8 byte, dan membengkak seiring lebar matriksnya.

Ambil matriks persegi berisi sepuluh ribu kolom dengan tipe float64. Ukuran satu barisnya 80.000 byte, sehingga berpindah satu baris berarti melompat sejauh itu. Membaca satu kolom berarti mengunjungi alamat yang saling berjauhan pada setiap langkah, sedangkan membaca satu baris mengalir searah dari awal penyangga hingga akhirnya. Perangkat keras membaca memori dalam blok yang lebih besar daripada satu elemen, sehingga aliran berurutan umumnya dimanfaatkan lebih efisien daripada lompatan berjarak jauh. NumPy tetap menghasilkan jawaban yang benar untuk kedua arah, tetapi kelancaran pembacaan penyangganya berbeda.

Array hasil operasi tidak selalu kontigu. Slice kolom, transpose, dan sebagian operasi lain menghasilkan view yang strides-nya tidak lagi memenuhi pola urutan C, meskipun penyangga dasarnya tidak berubah. Apabila array semacam itu perlu kembali ke urutan C, misalnya sebelum diberikan kepada kode yang menuntut susunan tersebut, fungsi np.ascontiguousarray membuat salinan baru yang penyangganya disusun ulang.

python
import numpy as np

matriks = np.arange(1, 7, dtype=np.int64).reshape(2, 3)
transpose = matriks.T
salinan = np.ascontiguousarray(transpose)

print(transpose.strides)
print(salinan.strides)
print(np.shares_memory(transpose, salinan))
print(salinan.flags["C_CONTIGUOUS"])
text Keluaran
(8, 24)
(16, 8)
False
True

Keluarannya menegaskan tiga hal: strides salinan kembali mengikuti pola urutan C, np.shares_memory melaporkan False karena penyangganya benar-benar baru, dan flag C_CONTIGUOUS kembali bernilai True. Salinan seperti ini berbiaya memori dan waktu sebanding ukuran array, sehingga dipanggil seperlunya, bukan di dalam setiap putaran perulangan.

Kesalahan umum#

Dua kesalahan berikut tidak memunculkan error apa pun, dan justru karena itulah keduanya sulit ditemukan. Keduanya melibatkan fungsi yang terlihat sepele, yaitu ravel dan slicing, pada array yang tata letaknya tidak lagi berurutan C. Keluaran yang tampak benar membuat polanya lolos dari pemeriksaan kasat mata.

Menganggap ravel selalu mengembalikan view#

Fungsi ravel() tanpa argumen membaca elemen menurut urutan C. Pada array berurutan C, urutan baca itu sama dengan urutan memori, sehingga hasilnya berupa view. Pada array berurutan F, urutan baca C tidak sama dengan urutan memori, sehingga ravel diam-diam membuat salinan. Apabila hasilnya dimutasi dengan harapan data aslinya ikut berubah, perubahan itu tidak pernah terjadi, sementara salinan tak terduga justru menambah pemakaian memori.

python
import numpy as np

urutan_f = np.asfortranarray(np.arange(1, 7, dtype=np.int64).reshape(2, 3))
rata_c = urutan_f.ravel()
rata_k = urutan_f.ravel(order="K")

print(np.shares_memory(rata_c, urutan_f))
print(np.shares_memory(rata_k, urutan_f))
print(rata_k)
text Keluaran
False
True
[1 4 2 5 3 6]

Perbaikannya adalah memakai ravel(order="K") yang membaca penyangga menurut urutan memorinya, sehingga keduanya tetap berbagi memori. Apabila urutan baca tertentu memang diinginkan dan salinan diterima, flatten lebih jujur karena selalu menyalin.

Menganggap hasil slicing tetap kontigu#

Slice yang mengambil sebagian kolom menghasilkan view yang penyangganya tetap milik array asal. Strides-nya dipertahankan, sedangkan bentuknya menyusut, sehingga pola urutan C tidak lagi terpenuhi dan flag kontiguitasnya berubah.

python
import numpy as np

matriks = np.arange(12, dtype=np.int64).reshape(3, 4)
kolom = matriks[:, 1:3]

print(kolom.strides)
print(kolom.flags["C_CONTIGUOUS"])
text Keluaran
(32, 8)
False

Keluaran tersebut wajar dan bukan tanda error: view mempertahankan strides array dasarnya. Apabila kontiguitas diperlukan, panggil np.ascontiguousarray atas view tersebut, dengan catatan pemanggilan itu membuat salinan baru sebagaimana dibahas pada bagian sebelumnya.

Ringkasan#

  • strides menyatakan lompatan byte per sumbu, nbytes menyatakan ukuran total penyangga, dan keduanya bersifat deterministik untuk tipe serta bentuk yang sama.
  • Urutan C menyimpan baris demi baris dan urutan F menyimpan kolom demi kolom, sedangkan transpose hanyalah view yang menukar strides tanpa menyalin apa pun.
  • Membaca sepanjang sumbu ber-strides kecil mengalir berurutan di memori, sedangkan sumbu ber-strides besar melompat jauh, sehingga kontiguitas memengaruhi kelancaran pembacaan penyangga.
  • np.ascontiguousarray mengembalikan array ke urutan C melalui salinan baru, dan ravel pada array non-C dapat diam-diam menyalin, sementara ravel(order="K") mengikuti urutan memori.

Referensi#