Struct

Tata letak field, alignment, dan padding dalam memori.

Daftar isi

Struct mengelompokkan beberapa field dengan tipe yang berbeda ke dalam satu nilai. Topik ini membahas deklarasi struct dan literalnya, penanganan pointer ke struct, lalu tata letak field di dalam memori. Setelah membaca, proc bawaan size_of, align_of, dan offset_of dapat digunakan untuk menjelaskan mengapa dua struct dengan field yang sama memiliki ukuran berbeda. Urutan field yang membuat struct menjadi ringkas juga dapat ditentukan.

Deklarasi struct dan literal#

Struct dideklarasikan dengan kata kunci :: struct diikuti daftar field. Nilai struct baru dibuat melalui struct literal, yaitu ekspresi yang mengisi setiap field di antara tanda kurung kurawal. Literal dengan label field boleh menyebut field dalam urutan apa pun, sedangkan literal tanpa label wajib mengikuti urutan deklarasi. Field diakses dengan operator titik, dan struct yang tidak diisi menghasilkan nilai nol untuk setiap fieldnya.

Program berikut menunjukkan literal, akses field, pointer, dan penyalinan struct.

odin main.odin
package main

import "core:fmt"

Titik :: struct {
        x: f32,
        y: f32,
}

Data_Mahasiswa :: struct {
        nim:      u32,
        nama:     string,
        ipk:      f32,
        semester: u8,
}

main :: proc() {
        // literal struct dengan label field
        t := Titik{x = 1.5, y = 2.5}
        fmt.println(t)

        // literal tanpa label, urutan field harus sesuai deklarasi
        u := Titik{3.0, 4.0}
        fmt.println(u)

        // akses field dan pointer ke struct
        p := &t
        fmt.println("lewat pointer:", p.x, p.y)
        p.y = 4.5
        fmt.println("sesudah p.y = 4.5:", t)

        // struct disalin nilai demi nilai saat ditugaskan
        salinan := t
        salinan.x = 99
        fmt.println("asli:", t.x, "salinan:", salinan.x)

        // struct berisi field beragam tipe
        mhs := Data_Mahasiswa{nim = 21045, nama = "Citra", ipk = 3.72, semester = 6}
        fmt.println(mhs)

        // nilai nol untuk setiap field bila literal kosong
        kosong := Data_Mahasiswa{}
        fmt.println(kosong)
}
bash
odin run main.odin -file
text Keluaran
Titik{x = 1.5, y = 2.5}
Titik{x = 3, y = 4}
lewat pointer: 1.5 2.5
sesudah p.y = 4.5: Titik{x = 1.5, y = 4.5}
asli: 1.5 salinan: 99
Data_Mahasiswa{nim = 21045, nama = "Citra", ipk = 3.72, semester = 6}
Data_Mahasiswa{nim = 0, nama = "", ipk = 0, semester = 0}

Perhatikan p.y = 4.5. Variabel p bertipe pointer ke Titik, tetapi field tetap diakses dengan satu titik saja. Odin menyederhanakan penulisan tersebut sehingga dereferensi eksplisit p^.y tidak perlu ditulis untuk akses field. Perhatikan pula penugasan salinan := t. Struct disalin nilai demi nilai, sehingga perubahan pada salinan tidak menyentuh aslinya. Perilaku ini berbeda dari slice pada topik berikutnya, yang merujuk data yang sama.

Alignment dan padding#

Setiap tipe memiliki alignment, yaitu jarak byte yang kelipatannya menjadi posisi mulai yang disukai bagi nilai bertipe itu. Tipe u32 dan f32 memiliki alignment 4, sedangkan f64 dan string memiliki alignment 8 pada target 64-bit. Ketika field berikutnya tidak menempati posisi yang sesuai dengan alignmentnya, compiler menyisipkan padding, yaitu byte tak terpakai yang menunda field hingga posisi yang disukai. Padding menjaga akses memori tetap efisien karena banyak arsitektur membaca memori lebih cepat pada alamat yang selaras.

Tiga proc bawaan bahasa berikut mengukur tata letak tersebut. size_of mengembalikan ukuran tipe dalam byte, align_of mengembalikan alignmentnya, dan offset_of mengembalikan jarak field dari awal struct. Argumen field pada offset_of ditulis sebagai nama field, bukan sebagai string.

odin tata-letak.odin
package main

import "core:fmt"

Data_Mahasiswa :: struct {
        nim:      u32,
        nama:     string,
        ipk:      f32,
        semester: u8,
}

Berat_A :: struct {
        a: u8,
        b: f64,
        c: u8,
}

Berat_B :: struct {
        b: f64,
        a: u8,
        c: u8,
}

Kotak_32 :: struct #align(32) {
        nilai: u8,
}

main :: proc() {
        fmt.println("Data_Mahasiswa: ukuran =", size_of(Data_Mahasiswa), "align =", align_of(Data_Mahasiswa))
        fmt.println("offset nim =", offset_of(Data_Mahasiswa, nim))
        fmt.println("offset nama =", offset_of(Data_Mahasiswa, nama))
        fmt.println("offset ipk =", offset_of(Data_Mahasiswa, ipk))
        fmt.println("offset semester =", offset_of(Data_Mahasiswa, semester))

        fmt.println("Berat_A: ukuran =", size_of(Berat_A), "offset a =", offset_of(Berat_A, a), "b =", offset_of(Berat_A, b), "c =", offset_of(Berat_A, c))
        fmt.println("Berat_B: ukuran =", size_of(Berat_B), "offset b =", offset_of(Berat_B, b), "a =", offset_of(Berat_B, a), "c =", offset_of(Berat_B, c))

        fmt.println("Kotak_32: ukuran =", size_of(Kotak_32), "align =", align_of(Kotak_32))
}
bash
odin run tata-letak.odin -file
text Keluaran
Data_Mahasiswa: ukuran = 32 align = 8
offset nim = 0
offset nama = 8
offset ipk = 24
offset semester = 28
Berat_A: ukuran = 24 offset a = 0 b = 8 c = 16
Berat_B: ukuran = 16 offset b = 0 a = 8 c = 9
Kotak_32: ukuran = 32 align = 32

Angka-angka pada keluaran dapat dihitung ulang dari ukuran dan alignment tiap tipe berikut. Struct Data_Mahasiswa menempatkan nim pada offset 0. Field nama bertipe string yang berukuran 16 byte dengan alignment 8, sehingga empat byte padding disisipkan setelah nim agar nama mulai pada offset 8. Setelah nama berakhir pada offset 24, field ipk mengisi offset 24 sampai 27, lalu semester menempati offset 28. Ukuran akhir struct dibulatkan menjadi kelipatan alignment terbesarnya, sehingga 29 byte isi menjadi 32 byte dengan tiga byte padding di akhir.

Diagram berikut menggambarkan tata letak 32 byte tersebut beserta area paddingnya.

nimpaddingnamaipksemester (offset 28)offset 048242831ukuran total 32 byte, alignment 8 byte, area kuning adalah padding
Tata letak memori Data_Mahasiswa beserta padding

Aturan pembulatan ukuran juga terlihat pada Data_Mahasiswa. Field semester menempati satu byte pada offset 28, sehingga isi struct berakhir sebelum offset 29, tetapi ukuran struct menjadi 32, yaitu kelipatan alignment terbesarnya yang bernilai 8. Alasannya terletak pada array: elemen berikutnya dalam [N]Data_Mahasiswa mulai pada kelipatan ukuran struct, dan elemen itu juga harus memenuhi alignment struct, sehingga ukuran struct wajib merupakan kelipatan alignmentnya. Ketiga proc pengukuran itu bekerja pada waktu kompilasi dan tidak menambah biaya apa pun saat program berjalan, sehingga memeriksa tata letak suatu tipe aman dilakukan kapan pun diperlukan.

Urutan field memengaruhi ukuran#

Dua struct Berat_A dan Berat_B berisi field yang sama, yaitu dua u8 dan satu f64, tetapi dinyatakan dengan urutan berbeda. Struct Berat_A berukuran 24 byte karena field b yang beralignment 8 harus ditunda sampai offset 8, sehingga tujuh byte padding terbentuk setelah a dan tujuh byte lagi setelah c. Struct Berat_B menyatakan b terlebih dahulu, sehingga kedua field u8 menumpuk rapat pada offset 8 dan 9 dan totalnya hanya 16 byte. Selisih 8 byte pada satu struct akan berlipat bila struct tersebut disimpan dalam jumlah besar, misalnya sebagai array nilai ujian per peserta.

Praktik yang mengurangi padding adalah menyatakan field dari yang berukuran terbesar ke yang terkecil. Padding tetap diperlukan untuk menjaga alignment, tetapi posisinya berpindah ke akhir struct dan jumlahnya minimal. Compiler tidak menyusun ulang field secara otomatis, karena urutan field yang ditulis di kode adalah urutan yang digunakan di memori.

Struct tag #align dan #soa#

Deklarasi struct dapat menerima struct tag, yaitu anotasi yang ditulis setelah kata struct. Tag #align(32) menaikkan alignment minimal struct menjadi 32 byte, seperti pada Kotak_32 yang berukuran 32 byte walau isinya hanya satu u8. Penggunaannya antara lain untuk data yang selaras dengan lebar baris cache. Tag #soa membentuk variasi struct yang menyimpan setiap field sebagai array terpisah, sehingga cocok untuk data yang banyak diproses secara paralel. Tata letak #soa beserta alasan performansinya dibahas pada modul pola berorientasi data.

Ringkasan#

  • Struct dideklarasikan dengan daftar field, dibuat melalui literal berlabel maupun tanpa label, dan disalin nilai demi nilai saat ditugaskan.
  • Pointer ke struct mengakses field dengan satu operator titik tanpa dereferensi eksplisit.
  • size_of, align_of, dan offset_of mengungkap tata letak memori: ukuran, alignment, dan posisi field.
  • Padding disisipkan agar field menempati posisi sesuai alignmentnya, dan urutan field menentukan banyaknya padding.
  • Tag #align menaikkan alignment minimal, sedangkan tag #soa mengubah penyimpanan field menjadi array terpisah.

Referensi#