Tại sao sizeof
toán tử trả về kích thước lớn hơn cho cấu trúc so với tổng kích thước của các thành viên của cấu trúc?
Tại sao sizeof
toán tử trả về kích thước lớn hơn cho cấu trúc so với tổng kích thước của các thành viên của cấu trúc?
Câu trả lời:
Điều này là do phần đệm được thêm vào để đáp ứng các ràng buộc căn chỉnh. Căn chỉnh cấu trúc dữ liệu ảnh hưởng đến cả hiệu suất và tính chính xác của các chương trình:
SIGBUS
).Dưới đây là một ví dụ sử dụng các cài đặt điển hình cho bộ xử lý x86 (tất cả các chế độ 32 và 64 bit được sử dụng):
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 */
Người ta có thể giảm thiểu kích thước của các cấu trúc bằng cách sắp xếp các thành viên bằng cách căn chỉnh (sắp xếp theo đủ kích thước cho các kiểu cơ bản) (giống như cấu trúc Z
trong ví dụ trên).
LƯU Ý QUAN TRỌNG: Cả hai tiêu chuẩn C và C ++ đều nói rằng sự liên kết cấu trúc được xác định theo thực hiện. Do đó, mỗi trình biên dịch có thể chọn căn chỉnh dữ liệu khác nhau, dẫn đến bố cục dữ liệu khác nhau và không tương thích. Vì lý do này, khi làm việc với các thư viện sẽ được sử dụng bởi các trình biên dịch khác nhau, điều quan trọng là phải hiểu cách trình biên dịch sắp xếp dữ liệu. Một số trình biên dịch có cài đặt dòng lệnh và / hoặc các #pragma
câu lệnh đặc biệt để thay đổi cài đặt căn chỉnh cấu trúc.
Đóng gói và căn chỉnh byte, như được mô tả trong C FAQ ở đây :
Đó là sự liên kết. Nhiều bộ xử lý không thể truy cập số lượng 2 và 4 byte (ví dụ: int và int dài) nếu chúng bị nhồi nhét mọi lúc.
Giả sử bạn có cấu trúc này:
struct { char a[3]; short int b; long int c; char d[3]; };
Bây giờ, bạn có thể nghĩ rằng cần phải đóng gói cấu trúc này vào bộ nhớ như thế này:
+-------+-------+-------+-------+ | a | b | +-------+-------+-------+-------+ | b | c | +-------+-------+-------+-------+ | c | d | +-------+-------+-------+-------+
Nhưng nó rất dễ dàng hơn nhiều trên bộ xử lý nếu trình biên dịch sắp xếp nó như thế này:
+-------+-------+-------+ | a | +-------+-------+-------+ | b | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | +-------+-------+-------+
Trong phiên bản đóng gói, hãy chú ý làm thế nào ít nhất một chút khó khăn cho bạn và tôi để xem các trường b và c bao quanh như thế nào? Tóm lại, nó cũng khó cho bộ xử lý. Do đó, hầu hết các trình biên dịch sẽ đệm cấu trúc (như thể có thêm các trường vô hình) như thế này:
+-------+-------+-------+-------+ | a | pad1 | +-------+-------+-------+-------+ | b | pad2 | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | pad3 | +-------+-------+-------+-------+
s
sau đó &s.a == &s
và &s.d == &s + 12
(đưa ra sự liên kết được hiển thị trong câu trả lời). Con trỏ chỉ được lưu trữ nếu các mảng có kích thước thay đổi (ví dụ: a
được khai báo char a[]
thay vì char a[3]
), nhưng sau đó các phần tử phải được lưu trữ ở nơi khác.
Nếu bạn muốn cấu trúc có kích thước nhất định với GCC chẳng hạn, hãy sử dụng __attribute__((packed))
.
Trên Windows, bạn có thể đặt căn chỉnh thành một byte khi sử dụng trình biên dịch cl.exe với tùy chọn / Zp .
Thông thường, CPU sẽ dễ dàng truy cập dữ liệu hơn bội số của 4 (hoặc 8), tùy thuộc vào nền tảng và cả trình biên dịch.
Vì vậy, nó là một vấn đề liên kết về cơ bản.
Bạn cần có lý do chính đáng để thay đổi nó.
Điều này có thể là do căn chỉnh byte và phần đệm để cấu trúc đi ra một số byte (hoặc từ) chẵn trên nền tảng của bạn. Ví dụ trong C trên Linux, 3 cấu trúc sau:
#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;
}
Có các thành viên có kích thước (tính bằng byte) lần lượt là 4 byte (32 bit), 8 byte (2x 32 bit) và 1 byte (2 + 6 bit). Chương trình trên (trên Linux sử dụng gcc) in các kích thước là 4, 8 và 4 - trong đó cấu trúc cuối cùng được đệm sao cho nó là một từ đơn (4 x 8 bit byte trên nền tảng 32 bit của tôi).
oneInt=4
twoInts=8
someBits=4
:2
và :6
thực sự chỉ định 2 và 6 bit, không phải là số nguyên 32 bit đầy đủ trong trường hợp này. someBits.x, chỉ có 2 bit chỉ có thể lưu trữ 4 giá trị có thể: 00, 01, 10 và 11 (1, 2, 3 và 4). Điều này có nghĩa không? Đây là một bài viết về tính năng: geekforgeek.org/bit-fields-c
Xem thêm:
cho Microsoft Visual C:
http://msdn.microsoft.com/en-us/l Library / 2e70t5y1% 28v = vs.80% 29.aspx
và yêu cầu tương thích GCC với trình biên dịch của Microsoft.:
http://gcc.gnu.org/onlinesocs/gcc/Str struct_002dPacking-Pragmas.html
Ngoài các câu trả lời trước, xin lưu ý rằng bất kể bao bì, không có bảo đảm đặt hàng thành viên trong C ++ . Trình biên dịch có thể (và chắc chắn làm) thêm con trỏ bảng ảo và các thành viên cấu trúc cơ sở vào cấu trúc. Ngay cả sự tồn tại của bảng ảo cũng không được đảm bảo theo tiêu chuẩn (việc thực hiện cơ chế ảo không được chỉ định) và do đó người ta có thể kết luận rằng bảo đảm đó là không thể.
Tôi khá chắc chắn rằng thứ tự thành viên được đảm bảo bằng C , nhưng tôi sẽ không tin vào điều đó, khi viết chương trình đa nền tảng hoặc trình biên dịch chéo.
Kích thước của một cấu trúc lớn hơn tổng của các bộ phận của nó vì những gì được gọi là đóng gói. Một bộ xử lý cụ thể có kích thước dữ liệu ưa thích mà nó hoạt động. Kích thước ưa thích của hầu hết các bộ xử lý hiện đại nếu 32 bit (4 byte). Truy cập bộ nhớ khi dữ liệu nằm trên loại ranh giới này hiệu quả hơn so với những thứ nằm trên ranh giới kích thước đó.
Ví dụ. Hãy xem xét cấu trúc đơn giản:
struct myStruct
{
int a;
char b;
int c;
} data;
Nếu máy là máy 32 bit và dữ liệu được căn chỉnh trên ranh giới 32 bit, chúng ta sẽ thấy một vấn đề ngay lập tức (giả sử không có sự liên kết cấu trúc). Trong ví dụ này, chúng ta giả sử rằng dữ liệu cấu trúc bắt đầu tại địa chỉ 1024 (0x400 - lưu ý rằng 2 bit thấp nhất bằng 0, do đó dữ liệu được căn chỉnh theo ranh giới 32 bit). Quyền truy cập vào data.a sẽ hoạt động tốt vì nó bắt đầu trên một ranh giới - 0x400. Quyền truy cập vào data.b cũng sẽ hoạt động tốt, vì nó ở địa chỉ 0x404 - một ranh giới 32 bit khác. Nhưng một cấu trúc không được sắp xếp sẽ đặt data.c tại địa chỉ 0x405. 4 byte dữ liệu.c là 0x405, 0x406, 0x407, 0x408. Trên máy 32 bit, hệ thống sẽ đọc data.c trong một chu kỳ bộ nhớ, nhưng sẽ chỉ nhận được 3 trong số 4 byte (byte thứ 4 nằm ở ranh giới tiếp theo). Vì vậy, hệ thống sẽ phải thực hiện truy cập bộ nhớ thứ hai để có được byte thứ 4,
Bây giờ, nếu thay vì đặt data.c tại địa chỉ 0x405, trình biên dịch đã đệm cấu trúc 3 byte và đặt data.c tại địa chỉ 0x408, thì hệ thống sẽ chỉ cần 1 chu kỳ để đọc dữ liệu, giảm thời gian truy cập vào phần tử dữ liệu đó bằng 50%. Đệm đổi hiệu quả bộ nhớ cho hiệu quả xử lý. Cho rằng máy tính có thể có bộ nhớ lớn (nhiều gigabyte), trình biên dịch cảm thấy rằng hoán đổi (tốc độ trên kích thước) là hợp lý.
Thật không may, vấn đề này trở thành một kẻ giết người khi bạn cố gắng gửi các cấu trúc qua mạng hoặc thậm chí ghi dữ liệu nhị phân vào một tệp nhị phân. Phần đệm được chèn giữa các thành phần của cấu trúc hoặc lớp có thể phá vỡ dữ liệu được gửi đến tệp hoặc mạng. Để viết mã di động (một mã sẽ đi đến một số trình biên dịch khác nhau), bạn có thể sẽ phải truy cập từng phần tử của cấu trúc một cách riêng biệt để đảm bảo "đóng gói" thích hợp.
Mặt khác, các trình biên dịch khác nhau có khả năng khác nhau để quản lý việc đóng gói cấu trúc dữ liệu. Ví dụ, trong Visual C / C ++, trình biên dịch hỗ trợ lệnh gói #pragma. Điều này sẽ cho phép bạn điều chỉnh đóng gói dữ liệu và căn chỉnh.
Ví dụ:
#pragma pack 1
struct MyStruct
{
int a;
char b;
int c;
short d;
} myData;
I = sizeof(myData);
Bây giờ tôi sẽ có độ dài 11. Không có pragma, tôi có thể là bất cứ thứ gì từ 11 đến 14 (và đối với một số hệ thống, nhiều như 32), tùy thuộc vào cách đóng gói mặc định của trình biên dịch.
#pragma pack
. Nếu các thành viên được phân bổ trên căn chỉnh mặc định của họ, tôi thường nói rằng cấu trúc không được đóng gói.
Nó có thể làm như vậy nếu bạn đã đặt ngầm định hoặc rõ ràng sự liên kết của cấu trúc. Một cấu trúc được căn chỉnh 4 sẽ luôn là bội số của 4 byte ngay cả khi kích thước của các thành viên của nó sẽ là một cái gì đó không phải là bội số của 4 byte.
Ngoài ra, một thư viện có thể được biên dịch dưới x86 với int 32 bit và bạn có thể so sánh các thành phần của nó trên quy trình 64 bit sẽ cho bạn một kết quả khác nếu bạn làm điều này bằng tay.
Dự thảo tiêu chuẩn C99 N1256
http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf
6.5.3.4 Toán tử sizeof :
3 Khi được áp dụng cho toán hạng có cấu trúc hoặc kiểu kết hợp, kết quả là tổng số byte trong một đối tượng như vậy, bao gồm cả phần đệm bên trong và dấu.
6.7.2.1 Cấu trúc và công cụ xác định liên minh :
13 ... Có thể có phần đệm không tên trong một đối tượng cấu trúc, nhưng không phải lúc bắt đầu.
và:
15 Có thể có phần đệm không tên ở cuối cấu trúc hoặc kết hợp.
Tính năng thành viên mảng linh hoạt C99 mới ( struct S {int is[];};
) cũng có thể ảnh hưởng đến phần đệm:
16 Trong trường hợp đặc biệt, phần tử cuối cùng của cấu trúc có nhiều thành viên được đặt tên có thể có kiểu mảng không hoàn chỉnh; đây được gọi là thành viên mảng linh hoạt. Trong hầu hết các tình huống, thành viên mảng linh hoạt bị bỏ qua. Cụ thể, kích thước của cấu trúc như thể thành viên mảng linh hoạt bị bỏ qua ngoại trừ việc nó có thể có nhiều phần đệm hơn so với thiếu sót sẽ ngụ ý.
Phụ lục J Các vấn đề về tính di động nhắc lại:
Sau đây là không xác định: ...
- Giá trị của byte đệm khi lưu trữ giá trị trong cấu trúc hoặc kết hợp (6.2.6.1)
Dự thảo tiêu chuẩn C ++ 11 N3337
http://www.open-std.org/jtc1/sc22/wg21/docs/ con / 2012 / n3337.pdf
5.3.3 Kích thước của :
2 Khi được áp dụng cho một lớp, kết quả là số byte trong một đối tượng của lớp đó bao gồm mọi phần đệm cần thiết để đặt các đối tượng của loại đó trong một mảng.
9.2 thành viên lớp :
Một con trỏ tới một đối tượng cấu trúc bố cục tiêu chuẩn, được chuyển đổi phù hợp bằng cách sử dụng reinterpret_cast, trỏ đến thành viên ban đầu của nó (hoặc nếu thành viên đó là một trường bit, sau đó đến đơn vị mà nó cư trú) và ngược lại. [Lưu ý: Do đó, có thể có phần đệm không tên trong một đối tượng cấu trúc bố cục tiêu chuẩn, nhưng không phải lúc đầu, khi cần thiết để đạt được sự liên kết thích hợp. - lưu ý cuối]
Tôi chỉ biết đủ C ++ để hiểu ghi chú :-)
Ngoài các câu trả lời khác, một cấu trúc có thể (nhưng thường không) có các hàm ảo, trong trường hợp đó kích thước của cấu trúc cũng sẽ bao gồm không gian cho vtbl.
Ngôn ngữ C để lại cho trình biên dịch một số tự do về vị trí của các thành phần cấu trúc trong bộ nhớ:
Ngôn ngữ C cung cấp một số đảm bảo cho lập trình viên về bố cục các thành phần trong cấu trúc:
Các vấn đề liên quan đến sự liên kết các yếu tố:
Cách căn chỉnh hoạt động:
ps Thông tin chi tiết có sẵn tại đây: "Samuel P.Harbison, Guy L.Steele CA Reference, (5.6.2 - 5.6.7)"
Ý tưởng là để xem xét tốc độ và bộ đệm, toán hạng nên được đọc từ các địa chỉ được căn chỉnh theo kích thước tự nhiên của chúng. Để thực hiện điều này, các trình biên dịch cấu trúc thành viên để các thành viên sau hoặc cấu trúc sau sẽ được căn chỉnh.
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
Kiến trúc x86 luôn có thể tìm nạp các địa chỉ sai. Tuy nhiên, nó chậm hơn và khi căn chỉnh sai chồng lấp hai dòng bộ đệm khác nhau, thì nó sẽ hiển thị hai dòng bộ đệm khi một truy cập được căn chỉnh sẽ chỉ loại bỏ một dòng.
Một số kiến trúc thực sự phải mắc kẹt trong việc đọc và ghi sai, và các phiên bản đầu tiên của kiến trúc ARM (phiên bản phát triển thành tất cả các CPU di động ngày nay) ... tốt, chúng thực sự chỉ trả về dữ liệu xấu cho những thứ đó. (Họ bỏ qua các bit thứ tự thấp.)
Cuối cùng, lưu ý rằng các dòng bộ đệm có thể lớn tùy ý và trình biên dịch không cố đoán chúng hoặc tạo ra sự đánh đổi giữa không gian và tốc độ. Thay vào đó, các quyết định căn chỉnh là một phần của ABI và thể hiện sự liên kết tối thiểu cuối cùng sẽ lấp đầy một dòng bộ đệm.
TL; DR: căn chỉnh là quan trọng.