Apakah ada cara standar atau alternatif standar untuk mengemas struct di c?

13

Ketika pemrograman dalam CI telah menemukan itu sangat berharga untuk mengemas struct menggunakan __attribute__((__packed__))atribut GCCs sehingga saya dapat dengan mudah mengkonversi potongan terstruktur dari memori volatile ke array byte yang akan ditransmisikan melalui bus, disimpan ke penyimpanan atau diterapkan ke blok register. Packs struct menjamin bahwa ketika diperlakukan sebagai array byte tidak akan mengandung padding, yang keduanya boros, risiko keamanan yang mungkin dan mungkin tidak kompatibel ketika dengan interfacing perangkat keras.

Apakah tidak ada standar untuk packing struct yang berfungsi di semua kompiler C? Jika tidak maka saya seorang outlier dalam berpikir ini adalah fitur penting untuk pemrograman sistem? Apakah pengguna awal bahasa C tidak menemukan kebutuhan untuk mengemas struct atau apakah ada beberapa alternatif?

saturinine
sumber
menggunakan struct di seluruh domain kompilasi adalah ide yang sangat buruk, khususnya untuk menunjuk pada perangkat keras (yang merupakan domain kompilasi lain). paket struct hanya satu trik untuk melakukan ini, mereka memiliki banyak efek samping yang buruk, jadi ada banyak solusi lain untuk masalah Anda dengan efek samping yang lebih sedikit, dan yang lebih portabel.
old_timer

Jawaban:

12

Dalam sebuah struct, yang penting adalah offset dari masing-masing anggota dari alamat setiap instance struct. Tidak terlalu banyak masalah seberapa ketat hal-hal yang dikemas.

Sebuah array, bagaimanapun, penting dalam bagaimana "dikemas". Aturan dalam C adalah bahwa setiap elemen array persis N byte dari sebelumnya, di mana N adalah jumlah byte yang digunakan untuk menyimpan tipe itu.

Tetapi dengan struct, tidak ada kebutuhan untuk keseragaman.

Inilah salah satu contoh skema pengemasan yang aneh:

Freescale (yang membuat mikrokontroler otomotif) membuat mikro yang memiliki co-prosesor Time Processing Unit (google for eTPU atau TPU). Ini memiliki dua ukuran data asli, 8 bit dan 24 bit, dan hanya berkaitan dengan bilangan bulat.

Struct ini:

struct a
{
  U24 elementA;
  U24 elementB;
};

akan melihat setiap U24 menyimpan blok 32 bitnya sendiri, tetapi hanya di area alamat tertinggi.

Ini:

struct b
{
  U24 elementA;
  U24 elementB;
  U8  elementC;
};

akan memiliki dua U24 disimpan di blok 32 bit yang berdekatan, dan U8 akan disimpan dalam "lubang" di depan U24 pertama elementA,.

Tetapi Anda dapat memberitahu kompiler untuk mengemas semuanya ke dalam blok 32 bitnya sendiri, jika Anda mau; ini lebih mahal pada RAM tetapi menggunakan lebih sedikit instruksi untuk mengakses.

"packing" tidak berarti "packing dengan ketat" - itu hanya berarti beberapa skema untuk mengatur elemen struct menggunakan offset.

Tidak ada skema generik, itu tergantung pada kompiler + arsitektur.

RichColours
sumber
1
Jika kompiler untuk TPU mengatur ulang struct buntuk bergerak elementCsebelum elemen lainnya, maka itu bukan kompiler C yang sesuai. Penataan ulang elemen tidak diizinkan di C
Bart van Ingen Schenau
Menarik tetapi U24 bukan tipe C standar en.m.wikipedia.org/wiki/C_data_types sehingga tidak mengherankan bahwa kompilator dipaksa untuk menanganinya dengan cara yang agak aneh.
satur9nine
Ini berbagi RAM dengan inti CPU utama yang memiliki ukuran kata 32 bit. Tetapi prosesor ini memiliki ALU yang hanya menangani 24 bit atau 8 bit. Jadi ia memiliki skema untuk meletakkan angka 24 bit dalam 32 bit kata. Non-standar, tetapi contoh yang bagus untuk pengemasan dan pelurusan. Setuju, ini sangat tidak standar.
RichColours
6

Ketika pemrograman dalam CI telah menemukan itu sangat berharga untuk mengemas struct menggunakan GCCs __attribute__((__packed__))[...]

Karena Anda menyebutkan __attribute__((__packed__)), saya menganggap niat Anda adalah untuk menghilangkan semua padding dalam a struct(membuat setiap anggota memiliki keselarasan 1-byte).

Apakah tidak ada standar untuk packing struct yang berfungsi di semua kompiler C?

... Dan jawabannya adalah tidak". Padding dan penyelarasan data relatif terhadap sebuah struct (dan array berdekatan dari struct dalam stack atau heap) ada karena alasan penting. Pada banyak mesin, akses memori yang tidak selaras dapat menyebabkan potensi penalti kinerja yang signifikan (meskipun menjadi kurang pada beberapa perangkat keras yang lebih baru). Dalam beberapa skenario kasus yang jarang terjadi, akses memori yang tidak selaras menyebabkan kesalahan bus yang tidak dapat dipulihkan (bahkan dapat merusak seluruh sistem operasi).

Karena standar C difokuskan pada portabilitas, tidak masuk akal untuk memiliki cara standar untuk menghilangkan semua lapisan dalam struktur dan hanya membiarkan bidang yang sewenang-wenang diselaraskan, karena dengan melakukan hal itu berpotensi berisiko membuat kode C non-portabel.

Cara teraman dan paling portabel untuk mengeluarkan data seperti itu ke sumber eksternal dengan cara yang menghilangkan semua lapisan adalah dengan membuat serial ke / dari byte stream alih-alih hanya mencoba mengirim isi memori mentah Anda structs. Itu juga mencegah program Anda dari menderita hukuman kinerja di luar konteks serialisasi ini, dan juga akan memungkinkan Anda untuk menambahkan bidang baru ke bebas structtanpa membuang dan merusak seluruh perangkat lunak. Ini juga akan memberi Anda ruang untuk mengatasi endianness dan hal-hal seperti itu jika itu menjadi masalah.

Ada satu cara untuk menghilangkan semua padding tanpa mencapai arahan khusus compiler, meskipun itu hanya berlaku jika urutan relatif antara bidang tidak masalah. Diberikan sesuatu seperti ini:

struct Foo
{
    double x;  // assume 8-byte alignment
    char y;    // assume 1-byte alignment
               // 7 bytes of padding for first field
};

... kita membutuhkan padding untuk akses memori yang disejajarkan relatif ke alamat struktur yang berisi bidang-bidang ini, seperti:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______y.......x_______y.......x_______y.......x_______y.......

... di mana .menunjukkan padding. Setiap orang xharus menyelaraskan ke batas 8-byte untuk kinerja (dan kadang-kadang bahkan memperbaiki perilaku).

Anda dapat menghilangkan padding dengan cara portabel dengan menggunakan representasi SoA (structure of array) seperti itu (mari kita asumsikan kita membutuhkan 8 Fooinstance):

struct Foos
{
   double x[8];
   char y[8];
};

Kami telah secara efektif menghancurkan struktur. Dalam hal ini, representasi memori menjadi seperti ini:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______x_______x_______x_______x_______x_______x_______x_______

... dan ini:

01234567
yyyyyyyy

... tidak ada lagi padding overhead, dan tanpa melibatkan akses memori yang tidak selaras karena kita tidak lagi mengakses bidang data ini sebagai penyeimbang alamat struktur, melainkan sebagai penyeimbang alamat dasar untuk apa yang secara efektif sebuah array.

Ini juga membawa bonus menjadi lebih cepat untuk akses berurutan sebagai akibat dari lebih sedikit data untuk dikonsumsi (tidak ada padding yang tidak relevan dalam campuran untuk memperlambat laju konsumsi data yang relevan dengan mesin) dan juga potensi bagi kompiler untuk melakukan vectorisasi pemrosesan dengan sangat sepele. .

The downside adalah bahwa itu adalah PITA ke kode. Ini juga berpotensi kurang efisien untuk akses acak dengan langkah lebih besar di antara bidang, di mana sering repetisi AoS atau AoSoA akan melakukan lebih baik. Tapi itu satu cara standar untuk menghilangkan bantalan dan mengemas barang sekencang mungkin tanpa mengacaukan segalanya.

ChrisF
sumber
2
Saya berpendapat bahwa memiliki sarana menentukan tata letak struktur secara eksplisit akan meningkatkan portabilitas secara besar-besaran . Sementara beberapa tata letak akan menghasilkan kode yang sangat efisien pada beberapa mesin dan kode yang sangat tidak efisien pada yang lain, kode tersebut akan bekerja pada semua mesin dan akan efisien pada setidaknya beberapa. Sebaliknya, dengan tidak adanya fitur seperti itu, satu-satunya cara untuk membuat kode berfungsi pada semua mesin adalah membuatnya menjadi tidak efisien pada semua mesin atau menggunakan banyak makro dan kompilasi bersyarat untuk menggabungkan non-portable yang cepat program dan yang portabel yang lambat di sumber yang sama.
supercat
Secara konseptual ya, jika kita dapat menentukan semuanya menjadi representasi bit dan byte, persyaratan penyelarasan, endianness, dll dan memiliki fitur yang memungkinkan kontrol eksplisit seperti itu di C sementara secara opsional menceraikannya lebih jauh dari arsitektur yang mendasarinya ... Tapi saya hanya berbicara tentang ATM - saat ini solusi yang paling portabel untuk serializer adalah menulisnya sedemikian rupa sehingga tidak tergantung pada representasi bit dan byte yang tepat dan penyelarasan tipe data. Sayangnya kami kekurangan sarana ATM untuk melakukan sebaliknya secara efektif (dalam C).
5

Tidak semua arsitektur sama, cukup aktifkan opsi 32 bit pada satu modul, dan lihat apa yang terjadi ketika menggunakan kode sumber yang sama dan kompiler yang sama. Pesanan Byte adalah batasan terkenal lainnya. Lemparkan dalam representasi floating point dan masalahnya menjadi lebih buruk. Menggunakan Packing untuk mengirim data biner adalah non-portabel. Untuk membakukannya sehingga bisa digunakan secara praktis, Anda perlu mendefinisikan ulang spesifikasi Bahasa C.

Meskipun umum, menggunakan Pack untuk mengirim data biner adalah ide yang buruk jika Anda menginginkan keamanan, portabilitas atau umur panjang data. Seberapa sering Anda membaca gumpalan biner dari sumber ke dalam program Anda. Seberapa sering Anda memeriksa semua nilai yang waras, bahwa peretas atau perubahan program belum 'mendapatkan' data? Pada saat Anda mengkodekan rutin pemeriksaan, Anda mungkin juga menggunakan rutin impor dan ekspor.

mattnz
sumber
0

Alternatif yang sangat umum adalah "namding padding":

struct s {
  short s1;
  char  c2;
  char  reserved; // Padding
};

Ini tidak menganggap struktur tidak akan melangkah ke 8 byte.

MSalters
sumber