เหตุใดsizeof
ผู้ประกอบการจึงส่งคืนขนาดที่ใหญ่กว่าสำหรับโครงสร้างมากกว่าขนาดทั้งหมดของสมาชิกโครงสร้าง
เหตุใดsizeof
ผู้ประกอบการจึงส่งคืนขนาดที่ใหญ่กว่าสำหรับโครงสร้างมากกว่าขนาดทั้งหมดของสมาชิกโครงสร้าง
คำตอบ:
นี่เป็นเพราะมีการเพิ่มการเติมเพื่อตอบสนองข้อ จำกัด การจัดตำแหน่ง การจัดโครงสร้างข้อมูลส่งผลกระทบต่อทั้งประสิทธิภาพและความถูกต้องของโปรแกรม:
SIGBUS
)นี่คือตัวอย่างการใช้การตั้งค่าทั่วไปสำหรับโปรเซสเซอร์ x86 (ทั้งหมดใช้โหมด 32 และ 64 บิต):
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 */
หนึ่งสามารถลดขนาดของโครงสร้างโดยการเรียงลำดับสมาชิกโดยการจัดตำแหน่ง (เรียงลำดับตามขนาดพอเพียงสำหรับประเภทพื้นฐาน) (เช่นโครงสร้างZ
ในตัวอย่างด้านบน)
หมายเหตุสำคัญ: ทั้งมาตรฐาน C และ C ++ ระบุว่าการจัดตำแหน่งโครงสร้างถูกกำหนดให้นำไปใช้งาน ดังนั้นคอมไพเลอร์แต่ละคนอาจเลือกที่จะจัดตำแหน่งข้อมูลที่แตกต่างกันส่งผลให้รูปแบบข้อมูลที่แตกต่างและเข้ากันไม่ได้ ด้วยเหตุผลนี้เมื่อต้องจัดการกับไลบรารีที่จะใช้คอมไพเลอร์ต่างกันสิ่งสำคัญคือต้องเข้าใจว่าคอมไพเลอร์จัดเรียงข้อมูลอย่างไร คอมไพเลอร์บางตัวมีการตั้งค่าบรรทัดคำสั่งและ / หรือ#pragma
ข้อความพิเศษเพื่อเปลี่ยนการตั้งค่าการจัดแนวโครงสร้าง
การบรรจุและการจัดตำแหน่งไบต์ตามที่อธิบายไว้ในคำถามที่พบบ่อย C ที่นี่ :
มันสำหรับการจัดตำแหน่ง โปรเซสเซอร์จำนวนมากไม่สามารถเข้าถึงปริมาณ 2- และ 4 ไบต์ (เช่น ints และ int ยาว) หากพวกเขาหนาตาในทุกทาง
สมมติว่าคุณมีโครงสร้างนี้:
struct { char a[3]; short int b; long int c; char d[3]; };
ตอนนี้คุณอาจคิดว่ามันควรจะเป็นไปได้ที่จะแพ็คโครงสร้างนี้ลงในหน่วยความจำเช่นนี้:
+-------+-------+-------+-------+ | a | b | +-------+-------+-------+-------+ | b | c | +-------+-------+-------+-------+ | c | d | +-------+-------+-------+-------+
แต่มันก็ง่ายกว่ามากหากโปรเซสเซอร์ใช้คอมไพเลอร์จัดเรียงมัน:
+-------+-------+-------+ | a | +-------+-------+-------+ | b | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | +-------+-------+-------+
ในรุ่นที่บรรจุให้สังเกตว่าอย่างน้อยมันก็ยากสำหรับคุณและฉันที่จะดูว่าฟิลด์ b และ c ล้อมรอบอย่างไร? โดยสรุปแล้วมันก็ยากสำหรับโปรเซสเซอร์เช่นกัน ดังนั้นคอมไพเลอร์ส่วนใหญ่จะวางโครงสร้าง (ราวกับมีฟิลด์พิเศษที่มองไม่เห็น) ดังนี้:
+-------+-------+-------+-------+ | a | pad1 | +-------+-------+-------+-------+ | b | pad2 | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | pad3 | +-------+-------+-------+-------+
s
แล้ว&s.a == &s
และ&s.d == &s + 12
(ได้รับการจัดตำแหน่งที่แสดงในคำตอบ) ตัวชี้จะถูกจัดเก็บเฉพาะในกรณีที่อาร์เรย์มีขนาดตัวแปร (เช่นa
ถูกประกาศchar a[]
แทนchar a[3]
) แต่จากนั้นองค์ประกอบจะต้องเก็บไว้ที่อื่น
ถ้าคุณต้องการให้โครงสร้างมีขนาดที่แน่นอนด้วย GCC ตัวอย่างเช่นใช้ __attribute__((packed))
ตัวอย่างเช่นการใช้งาน
บน Windows คุณสามารถตั้งค่าการจัดตำแหน่งเป็นหนึ่งไบต์เมื่อใช้คอมไพเลอร์ cl.exe ด้วยตัวเลือก / Zpตัวเลือก
โดยปกติจะง่ายกว่าสำหรับ CPU ในการเข้าถึงข้อมูลที่มีหลาย ๆ 4 (หรือ 8) ขึ้นอยู่กับแพลตฟอร์มและคอมไพเลอร์
ดังนั้นจึงเป็นเรื่องของการจัดตำแหน่งโดยทั่วไป
คุณต้องมีเหตุผลที่ดีในการเปลี่ยนแปลง
ซึ่งอาจเกิดจากการจัดเรียงไบต์และการเติมเพื่อให้โครงสร้างออกมาเป็นจำนวนไบต์ (หรือคำ) บนแพลตฟอร์มของคุณ ตัวอย่างเช่นใน C บน Linux โครงสร้าง 3 รายการต่อไปนี้:
#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;
}
มีสมาชิกที่มีขนาด (เป็นไบต์) คือ 4 ไบต์ (32 บิต), 8 ไบต์ (2x32 บิต) และ 1 ไบต์ (2 + 6 บิต) ตามลำดับ โปรแกรมข้างต้น (บน Linux โดยใช้ gcc) พิมพ์ขนาดเป็น 4, 8 และ 4 - โดยที่โครงสร้างสุดท้ายมีการเสริมเพื่อให้เป็นคำเดียว (4 x 8 บิตไบต์บนแพลตฟอร์ม 32 บิตของฉัน)
oneInt=4
twoInts=8
someBits=4
:2
และ:6
เป็นจริงระบุ 2 และ 6 บิตไม่ใช่เต็ม 32 บิตในกรณีนี้ someBits.x ซึ่งมีเพียง 2 บิตเท่านั้นที่สามารถเก็บค่าที่เป็นไปได้ 4 ค่า: 00, 01, 10 และ 11 (1, 2, 3 และ 4) มันสมเหตุสมผลหรือไม่ นี่คือบทความเกี่ยวกับคุณสมบัติ: geeksforgeeks.org/bit-fields-c
ดูสิ่งนี้ด้วย:
สำหรับ Microsoft Visual C:
http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=vs.80%29.aspx
และ GCC เรียกร้องความเข้ากันได้กับคอมไพเลอร์ของ Microsoft:
http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html
นอกจากคำตอบก่อนหน้านี้โปรดทราบว่าไม่ว่าบรรจุภัณฑ์จะไม่มีการรับประกันการเป็นสมาชิกใน C ++C คอมไพเลอร์อาจ (และแน่นอนทำ) เพิ่มตัวชี้ตารางเสมือนและสมาชิกโครงสร้างพื้นฐานของโครงสร้าง แม้แต่การดำรงอยู่ของตารางเสมือนก็ไม่ได้รับการรับรองตามมาตรฐาน (ไม่ได้ระบุการใช้งานกลไกเสมือน) และดังนั้นจึงสามารถสรุปได้ว่าการรับประกันดังกล่าวเป็นไปไม่ได้
ฉันค่อนข้างมั่นใจว่าคำสั่งซื้อของสมาชิกนั้นรับประกันใน Cแต่ฉันจะไม่นับที่มันเมื่อเขียนข้ามแพลตฟอร์มหรือข้ามคอมไพเลอร์โปรแกรม
ขนาดของโครงสร้างใหญ่กว่าผลรวมของชิ้นส่วนเพราะสิ่งที่เรียกว่าการบรรจุ ตัวประมวลผลเฉพาะมีขนาดข้อมูลที่ต้องการซึ่งทำงานได้ ขนาดที่ต้องการตัวประมวลผลที่ทันสมัยที่สุดหาก 32- บิต (4 ไบต์) การเข้าถึงหน่วยความจำเมื่อข้อมูลอยู่ในขอบเขตชนิดนี้จะมีประสิทธิภาพมากกว่าสิ่งที่ข้ามเขตแดนขนาดนั้น
ตัวอย่างเช่น. พิจารณาโครงสร้างที่เรียบง่าย:
struct myStruct
{
int a;
char b;
int c;
} data;
หากเครื่องเป็นเครื่อง 32- บิตและข้อมูลถูกจัดเรียงบนขอบเขต 32 บิตเราจะเห็นปัญหาทันที (สมมติว่าไม่มีการจัดแนวโครงสร้าง) ในตัวอย่างนี้ให้เราสมมติว่าข้อมูลโครงสร้างเริ่มต้นที่ที่อยู่ 1024 (0x400 - โปรดทราบว่า 2 บิตที่ต่ำที่สุดเป็นศูนย์ดังนั้นข้อมูลจะถูกจัดแนวกับขอบเขต 32 บิต) การเข้าถึง data.a จะทำงานได้ดีเพราะมันเริ่มที่ขอบเขต - 0x400 การเข้าถึง data.b จะทำงานได้ดีเพราะอยู่ที่ 0x404 - ขอบเขต 32 บิตอื่น แต่โครงสร้างที่ไม่ได้จัดแนวจะทำให้ data.c อยู่ที่ 0x405 data.c ขนาด 4 ไบต์อยู่ที่ 0x405, 0x406, 0x407, 0x408 บนเครื่อง 32- บิตระบบจะอ่าน data.c ในระหว่างรอบหนึ่งหน่วยความจำ แต่จะได้รับ 3 จาก 4 ไบต์เท่านั้น (ไบต์ที่ 4 อยู่ในขอบเขตถัดไป) ดังนั้นระบบจะต้องทำการเข้าถึงหน่วยความจำที่สองเพื่อรับไบต์ที่ 4
ทีนี้ถ้าแทนที่จะใส่ data.c ที่ address 0x405 คอมไพเลอร์จะเสริมโครงสร้างด้วย 3 ไบต์และใส่ data.c ที่แอดเดรส 0x408 จากนั้นระบบจะต้องการเพียง 1 รอบในการอ่านข้อมูลซึ่งจะช่วยลดเวลาในการเข้าถึงองค์ประกอบข้อมูลนั้น 50% Padding สลับประสิทธิภาพหน่วยความจำเพื่อประสิทธิภาพการประมวลผล เนื่องจากคอมพิวเตอร์สามารถมีหน่วยความจำจำนวนมาก (หลายกิกะไบต์) คอมไพเลอร์รู้สึกว่าการสลับ (ความเร็วสูงกว่าขนาด) นั้นเหมาะสม
น่าเสียดายที่ปัญหานี้กลายเป็นฆาตกรเมื่อคุณพยายามส่งโครงสร้างผ่านเครือข่ายหรือแม้แต่เขียนข้อมูลไบนารีไปยังไฟล์ไบนารี การเติมเต็มระหว่างองค์ประกอบของโครงสร้างหรือคลาสสามารถขัดขวางข้อมูลที่ส่งไปยังไฟล์หรือเครือข่าย ในการเขียนโค้ดพกพา (อันที่จะไปกับคอมไพเลอร์ต่าง ๆ ) คุณอาจจะต้องเข้าถึงแต่ละองค์ประกอบของโครงสร้างแยกต่างหากเพื่อให้แน่ใจว่า "บรรจุ" ที่เหมาะสม
ในขณะที่คอมไพเลอร์ที่แตกต่างกันมีความสามารถแตกต่างกันในการจัดการการจัดโครงสร้างข้อมูล ตัวอย่างเช่นใน Visual C / C ++ คอมไพเลอร์สนับสนุนคำสั่ง #pragma pack สิ่งนี้จะช่วยให้คุณปรับการบรรจุและการจัดแนวข้อมูล
ตัวอย่างเช่น:
#pragma pack 1
struct MyStruct
{
int a;
char b;
int c;
short d;
} myData;
I = sizeof(myData);
ตอนนี้ฉันควรมีความยาว 11 โดยไม่มี pragma ฉันสามารถเป็นอะไรก็ได้ตั้งแต่ 11 ถึง 14 (และสำหรับบางระบบมากถึง 32) ขึ้นอยู่กับการบรรจุเริ่มต้นของคอมไพเลอร์
#pragma pack
ในบริบทนี้มักจะหมายถึงสมาชิกจัดสรรให้กระชับยิ่งขึ้นกว่าค่าเริ่มต้นเช่นเดียวกับ หากสมาชิกได้รับการจัดสรรในการจัดตำแหน่งเริ่มต้นโดยทั่วไปฉันจะบอกว่าโครงสร้างไม่ได้บรรจุ
มันสามารถทำได้หากคุณได้ตั้งค่าการจัดตำแหน่งของ impl โดยปริยายหรืออย่างชัดเจน struct ที่จัดอยู่ในแนว 4 จะเป็นผลคูณของ 4 ไบต์เสมอแม้ว่าขนาดของสมาชิกจะเป็นขนาดที่ไม่ใช่หลายคูณ 4 ไบต์
นอกจากนี้ไลบรารีอาจถูกคอมไพล์ภายใต้ x86 ด้วย int 32- บิตและคุณอาจเปรียบเทียบส่วนประกอบในกระบวนการ 64 บิตจะให้ผลลัพธ์ที่แตกต่างถ้าคุณทำด้วยมือ
ร่างมาตรฐาน C99 N1256
http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf
6.5.3.4 ขนาดของผู้ประกอบการ :
3 เมื่อนำไปใช้กับตัวถูกดำเนินการที่มีโครงสร้างหรือประเภทยูเนี่ยนผลลัพธ์ที่ได้คือจำนวนไบต์ทั้งหมดในวัตถุดังกล่าวรวมถึงการขยายภายในและการเติมท้าย
6.7.2.1 โครงสร้างและตัวระบุสหภาพ :
13 ... อาจมีช่องว่างภายในที่ไม่มีชื่อในวัตถุโครงสร้าง แต่ไม่ใช่จุดเริ่มต้น
และ:
15 อาจมีช่องว่างภายในที่ไม่มีชื่อในตอนท้ายของโครงสร้างหรือสหภาพ
คุณลักษณะสมาชิกอาร์เรย์แบบยืดหยุ่น C99 ใหม่( struct S {int is[];};
) อาจส่งผลกระทบต่อการขยาย:
16 เป็นกรณีพิเศษองค์ประกอบสุดท้ายของโครงสร้างที่มีสมาชิกที่มีชื่อมากกว่าหนึ่งรายอาจมีประเภทอาเรย์ที่ไม่สมบูรณ์ สิ่งนี้เรียกว่าสมาชิกอาเรย์ที่มีความยืดหยุ่น ในสถานการณ์ส่วนใหญ่สมาชิกอาร์เรย์ที่ยืดหยุ่นจะถูกละเว้น โดยเฉพาะอย่างยิ่งขนาดของโครงสร้างจะเหมือนกับว่าสมาชิกอาร์เรย์ที่มีความยืดหยุ่นถูกละเว้นยกเว้นว่ามันอาจมีช่องว่างภายในเพิ่มเติมกว่าการละเว้นจะบ่งบอกถึง
ประเด็นปัญหาการพกพาของ Annex J นั้นย้ำ:
ต่อไปนี้ไม่ได้ระบุ: ...
- ค่าของการเสริมไบต์เมื่อจัดเก็บค่าในโครงสร้างหรือสหภาพ (6.2.6.1)
ร่างมาตรฐาน C ++ 11 N3337
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
5.3.3 ขนาดของ :
2 เมื่อนำไปใช้กับคลาสผลที่ได้คือจำนวนไบต์ในวัตถุของคลาสนั้นรวมถึงการเติมเต็มที่จำเป็นสำหรับการวางวัตถุประเภทนั้นในอาร์เรย์
9.2 สมาชิกกลุ่ม :
ตัวชี้ไปยังวัตถุโครงสร้างเค้าโครงมาตรฐานการแปลงอย่างเหมาะสมโดยใช้ reinterpret_cast ชี้ไปที่สมาชิกเริ่มต้น (หรือถ้าสมาชิกนั้นเป็นบิตฟิลด์จากนั้นไปยังหน่วยที่มันอยู่) และในทางกลับกัน [หมายเหตุ: อาจมีการเติมชื่อที่ไม่มีชื่อภายในวัตถุโครงสร้างเลย์เอาต์มาตรฐาน แต่ไม่ใช่ที่จุดเริ่มต้นตามความจำเป็นเพื่อให้ได้การจัดตำแหน่งที่เหมาะสม - บันทึกท้าย]
ฉันรู้ C ++ เพียงพอที่จะเข้าใจหมายเหตุ :-)
นอกเหนือจากคำตอบอื่น ๆ แล้ว struct สามารถ (แต่ไม่ปกติ) มีฟังก์ชั่นเสมือนจริงซึ่งในกรณีนี้ขนาดของ struct จะรวมพื้นที่สำหรับ vtbl ด้วย
ภาษา C ทำให้คอมไพเลอร์มีอิสระบางอย่างเกี่ยวกับตำแหน่งขององค์ประกอบโครงสร้างในหน่วยความจำ:
ภาษา C ให้ความมั่นใจกับโปรแกรมเมอร์ของโครงร่างองค์ประกอบในโครงสร้าง:
ปัญหาที่เกี่ยวข้องกับการจัดตำแหน่งองค์ประกอบ:
การจัดตำแหน่งทำงานอย่างไร:
ป.ล. ข้อมูลรายละเอียดเพิ่มเติมอยู่ที่นี่: "ซามูเอลพีแฮร์ริสัน, Guy L.Steele อ้างอิง CA, (5.6.2 - 5.6.7)
แนวคิดคือสำหรับการพิจารณาความเร็วและแคชตัวถูกดำเนินการควรอ่านจากที่อยู่ที่สอดคล้องกับขนาดตามธรรมชาติ เมื่อต้องการทำให้สิ่งนี้เกิดขึ้นสมาชิกโครงสร้างแผ่นคอมไพเลอร์ดังนั้นสมาชิกต่อไปนี้หรือโครงสร้างต่อไปนี้จะถูกจัดตำแหน่ง
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
สถาปัตยกรรม x86 สามารถดึงที่อยู่ที่ไม่ตรงแนวได้เสมอ อย่างไรก็ตามมันจะช้ากว่าและเมื่อการวางแนวไม่ตรงกับสองบรรทัดของแคชที่ต่างกันมันจะแสดงสองบรรทัดของแคชเมื่อการเข้าถึงที่ถูกจัดชิดจะขับไล่หนึ่งบรรทัด
สถาปัตยกรรมบางอันต้องดักอ่านและเขียนที่ไม่ตรงแนวและสถาปัตยกรรม ARM รุ่นแรก (อันที่พัฒนาไปสู่ซีพียูมือถือทุกวันนี้) ... เอาละพวกเขาแค่ส่งคืนข้อมูลที่ไม่ดีสำหรับพวกเขา (พวกเขาเพิกเฉยบิตต่ำ ๆ )
สุดท้ายโปรดทราบว่าบรรทัดแคชอาจมีขนาดใหญ่ตามอำเภอใจและคอมไพเลอร์ไม่ได้พยายามเดาสิ่งเหล่านั้นหรือทำการแลกเปลี่ยน space-vs-speed การตัดสินใจจัดตำแหน่งนั้นเป็นส่วนหนึ่งของ ABI และเป็นตัวแทนของการจัดตำแหน่งขั้นต่ำที่จะเติมแคชให้เท่ากันในที่สุด
TL; DR:การจัดตำแหน่งเป็นสิ่งสำคัญ