Mengapa sizeof
operator mengembalikan ukuran yang lebih besar untuk struktur daripada ukuran total anggota struktur?
Mengapa sizeof
operator mengembalikan ukuran yang lebih besar untuk struktur daripada ukuran total anggota struktur?
Jawaban:
Ini karena bantalan ditambahkan untuk memenuhi kendala penyelarasan. Penyelarasan struktur data berdampak pada kinerja dan kebenaran program:
SIGBUS
).Berikut adalah contoh menggunakan pengaturan khas untuk prosesor x86 (semua mode 32 dan 64 bit yang digunakan):
struct X
{
short s; /* 2 bytes */
/* 2 padding bytes */
int i; /* 4 bytes */
char c; /* 1 byte */
/* 3 padding bytes */
};
struct Y
{
int i; /* 4 bytes */
char c; /* 1 byte */
/* 1 padding byte */
short s; /* 2 bytes */
};
struct Z
{
int i; /* 4 bytes */
short s; /* 2 bytes */
char c; /* 1 byte */
/* 1 padding byte */
};
const int sizeX = sizeof(struct X); /* = 12 */
const int sizeY = sizeof(struct Y); /* = 8 */
const int sizeZ = sizeof(struct Z); /* = 8 */
Seseorang dapat meminimalkan ukuran struktur dengan menyortir anggota dengan menyelaraskan (menyortir berdasarkan ukuran sudah cukup untuk itu dalam tipe dasar) (seperti struktur Z
dalam contoh di atas).
CATATAN PENTING: Standar C dan C ++ menyatakan bahwa penyelarasan struktur ditentukan oleh implementasi. Oleh karena itu setiap kompiler dapat memilih untuk menyelaraskan data secara berbeda, menghasilkan tata letak data yang berbeda dan tidak kompatibel. Untuk alasan ini, ketika berhadapan dengan perpustakaan yang akan digunakan oleh kompiler yang berbeda, penting untuk memahami bagaimana kompiler menyelaraskan data. Beberapa kompiler memiliki pengaturan baris perintah dan / atau #pragma
pernyataan khusus untuk mengubah pengaturan penyelarasan struktur.
Pengemasan dan perataan byte, seperti dijelaskan dalam FAQ C di sini :
Ini untuk penyelarasan. Banyak prosesor tidak dapat mengakses kuantitas 2- dan 4-byte (mis. Int dan int panjang) jika mereka dijejalkan dalam segala hal.
Misalkan Anda memiliki struktur ini:
struct { char a[3]; short int b; long int c; char d[3]; };
Sekarang, Anda mungkin berpikir bahwa mungkin untuk mengemas struktur ini ke dalam memori seperti ini:
+-------+-------+-------+-------+ | a | b | +-------+-------+-------+-------+ | b | c | +-------+-------+-------+-------+ | c | d | +-------+-------+-------+-------+
Tetapi jauh lebih mudah pada prosesor jika kompiler mengaturnya seperti ini:
+-------+-------+-------+ | a | +-------+-------+-------+ | b | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | +-------+-------+-------+
Dalam versi dikemas, perhatikan bagaimana setidaknya sedikit sulit bagi Anda dan saya untuk melihat bagaimana bidang b dan c membungkus? Singkatnya, sulit untuk prosesor juga. Oleh karena itu, sebagian besar penyusun akan memasang struktur (seolah-olah dengan bidang ekstra dan tak terlihat) seperti ini:
+-------+-------+-------+-------+ | a | pad1 | +-------+-------+-------+-------+ | b | pad2 | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | pad3 | +-------+-------+-------+-------+
s
maka &s.a == &s
dan &s.d == &s + 12
(diberi perataan yang ditunjukkan dalam jawaban) Pointer hanya disimpan jika array memiliki ukuran variabel (misalnya, a
dideklarasikan char a[]
sebagai ganti char a[3]
), tetapi kemudian elemen harus disimpan di tempat lain.
Jika Anda ingin struktur memiliki ukuran tertentu dengan GCC misalnya gunakan __attribute__((packed))
.
Pada Windows Anda dapat mengatur perataan ke satu byte saat menggunakan compier cl.exe dengan opsi / Zp .
Biasanya lebih mudah bagi CPU untuk mengakses data yang merupakan kelipatan dari 4 (atau 8), tergantung platform dan juga pada kompiler.
Jadi pada dasarnya ini adalah masalah penyelarasan.
Anda harus punya alasan kuat untuk mengubahnya.
Ini bisa disebabkan oleh perataan byte dan padding sehingga struktur keluar hingga jumlah byte (atau kata-kata) yang merata pada platform Anda. Misalnya dalam C di Linux, 3 struktur berikut:
#include "stdio.h"
struct oneInt {
int x;
};
struct twoInts {
int x;
int y;
};
struct someBits {
int x:2;
int y:6;
};
int main (int argc, char** argv) {
printf("oneInt=%zu\n",sizeof(struct oneInt));
printf("twoInts=%zu\n",sizeof(struct twoInts));
printf("someBits=%zu\n",sizeof(struct someBits));
return 0;
}
Memiliki anggota yang ukurannya (dalam byte) masing-masing adalah 4 byte (32 bit), 8 byte (2x 32 bit) dan 1 byte (2 + 6 bit). Program di atas (di Linux menggunakan gcc) mencetak ukuran 4, 8, dan 4 - di mana struktur terakhir diisi sehingga menjadi satu kata (4 x 8 bit byte pada platform 32bit saya).
oneInt=4
twoInts=8
someBits=4
:2
dan :6
sebenarnya menentukan 2 dan 6 bit, bukan bilangan bulat 32 bit penuh dalam kasus ini. someBits.x, karena hanya 2 bit hanya dapat menyimpan 4 nilai yang mungkin: 00, 01, 10, dan 11 (1, 2, 3 dan 4). Apakah ini masuk akal? Inilah artikel tentang fitur ini: geeksforgeeks.org/bit-fields-c
Lihat juga:
untuk Microsoft Visual C:
http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=vs.80%29.aspx
dan kompatibilitas klaim GCC dengan kompiler Microsoft .:
http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html
Selain jawaban sebelumnya, harap perhatikan bahwa terlepas dari kemasannya, tidak ada jaminan pesanan anggota di C ++ . Compiler dapat (dan tentu saja melakukannya) menambahkan pointer tabel virtual dan anggota struktur dasar ke struktur. Bahkan keberadaan tabel virtual tidak dijamin oleh standar (implementasi mekanisme virtual tidak ditentukan) dan oleh karena itu orang dapat menyimpulkan bahwa jaminan tersebut tidak mungkin.
Saya yakin anggota-order yang dijamin dalam C , tapi aku tidak akan mengandalkan itu, ketika menulis sebuah cross-platform atau cross-compiler Program.
Ukuran struktur lebih besar daripada jumlah bagian-bagiannya karena apa yang disebut pengepakan. Prosesor tertentu memiliki ukuran data yang disukai yang bekerja dengannya. Ukuran prosesor paling modern lebih disukai jika 32-bit (4 byte). Mengakses memori ketika data pada batas jenis ini lebih efisien daripada hal-hal yang mengangkangi batas ukuran itu.
Sebagai contoh. Pertimbangkan struktur sederhana:
struct myStruct
{
int a;
char b;
int c;
} data;
Jika mesin adalah mesin 32-bit dan data disejajarkan pada batas 32-bit, kami melihat masalah langsung (dengan asumsi tidak ada struktur keselarasan). Dalam contoh ini, mari kita asumsikan bahwa data struktur dimulai pada alamat 1024 (0x400 - perhatikan bahwa 2 bit terendah adalah nol, sehingga data disejajarkan dengan batas 32-bit). Akses ke data.a akan berfungsi dengan baik karena dimulai pada batas - 0x400. Akses ke data.b juga akan berfungsi dengan baik, karena berada di alamat 0x404 - batas 32-bit lainnya. Tetapi struktur yang tidak selaras akan menempatkan data.c di alamat 0x405. 4 byte data.c berada di 0x405, 0x406, 0x407, 0x408. Pada mesin 32-bit, sistem akan membaca data.c selama satu siklus memori, tetapi hanya akan mendapatkan 3 dari 4 byte (byte ke-4 berada pada batas berikutnya). Jadi, sistem harus melakukan akses memori kedua untuk mendapatkan byte ke-4,
Sekarang, jika alih-alih meletakkan data.c di alamat 0x405, kompiler mengisi struktur dengan 3 byte dan meletakkan data.c di alamat 0x408, maka sistem hanya perlu 1 siklus untuk membaca data, memotong waktu akses ke elemen data tersebut sebesar 50%. Padding menukar efisiensi memori untuk efisiensi pemrosesan. Mengingat bahwa komputer dapat memiliki jumlah memori yang sangat besar (banyak gigabytes), kompiler merasa bahwa swap (kecepatan lebih dari ukuran) adalah wajar.
Sayangnya, masalah ini menjadi pembunuh ketika Anda mencoba mengirim struktur melalui jaringan atau bahkan menulis data biner ke file biner. Padding yang disisipkan di antara elemen struktur atau kelas dapat mengganggu data yang dikirim ke file atau jaringan. Untuk menulis kode portabel (yang akan menuju ke beberapa kompiler berbeda), Anda mungkin harus mengakses setiap elemen struktur secara terpisah untuk memastikan "pengepakan" yang tepat.
Di sisi lain, kompiler yang berbeda memiliki kemampuan berbeda untuk mengelola pengemasan struktur data. Misalnya, dalam Visual C / C ++ kompiler mendukung perintah paket #pragma. Ini akan memungkinkan Anda untuk menyesuaikan pengemasan dan penyelarasan data.
Sebagai contoh:
#pragma pack 1
struct MyStruct
{
int a;
char b;
int c;
short d;
} myData;
I = sizeof(myData);
Saya sekarang harus memiliki panjang 11. Tanpa pragma, saya bisa apa saja dari 11 hingga 14 (dan untuk beberapa sistem, sebanyak 32), tergantung pada kemasan default dari kompiler.
#pragma pack
. Jika anggota dialokasikan pada penyelarasan default mereka, saya biasanya mengatakan struktur tidak dikemas.
Ini dapat dilakukan jika Anda secara implisit atau eksplisit mengatur penyelarasan struct. Sebuah struct yang disejajarkan 4 akan selalu menjadi kelipatan dari 4 byte bahkan jika ukuran anggotanya akan menjadi sesuatu yang bukan kelipatan dari 4 byte.
Juga sebuah perpustakaan dapat dikompilasi di bawah x86 dengan int 32-bit dan Anda dapat membandingkan komponen-komponennya pada proses 64-bit akan memberi Anda hasil yang berbeda jika Anda melakukannya dengan tangan.
C99 N1256 draft standar
http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf
6.5.3.4 Ukuran operator :
3 Ketika diterapkan pada operan yang memiliki tipe struktur atau gabungan, hasilnya adalah jumlah total byte dalam objek tersebut, termasuk padding internal dan trailing.
6.7.2.1 Penentu struktur dan serikat :
13 ... Mungkin ada bantalan yang tidak disebutkan namanya dalam objek struktur, tetapi tidak pada awalnya.
dan:
15 Mungkin ada bantalan yang tidak disebutkan namanya di ujung struktur atau gabungan.
Fitur anggota array fleksibel C99 baru ( struct S {int is[];};
) juga dapat mempengaruhi bantalan:
16 Sebagai kasus khusus, elemen terakhir dari suatu struktur dengan lebih dari satu anggota bernama dapat memiliki tipe array yang tidak lengkap; ini disebut anggota array yang fleksibel. Dalam sebagian besar situasi, anggota array fleksibel diabaikan. Secara khusus, ukuran struktur adalah seolah-olah anggota larik fleksibel dihilangkan kecuali bahwa itu mungkin memiliki lebih banyak jejak daripada yang akan disiratkan kelalaian.
Lampiran J Masalah Portabilitas mengulangi:
Berikut ini tidak ditentukan: ...
- Nilai padding byte saat menyimpan nilai dalam struktur atau serikat pekerja (6.2.6.1)
C ++ 11 N3337 konsep standar
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
5.3.3 Ukuran :
2 Ketika diterapkan pada suatu kelas, hasilnya adalah jumlah byte dalam suatu objek dari kelas itu termasuk setiap bantalan yang diperlukan untuk menempatkan objek jenis itu dalam sebuah array.
9.2 Anggota kelas :
Sebuah penunjuk ke objek struct tata letak standar, yang sesuai dikonversi menggunakan reinterpret_cast, menunjuk ke anggota awalnya (atau jika anggota itu adalah bidang-bit, kemudian ke unit di mana ia berada) dan sebaliknya. [Catatan: Karena itu mungkin ada padding tanpa nama di dalam objek struct tata letak standar, tetapi tidak pada awalnya, yang diperlukan untuk mencapai penyelarasan yang tepat. - catatan akhir]
Saya hanya tahu cukup C ++ untuk memahami catatan :-)
Selain jawaban lain, sebuah struct dapat (tetapi biasanya tidak) memiliki fungsi virtual, dalam hal ini ukuran dari struct juga akan mencakup ruang untuk vtbl.
Bahasa C membuat kompiler memiliki kebebasan tentang lokasi elemen struktural dalam memori:
Bahasa C memberikan jaminan kepada programmer tentang tata letak elemen dalam struktur:
Masalah yang terkait dengan penyelarasan elemen:
Cara kerja pelurusan:
ps. Informasi lebih rinci tersedia di sini: "Referensi Samuel P. Harbison, Guy L.Steele CA, (5.6.2 - 5.6.7)"
Idenya adalah untuk pertimbangan kecepatan dan cache, operan harus dibaca dari alamat yang disesuaikan dengan ukuran alami mereka. Untuk membuat ini terjadi, kompiler bantalan struktur anggota sehingga anggota berikut atau struct berikut akan disejajarkan.
struct pixel {
unsigned char red; // 0
unsigned char green; // 1
unsigned int alpha; // 4 (gotta skip to an aligned offset)
unsigned char blue; // 8 (then skip 9 10 11)
};
// next offset: 12
Arsitektur x86 selalu dapat mengambil alamat yang tidak selaras. Namun, itu lebih lambat dan ketika misalignment tumpang tindih dua garis cache yang berbeda, maka itu akan membatalkan dua baris cache ketika akses yang selaras hanya akan mengusir satu.
Beberapa arsitektur benar-benar harus menjebak membaca dan menulis yang tidak selaras, dan versi awal arsitektur ARM (yang berevolusi menjadi semua CPU seluler saat ini) ... well, mereka sebenarnya baru saja mengembalikan data buruk untuk itu. (Mereka mengabaikan bit orde rendah.)
Akhirnya, perhatikan bahwa garis cache bisa menjadi besar secara sewenang-wenang, dan kompiler tidak berusaha menebaknya atau membuat tradeoff ruang-vs-kecepatan. Sebaliknya, keputusan penyelarasan adalah bagian dari ABI dan mewakili penyelarasan minimum yang pada akhirnya akan secara merata mengisi garis cache.
TL; DR: penyelarasan itu penting.