C/C++ 中结构布局的最佳实践
Best practice for struct layout in C/C++
我已经阅读了一些文章和 SO 答案,内容涉及如果我们按照独立字段大小的降序排列结构,我们如何能够切断编译器添加的填充。
例如:而不是像这样布置 (L1) 结构
typedef struct dummy {
char c1;
short s1;
int i1;
unsigned int ui1;
double d1;
} dummy_t;
像这样创建布局 (L2)
typedef struct dummy {
double d1;
unsigned int ui1;
int i1;
short s1;
char c1;
} dummy_t;
我的问题是,与 L1 或任何其他布局相比,L2 布局是否存在缺点?或者我可以将它纳入我的设计规则以始终使用 L2 吗?也是自然对齐规则
natural alignment = sizeof(T)
基本类型总是正确的?
只有当您 运行 在内存受限的系统上(您想限制每个元素的大小)时,您才应该考虑 alignement/structure 大小。这仅对 systems/designs 有意义,您知道您将存储大量此类结构,而使用的存储空间 space 是一个问题。另一方面,从逻辑上对结构的内容进行分组可以使您的代码更加清晰。我总是赞成清晰度而不是性能,尤其是在早期设计阶段。如果您没有与性能相关的限制,甚至更多。
关于性能,从 L2 访问“随机”内容可能会比从 L1 访问“随机”内容更糟糕,因为内存缓存对齐约束也会起作用。 (例如,连续访问 struct L1 的 2 个元素可能比访问 L2 的 2 个元素慢或快)。对于此类优化,分析是了解哪种布局最佳的唯一方法。
My question here is, are there scenarios where layout L2 has downsides when compared to L1 or any other layout ?
有时您需要以不同的顺序排列成员。原因可能包括:
- 该结构是通信协议的一部分,必须通过网络或其他通信设备逐字节发送。在这种情况下,结构的布局可能由协议决定,您必须完全匹配它(包括填充或缺少填充)。
- 该结构是共享初始成员序列的结构家族的一部分,因此必须将这些成员放在首位。有时这样做是为了通过仅对那些初始成员进行操作,以“通用”方式处理结构。
- 该结构包含一个缓冲区,其长度会有所不同(通过根据程序执行期间出现的需要动态分配)。这样的缓冲区是用一个灵活的数组成员实现的,它必须是结构中的最后一个成员。
此外,成员的排序方式可能会产生附带影响。例如,如果成员 x
恰好比结构的其他成员更频繁地使用,将它放在前面可能允许编译器使用更简单的地址算法访问它,因为它距结构开头的偏移量将为零。这在编程中很少考虑,但为了完整起见我提到它。
抛开这些考虑,您通常可以随意订购会员。
Also is the natural alignment rule natural alignment = sizeof(T)
always true for primitive types ?
没有。例如,一个八字节的 long
可能有一个、两个、四个或八个字节的对齐要求。
… we can cutoff compiler added padding if we layout the structs in the decreasing order of size of independent fields.
对于作为聚合的成员(数组、结构和联合),情况并非如此。考虑成员 char x[13];
的大小为 13 个字节,但只需要对齐一个字节。为了尽量减少填充,请按对齐要求递减的顺序对成员进行排序,而不是按大小递减的顺序排列。
IMO 从缓存等实现细节中抽象出来(太宽泛,无法在 post 答案中讨论)没有区别。
不同之处在于,如果将可变大小(或零大小)的对象放在结构的末尾(示例):
typedef struct
{
size_t size;
char data[];
}string;
string *create(size_t size)
{
string *ptr = malloc(sizeof(*ptr) + size);
if(ptr)
{
ptr -> size = size;
}
return ptr;
}
如果结构将存储一些接收到的二进制数据(例如通信包头),那么当然需要逻辑成员顺序(和潜在的打包)
我已经阅读了一些文章和 SO 答案,内容涉及如果我们按照独立字段大小的降序排列结构,我们如何能够切断编译器添加的填充。
例如:而不是像这样布置 (L1) 结构
typedef struct dummy {
char c1;
short s1;
int i1;
unsigned int ui1;
double d1;
} dummy_t;
像这样创建布局 (L2)
typedef struct dummy {
double d1;
unsigned int ui1;
int i1;
short s1;
char c1;
} dummy_t;
我的问题是,与 L1 或任何其他布局相比,L2 布局是否存在缺点?或者我可以将它纳入我的设计规则以始终使用 L2 吗?也是自然对齐规则
natural alignment = sizeof(T)
基本类型总是正确的?
只有当您 运行 在内存受限的系统上(您想限制每个元素的大小)时,您才应该考虑 alignement/structure 大小。这仅对 systems/designs 有意义,您知道您将存储大量此类结构,而使用的存储空间 space 是一个问题。另一方面,从逻辑上对结构的内容进行分组可以使您的代码更加清晰。我总是赞成清晰度而不是性能,尤其是在早期设计阶段。如果您没有与性能相关的限制,甚至更多。
关于性能,从 L2 访问“随机”内容可能会比从 L1 访问“随机”内容更糟糕,因为内存缓存对齐约束也会起作用。 (例如,连续访问 struct L1 的 2 个元素可能比访问 L2 的 2 个元素慢或快)。对于此类优化,分析是了解哪种布局最佳的唯一方法。
My question here is, are there scenarios where layout L2 has downsides when compared to L1 or any other layout ?
有时您需要以不同的顺序排列成员。原因可能包括:
- 该结构是通信协议的一部分,必须通过网络或其他通信设备逐字节发送。在这种情况下,结构的布局可能由协议决定,您必须完全匹配它(包括填充或缺少填充)。
- 该结构是共享初始成员序列的结构家族的一部分,因此必须将这些成员放在首位。有时这样做是为了通过仅对那些初始成员进行操作,以“通用”方式处理结构。
- 该结构包含一个缓冲区,其长度会有所不同(通过根据程序执行期间出现的需要动态分配)。这样的缓冲区是用一个灵活的数组成员实现的,它必须是结构中的最后一个成员。
此外,成员的排序方式可能会产生附带影响。例如,如果成员 x
恰好比结构的其他成员更频繁地使用,将它放在前面可能允许编译器使用更简单的地址算法访问它,因为它距结构开头的偏移量将为零。这在编程中很少考虑,但为了完整起见我提到它。
抛开这些考虑,您通常可以随意订购会员。
Also is the natural alignment rule
natural alignment = sizeof(T)
always true for primitive types ?
没有。例如,一个八字节的 long
可能有一个、两个、四个或八个字节的对齐要求。
… we can cutoff compiler added padding if we layout the structs in the decreasing order of size of independent fields.
对于作为聚合的成员(数组、结构和联合),情况并非如此。考虑成员 char x[13];
的大小为 13 个字节,但只需要对齐一个字节。为了尽量减少填充,请按对齐要求递减的顺序对成员进行排序,而不是按大小递减的顺序排列。
IMO 从缓存等实现细节中抽象出来(太宽泛,无法在 post 答案中讨论)没有区别。
不同之处在于,如果将可变大小(或零大小)的对象放在结构的末尾(示例):
typedef struct
{
size_t size;
char data[];
}string;
string *create(size_t size)
{
string *ptr = malloc(sizeof(*ptr) + size);
if(ptr)
{
ptr -> size = size;
}
return ptr;
}
如果结构将存储一些接收到的二进制数据(例如通信包头),那么当然需要逻辑成员顺序(和潜在的打包)