Post

Alignment and Padding

Alignment and Padding

Background

  上篇文章Unaligned Fault - Defect Raised by Compiler Optimization里提出了疑问,编译器是怎么在内存中安排所定义变量的。本文将用一些示例来展示结果。

Compiler: (Arm GNU Toolchain 13.3.Rel1 (Build arm-13.24)) 13.3.1 20240614

Data Model

  在开始这个话题前有必要知道各个数据类型的长度是如何定义的。有童鞋会问,这不是定好的吗?比如char是1B,int是4B。规定好的不假,然而不同的data model里,数据长度有可能是不同的。主要的data model有LP32,ILP32,LLP64,ILP64,LP64。下表列出在各个data model下,不同数据类型的长度:

typeLP32ILP32LLP64ILP64LP64
char8b8b8b8b8b
short16b16b16b16b16b
int16b32b32b64b32b
long32b32b32b64b64b
long long64b64b64b64b64b
pointer32b32b64b64b64b

aarch32默认的data modle为ILP32 aarch64默认的data modle为LP64

Alignment and Padding

  上文里提到,非对齐访问对比对齐访问,在硬件实现上要加另外的逻辑,在软件上又会降低性能,所以编译器会默认会把变量按照对齐的方式放置,比如下面的例子:

1
2
3
4
    uint8_t a;
    uint16_t b;
    uint8_t c;
    uint32_t d;

  编译来看看实际是如何放置的。这里要注意的是,c中变量所在的位置有三种情况,bss, data,or stack,申明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
uint8_t a_bss;
uint16_t b_bss;
uint8_t c_bss;
uint32_t d_bss;

uint8_t a_data = 1;
uint16_t b_data = 2;
uint8_t c_data = 3;
uint32_t d_data = 4;

void print_address(void)
{
    uint8_t a_stack;
    uint16_t b_stack;
    uint8_t c_stack;
    uint32_t d_stack;
    
    printf("a_stack = 0x%x\n", (unsigned int)(uintptr_t)&a_stack);
	printf("b_stack = 0x%x\n", (unsigned int)(uintptr_t)&b_stack);
	printf("c_stack = 0x%x\n", (unsigned int)(uintptr_t)&c_stack);
	printf("d_stack = 0x%x\n", (unsigned int)(uintptr_t)&d_stack);

	printf("a_bss = 0x%x\n", (unsigned int)(uintptr_t)&a_bss);
	printf("b_bss = 0x%x\n", (unsigned int)(uintptr_t)&b_bss);
	printf("c_bss = 0x%x\n", (unsigned int)(uintptr_t)&c_bss);
	printf("d_bss = 0x%x\n", (unsigned int)(uintptr_t)&d_bss);

	printf("a_data = 0x%x\n", (unsigned int)(uintptr_t)&a_data);
	printf("b_data = 0x%x\n", (unsigned int)(uintptr_t)&b_data);
	printf("c_data = 0x%x\n", (unsigned int)(uintptr_t)&c_data);
	printf("d_data = 0x%x\n", (unsigned int)(uintptr_t)&d_data);
}

  结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
a_stack = 0xf9206270
b_stack = 0xf9206272
c_stack = 0xf9206271
d_stack = 0xf9206274
a_bss = 0xf92062e0
b_bss = 0xf92062e2
c_bss = 0xf92062e4
d_bss = 0xf9206318
a_data = 0xf9206390
b_data = 0xf9206392
c_data = 0xf9206394
d_data = 0xf9206398

  通过结果可以看到,所有的变量都是对齐的,但在stack中的变量做了重排,节省了4个字节的空间。下面来直观的看下区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bss, data:
   3                   2                   1                   0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|               b               |    Reserved   |       a       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Reserved                   |       c       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               d                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

stack:
   3                   2                   1                   0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|               b               |       c       |       a       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               d                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

  结构又和变量的排列有区别。此外还要明确当前使用的是data model(后续例子都以LP64为例)。看下例:

1
2
3
4
5
6
typedef struct {
    uint8_t a;
    uint16_t b;
    uint8_t c;
    uint32_t d;
} test1_t;

  对于结构编译器是不会重排节省空间的,所以它的layout如下:

1
2
3
4
5
6
7
8
   3                   2                   1                   0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|               b               |    Reserved   |       a       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Reserved                   |       c       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               d                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

  是不是很简单?如果是结构套结构呢?

1
2
3
4
5
6
7
8
9
10
11
typedef struct {
    uint8_t a;
    uint8_t b;
    uint8_t c;
} test2_t;

typedef struct {
    uint8_t a;
    uint8_t b;
    test2_t c;
} test3_t;

  在结构套结构的时候就把内层结构展开,而不是以内层结构的大小做对齐。这里结果是5。

1
2
3
4
5
typedef struct {
    uint8_t a;
    uint64_t b;
    uint8_t c;
} test4_t;

  还有这个,结果是24,因为要保证uint64_t的对齐,所以结构的size是其中最大的基本类型的整数倍。既然这样,猜一下下面这个结构所占的size。

1
2
3
4
typedef struct {
    uint64_t a;
    uint8_t b;
} test5_t;

  总结如下:

  • 结构中每个变量都要放在对齐的位置
  • 结构套结构的时候,把内层结构展开
  • 结构大小总是其中最大数据类型大小的整数倍

  另外在某些特殊应用里,开发者不想让编译器把结构做对齐,也可以用__attribute__((packed))来定义结构,这样得到的size就是结构中所以变量size的和。

1
2
3
4
typedef struct {
    uint64_t a;
    uint8_t b;
} __attribute__((packed)) test6_t;

  上面定义的结构在内存中占用的字节就是8 + 1 = 9B。

  这里还有另外一个可以限定范围的pack,如下:

1
2
3
4
5
6
7
8
#pragma pack(push)
#pragma pack(1)
//or #pragma pack(push, 1)
typedef struct {
    uint64_t a;
    uint8_t b;
} test7_t;
#pragma pack(pop)

  上例中在push/pop范围内struct的定义都遵循1B alignment。pop后则恢复默认。当然并非所有的编译器都支持这种方式(本文中所用arm compiler是支持的)。

  最后附一张对齐非对齐的表以供参考。

alignment

Reference

Why LP64?
Alignment, Padding and Data Packing
Data Structure Alignment
Deep Dive
Data Alignment Problems
Data Alignment, Padding, and Optimization Techniques

This post is licensed under CC BY 4.0 by the author.

© sfeng. Some rights reserved.

Using the Chirpy theme for Jekyll.