是否有C结构体的“紧凑对齐”选项?

huangapple go评论66阅读模式
英文:

Is there a C struct "packed aligned" option?

问题

以下是翻译好的部分:

编译器:我个人使用gcc,但问题是概念性的。我对任何编译器的选项都感兴趣。

有没有办法告诉C编译器使struct Bstruct AB具有相同的大小,而不牺牲对齐方式?

将其放入数组时,它还应该尊重对齐方式。

我尝试使用__attribute__((__packed__, aligned(4))),但这似乎与不使用任何属性一样(大小仍然四舍五入到对齐方式)。

我不明白为什么这不是一个明显的改进:在某些情况下,它可以节省相当多的结构空间,而不会牺牲性能(或人体工程学)上的字段查找。编译器所需的只是为每个结构存储一个(大小、对齐方式)。

#include <stdio.h>
#include <stdint.h>

struct A { // 总大小:6字节(实际上是8字节)
  uint32_t a0; // 4字节
  uint16_t a1; // 2字节
};

struct B { // 总大小:8字节(实际上是12字节)
  struct A b0; // 6字节
  uint16_t b1; // 2字节
};

struct AB { // 总大小:8字节(实际上是8字节)
  uint32_t a0; // 4字节
  uint16_t a1; // 2字节
  uint16_t b1; // 2字节
};

// 有点奏效,但牺牲了对齐方式
struct __attribute__((__packed__)) Ap {
  uint32_t a0; // 4字节
  uint8_t a1;  // 1字节
};
struct __attribute__((__packed__)) Bp {
  struct Ap b0;
  uint16_t  b1;
};

int main() {
  printf("sizeof(A)  = %u\n", sizeof(struct A));  // 8  (不是6)
  printf("sizeof(B)  = %u\n", sizeof(struct B));  // 12 (不是8)
  printf("sizeof(AB) = %u\n", sizeof(struct AB)); // 8  (与期望相同)
  printf("sizeof(Ap) = %u\n", sizeof(struct Ap)); // 5  (与期望相同)
  printf("sizeof(Bp) = %u\n", sizeof(struct Bp)); // 7  (不是8)
  return 0;
}

我实际上一直在这样做的方式:

#define STRUCT_A  \
  uint32_t a0; \
  uint8_t a1

struct AB {
  STRUCT_A;    // 6字节
  uint16_t b1; // 2字节
};
英文:

Compiler: I'm personally using gcc, but the question is conceptual. I'm interested in options for any compiler.

Is there a way to tell the C compiler to make struct B have the same size as struct AB without sacrificing alignment?

It should also respect alignment when put into an array.

I've tried using __attribute__ ((__packed__, aligned(4))) but this seems to be the same as not using any attributes (the size is still rounded up to the alignment).

I don't understand how this isn't an obvious improvement: in certain cases it could save quite a bit of space for structs without sacrificing performance (or ergonomics) on field lookups. All it would require for the compiler is to store a (size, alignment) for each struct.

#include &lt;stdio.h&gt;
#include &lt;stdint.h&gt;

struct A { // total size: 6 bytes (actually 8)
  uint32_t a0; // 4 bytes
  uint16_t a1; // 2 bytes
};

struct B { // total size: 8 bytes (actually 12)
  struct A b0; // 6 bytes
  uint16_t b1; // 2 bytes
};

struct AB { // total size: 8 bytes (actually 8)
  uint32_t a0; // 4 bytes
  uint16_t a1; // 2 bytes
  uint16_t b1; // 2 bytes
};

// Kind of works, but sacrifices alignment
struct __attribute__ ((__packed__)) Ap {
  uint32_t a0; // 4 bytes
  uint8_t a1;  // 1 byte
};
struct __attribute__ ((__packed__)) Bp {
  struct Ap b0;
  uint16_t  b1;
};

int main() {
  printf(&quot;sizeof(A)  = %u\n&quot;, sizeof(struct A));  // 8  (not 6)
  printf(&quot;sizeof(B)  = %u\n&quot;, sizeof(struct B));  // 12 (not 8)
  printf(&quot;sizeof(AB) = %u\n&quot;, sizeof(struct AB)); // 8  (same as desired)
  printf(&quot;sizeof(Ap) = %u\n&quot;, sizeof(struct Ap)); // 5  (as desired)
  printf(&quot;sizeof(Bp) = %u\n&quot;, sizeof(struct Bp)); // 7  (not 8)
  return 0;
}

The way I've been actually doing this:

#define STRUCT_A  \
  uint32_t a0; \
  uint8_t a1

struct AB {
  STRUCT_A;    // 6 bytes
  uint16_t b1; // 2 bytes
};

答案1

得分: 2

如果我正确理解您的愿望,那是不可能的。这不仅仅是编译器或ABI的限制;它实际上与C语言的以下基本原则相矛盾。

1. 在类型为T的数组中,连续的元素间隔为sizeof(T)字节。

这个保证是您正确实现“通用”数组处理函数如qsort的基础。例如,如果我们想要一个将数组的第3个元素复制到第4个元素的函数,那么语言承诺以下操作必须有效:

void copy_3_to_4(void *arr, size_t elem_size) {
    unsigned char *c_arr = arr; // 为了最小化强制类型转换而使用的便利性
    for (size_t i = 0; i < elem_size; i++) {
        c_arr[4 * elem_size + i] = c_arr[3 * elem_size + i];
    }
}

struct foo { ... };
struct foo my_array[100];
copy_3_to_4(my_array, sizeof(struct foo)); // 等同于 my_array[4] = my_array[3]

由此可见,如果对象T具有所需的对齐要求为k字节,则sizeof(T)必须必然是k的倍数。否则,足够大的数组的元素将无法正确对齐。因此,您提出的大小为6且对齐为4的对象概念与此原则不一致。

因此,对于您示例中的struct A,其中包含uint32_tuint16_t成员:如果我们假设,像大多数常见平台一样,uint32_t需要4字节对齐,则struct A也需要相同的对齐要求,因此sizeof(struct A)不能为6;它必须为8。 (或者,原则上,可以是12、16等,但那将很奇怪。)2字节的填充是不可避免的。

2. 不同的对象不能重叠。

在这里,“重叠”是以sizeof为基础来定义的。从地址&foo开始的sizeof(T)字节不能与任何其他对象bar的相应字节重叠。这包括它们可能包含的任何填充字节。对于一个struct,这意味着修改一个struct的对象可以自由修改它的填充字节,如果编译器发现这样做更方便的话。对于您的struct Astruct B示例,我们可以想象:

void copy(struct A *dst, const struct A *src) {
    *dst = *src;
}

编译器被允许将这个函数编译成一个单独的64位加载/存储对,它不仅会复制实际数据的6个字节,还会复制2个字节的填充。如果不能这样做,它就必须将其编译为32位复制和16位复制,这将不太高效。

也许一个更好的例子是,您还可以通过memcpy(&y, &x, sizeof(struct A))来复制struct A,这显然会复制8个字节,或者像上面的copy_3_to_4一样逐字节复制sizeof(struct A)字节。

而且,这是合法的:

struct A foo = { 42 };
struct B bar;
bar.b1 = 17;
copy(&bar.b0, &foo);
assert(bar.b1 == 17); // 应该不会改变

如果您希望sizeof(struct B) == 8,那么b1成员必须存在于b0成员的填充中。因此,如果copy(&bar.b0, &foo)执行64位复制,那么它将覆盖它。我们不能要求copy特殊处理这种情况,因为它可能在一个完全不同的文件中编译,并且无法知道它的参数是否存在于某个较大的对象中。我们也不能告诉程序员他们不能做copy(&bar.b0, &foo);对象bar.b0struct A类型的正式对象,有权享受该类型的任何对象的权利和特权。

因此,唯一的出路是sizeof(struct B)必须大于等于8。由于其要求的对齐仍然是4(从struct A继承而来,从uint32_t继承而来),所以sizeof(struct B)必须是12或更多。

英文:

If I correctly understand what you're wishing for, it's impossible. It's not merely a compiler or ABI restriction; it would actually be inconsistent with the following fundamental principles of the C language.

1. In an array of type T, successive elements are at intervals of sizeof(T) bytes.

This guarantee is what allows you to correctly implement "generic" array processing functions like qsort. If for instance we want a function that copies element 3 of an array to element 4, then the language promises that the following must work:

void copy_3_to_4(void *arr, size_t elem_size) {
    unsigned char *c_arr = arr; // convenience to minimize casting
    for (size_t i = 0; i &lt; elem_size; i++) {
        c_arr[4*elem_size + i] = c_arr[3*elem_size+i];
    }
}

struct foo { ... };
struct foo my_array[100];
copy_3_to_4(my_array, sizeof(struct foo)); // equivalent to my_array[4] = my_array[3]

From this it follows that if an object T has a required alignment of k bytes, then sizeof(T) must necessarily be a multiple of k. Otherwise, the elements of a large enough array could not all be correctly aligned. So your proposed notion of an object of size 6 and alignment 4 cannot be consistent with this principle.

So for the struct A in your example, with a uint32_t and a uint16_t member: if we suppose that, as on most common platforms, uint32_t requires 4-byte alignment, then struct A requires the same, and so sizeof(struct A) can't be 6; it has to be 8. (Or, in principle, 12, 16, etc, but that would be weird.) The 2 bytes of padding is unavoidable.

2. Distinct objects cannot overlap.

And here "overlap" is defined in terms of sizeof. The sizeof(T) bytes starting at address &amp;foo cannot coincide with any of the corresponding bytes of any other object bar. This includes any padding bytes that either object may contain. And distinct members of a struct (other than bitfields) are distinct objects for this purpose.

For a struct, this means that an object which modifies a struct is allowed to freely modify its padding bytes, if the compiler finds it convenient to do so. With your struct A and struct B examples, we could imagine:

void copy(struct A *dst, const struct A *src) {
    *dst = *src;
}

The compiler is allowed to compile this into a single 64-bit load/store pair, which copies not only the 6 bytes of actual data but also the 2 bytes of padding. If it couldn't do that, it would have to compile it as a 32-bit copy plus a 16-bit copy, which would be less efficient.

Perhaps an even better example is that you are also allowed to copy a struct A by doing memcpy(&amp;y, &amp;x, sizeof(struct A)), which will more obviously copy 8 bytes, or a byte-by-byte copy of sizeof(struct A) bytes as in copy_3_to_4 above.

And it is legal to do:

struct A foo = { 42 };
struct B bar;
bar.b1 = 17;
copy(&amp;bar.b0, &amp;foo);
assert(bar.b1 == 17); // should be unchanged

If you wanted to have sizeof(struct B) == 8, then the b1 member would have to exist within the padding of the b0 member. So if copy(&amp;bar.b0, &amp;foo) does a 64-bit copy then it would overwrite it. We can't require that copy handle this case specially, because it could be compiled in an entirely separate file, and has no way of knowing whether its argument exists within some larger object. And we also can't tell the programmer they can't do copy(&amp;bar.b0, &amp;foo); the object bar.b0 is a bona fide object of type struct A and is entitled to all the rights and privileges of any object of that type.

So the only way out of this dilemma is for sizeof(struct B) to be larger than 8. And since its required alignment is still 4 (as inherited from struct A, as inherited from uint32_t), then necessarily sizeof(struct B) must be 12 or more.

答案2

得分: 0

这是一个既不安全也不推荐的方法,只是因为C语言不支持需要非2的幂对齐的某些大小的结构体而必须执行的操作。

感谢@KamilKuk指出,您可以对每个字段进行紧凑排列并使用alignas。然而,对于我的用例,我仍然无法使其完全正常工作。

看起来唯一能够做的事情就是手动指定对齐方式为字节。

#include <stdio.h>
#include <stdint.h>

struct __attribute__((__packed__)) A {
  uint32_t a0; // 4字节
  uint16_t a1; // 2字节
};

struct __attribute__((__packed__)) B {
  struct A b0; // 6字节
  uint16_t b1; // 2字节
};

struct __attribute__((__packed__)) C { // 总大小:
  struct A c0;  uint8_t __align0[2];
  struct A c1;
};

// 在数组内使用此结构,否则数组将不对齐。
struct __attribute__((__packed__)) C_El { struct C el; uint8_t __align0[2]; };

int main() {
  printf("sizeof(A)    = %u\n", sizeof(struct A));    // 6
  printf("sizeof(B)    = %u\n", sizeof(struct B));    // 8
  printf("sizeof(C)    = %u\n", sizeof(struct C));    // 14

  struct C arrC[4]; ssize_t arr0 = (ssize_t)arrC;
  printf("C索引指针: %u, %u\n", 0, (ssize_t)(&arrC[1]) - arr0);

  struct C_El arrC_El[4]; ssize_t arr0el = (ssize_t)arrC_El;
  printf("C_El索引指针: %u, %u\n", 0, (ssize_t)(&arrC_El[1]) - arr0el);
  return 0;
}

我希望有一个类似__attribute__((__packed_aligned__))的东西,用于整个结构体(以及子结构体),以保持对齐但不浪费字节。不幸的是,似乎确实缺少这样的功能。

英文:

This is neither safe nor recommended, it's just what needs to be done since C has zero support for a struct of some size that requires a non-power-of-2 alignment

Thanks to @KamilKuk for pointing out you can pack and use alignas on each field. However, I still couldn't quite make it work for my use-case.

It looks like the only thing that can be done is manually specify the alignments as bytes.

#include &lt;stdio.h&gt;                                                                        
#include &lt;stdint.h&gt;
  
struct __attribute__(( __packed__ )) A {                                                  
  uint32_t a0; // 4 bytes                                                                 
  uint16_t a1; // 2 byte                                                                  
};                                                                                        
                                                                                          
struct __attribute__(( __packed__ )) B {                                                  
  struct A b0; // 6 bytes                                                     
  uint16_t b1; // 2 bytes                                                                 
};                                                                                        
                                                                                          
struct __attribute__(( __packed__ )) C { // total size:                                   
  struct A c0;  uint8_t __align0[2];                                                      
  struct A c1;                                       
};                                                                                        
                                                                                          
// Use this inside arrays, otherwise arrays will be mis-aligned.                          
struct __attribute__(( __packed__ )) C_El { struct C el; uint8_t __align0[2]; };          
                                                                                          
int main() {                                                                              
  printf(&quot;sizeof(A)    = %u\n&quot;, sizeof(struct A));    // 6
  printf(&quot;sizeof(B)    = %u\n&quot;, sizeof(struct B));    // 8
  printf(&quot;sizeof(C)    = %u\n&quot;, sizeof(struct C));    // 14
                                                                                          
  struct C arrC[4]; ssize_t arr0 = (ssize_t)arrC;
  printf(&quot;C index pointers: %u, %u\n&quot;, 0, (ssize_t)(&amp;arrC[1]) - arr0);                    
                                                                                          
  struct C_El arrC_El[4]; ssize_t arr0el = (ssize_t)arrC_El;                              
  printf(&quot;C_El index pointers: %u, %u\n&quot;, 0, (ssize_t)(&amp;arrC_El[1]) - arr0el);            
  return 0;                                                                               
}

I wish there was an __attribute__(( __packed_aligned__ )) or similar for the whole struct (and sub-structs) to preserve alignment but not waste bytes. Unfortunately that really does seem to be missing.

huangapple
  • 本文由 发表于 2023年6月11日 23:44:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/76451239.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定