C语言字节对齐与访问效率的研究
字节对齐
1. 什么是字节对齐?
在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
编译器为了提高CPU对数据成员的访问速度,通常会在不同大小的数据成员之间插入空白内存(padding),进行补齐操作,即内存对齐。
2. 默认对齐规则
规则1:结构体(struct)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存放在offset为该数据成员大小的整数倍的地方(比如int在32位机为4字节,则要从4的整数倍地址开始存储)。
规则2:如果一个结构体B里嵌套另一个结构体A,则结构体A应从offset为A内部最大成员的整数倍的地方开始存储。(struct B里存有struct A,A里有char,int,double等成员,那A应该从8的整数倍开始存储。),结构体A中的成员的对齐规则仍满足原则1、原则2。
规则3:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。
3. 字节对齐有什么作用?
字节对齐的主要作用是提高cpu访问效率,同时合理的利用字节对齐可以有效地节省存储空间。
对于32位机来说,4字节对齐能够使cpu访问速度提高,比如说一个uint32_t类型的变量,如果跨越了4字节边界存储,那么cpu要读取两次,这样效率就低了。但是在32位机中使用1字节或者2字节对齐,反而会使变量访问速度降低。所以这要考虑处理器类型,另外还得考虑编译器的类型。在vc中默认是4字节对齐的,GNU gcc 也是默认4字节对齐。
注意:在设计不同CPU下的通信协议时或者编写硬件驱动程序时寄存器的结构这两个地方都一定要考虑字节对齐方式,以免不同的编译器生成的代码不一样。
4. 更改字节对齐规则
在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:
- 编译选项
使用伪指令#pragma pack (n),C编译器将按照n个字节对齐,使用伪指令#pragma pack (),取消自定义字节对齐方式。
#pragma pack(1) //让编译器对这个结构作1字节对齐
typedef struct
{
char a;
short b;
int d;
} tstTestType;
#pragma pack() //取消1字节对齐,恢复为默认4字节对齐
- attribute属性
__attribute__((aligned (n)))让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
__attribute ((packed))取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。它也可以指定某个数据成员不需要进行内存对齐。
typedef struct __attribute__((packed))
{
char a;
short b;
int d;
} tstTestType;
注意:__attribute__((packed))和 #pragma pack(n)的作用范围不同。使用__attribute__((packed))可以精准地对其声明的结构体或者联合体进行打包压缩,而不会影响其他及结构体,甚至是其子结构体(其数据成员也为结构体)的正常内存对齐。而 #pragma pack(n) 的作用范围更大,在使用#pragma pack(n)之后的所有结构体联合体(子结构体若在#pragma pack(n)之前不受影响)的内存对齐都会受到其影响。
字节对齐测试
#pragma pack(n)实验
/* ================= test 1 ==================*/
struct stc
{
char one;
short two;
char three;
int four;
} cc;
#pragma pack (1)
struct stcd
{
char one;
short two;
char three;
int four;
} dd;
typedef struct
{
char sex;
int length;
char name[10];
} ee;
// sizeof(cc) = 12
// sizeof(dd) = 8
// sizeof(ee) = 20
/* ================= test 2 ==================*/
#pragma pack (1)
struct stc
{
char one;
short two;
char three;
int four;
} cc;
struct stcd
{
char one;
short two;
char three;
int four;
} dd;
// sizeof(cc) = 8
// sizeof(dd) = 8
/* ================= test3 ==================*/
typedef struct
{
char one;
short two;
char three;
int four;
} cc;
struct stcd
{
char one;
short two;
char three;
int four;
cc C;
} dd;
// sizeof(cc) = 12
// sizeof(dd) = 24
/* ================= test4 ==================*/
#pragma pack (1)
typedef struct
{
char one;
short two;
char three;
int four;
}cc;
struct stcd
{
char one;
short two;
char three;
int four;
cc C;
} dd;
// sizeof(cc) = 8
// sizeof(dd) = 16
/* ================= test5 ==================*/
typedef struct
{
char one;
short two;
char three;
int four;
} cc;
#pragma pack (1)
struct stcd
{
char one;
short two;
char three;
int four;
cc C;
} dd;
// sizeof(cc) = 12
// sizeof(dd) = 20
/* ================= test6 ==================*/
typedef struct
{
char one;
short two;
char three;
int four;
} cc;
struct stcd
{
char one;
short two;
char three;
int four;
#pragma pack (1)
cc C;
} dd;
// sizeof(cc) = 12
// sizeof(dd) = 20
从test4,test5和test6可以看到,虽然 在#pragma pack(n)之后,都会受到其对齐影响,但是结构体dd中的C结构体仍保持自然的内存对齐,并未受影响。此外,在结构体dd内使用#pragma pack(n),仍相当于对整个结构体dd起作用。
__attribute__((packed))实验
/* ================= test1 ==================*/
typedef struct __attribute__((packed))
{
char one;
short two;
char three;
int four;
} stc;
stc cc;
struct stcd
{
char one;
short two;
char three;
int four;
stc C;
} dd;
// sizeof(cc) = 8
// sizeof(dd) = 20
/* ================= test2 ==================*/
typedef struct __attribute__((packed))
{
char one;
short two;
char three;
int four;
} stc;
stc cc;
struct __attribute__((packed)) stcd
{
char one;
short two;
char three;
int four;
stc C;
} dd;
// sizeof(cc) = 8
// sizeof(dd) = 16
/* ================= test3 ==================*/
typedef struct
{
char one;
short two;
char three;
int four;
} stc;
stc cc;
struct __attribute__((packed)) stcd
{
char one;
short two;
char three;
int four;
stc C;
} dd;
// sizeof(cc) = 12
// sizeof(dd) = 20
/* ================ test 4 ==============*/
typedef struct
{
char one;
short two;
char three;
int four;
} stc;
stc __attribute__((packed)) cc;
// sizeof(cc) = 12
/* ================ test 5 ==============*/
typedef struct __attribute__((packed))
{
char one;
short two;
char three;
int four;
} stc;
stc cc;
// sizeof(cc) = 8
使用__attribute__((packed))可以精准地对想要打包压缩的结构体进行操作,而不像#pragma pack(n)的全局生效。同时也是对整个结构体生效,如果结构体内的数据成员仍为结构体,则不对子结构体生效。 __attribute__((packed))相当于#pragma pack(1),即尽可能地压缩结构体空间,不像#pragma pack(n)那样可以选择不同大小的对齐尺寸。
使用__attribute__((packed))声明变量时,要注意其摆放位置,比如test4的声明是错误的,虽然可以编译通过,但是__attribute__((packed))并不会生效,还会报一个warning:
[Warning] 'packed' attribute ignored [-Wattributes]
访问效率测试
我们知道字节对齐影响CPU访问速度,那实际影响有多大呢?我们在STM32上进行一个测试。
使用的MCU为STM32F446 主频最高180MHz,定时器配置如下,设置定时器时钟90MHz,每个计数值为1/90000000s(约为11ns),定时器周期设置了一个整数为60000的计数值(16位定时器)。
/* Compute the prescaler value to have TIMx counter clock equal to 90 MHz */
uwPrescalerValue = (uint32_t)((SystemCoreClock) / 90000000) - 1;
/* Set TIMx instance */
TimHandle.Instance = TIMx;
/* Initialize TIMx peripheral as follows:
+ Period = 10000 - 1
+ Prescaler = (SystemCoreClock / 90MHz) - 1
+ ClockDivision = 0
+ Counter direction = Up
*/
TimHandle.Init.Period = 60000 - 1;
TimHandle.Init.Prescaler = uwPrescalerValue;
TimHandle.Init.ClockDivision = 0;
TimHandle.Init.CounterMode = TIM_COUNTERMODE_UP;
TimHandle.Init.RepetitionCounter = 0;
if (HAL_TIM_Base_Init(&TimHandle) != HAL_OK)
{
/* Initialization Error */
Error_Handler();
}
数据类型定义如下:
#pragma pack(1)
typedef struct {
uint8_t a;
uint32_t b;
} tstTestAlignType;
#pragma pack()
tstTestAlignType stTest;
测试代码如下:
volatile uint32_t u32TimeIntCnt = 0; /* Timer overflow interrupt count. */
uint32_t u32TimerCounter = 0; /* The current count of the timer .*/
#define LOOP_COUNT 100000
printf("****** Test start. ******\r\n");
__HAL_TIM_SET_COUNTER(&TimHandle, 0);
u32TimeIntCnt = 0;
for(volatile uint32_t i = 0; i < LOOP_COUNT; i++)
{
stTest.b = i;
stTest.a = stTest.b & 0xFF;
}
HAL_TIM_Base_Stop_IT(&TimHandle);
u32TimerCounter = __HAL_TIM_GET_COUNTER(&TimHandle);
printf("****** Test end. ******\r\n");
printf("Loop count is %d, time elapse %dns.\n\r", LOOP_COUNT, (u32TimeIntCnt * 60000 + u32TimerCounter) * 11);
宏LOOP_COUNT为100000,1000000,10000000分别测试,结果如下:
****** Test start. ******
****** Test end. ******
Loop count is 100000, time elapse 7162903ns.
****** Test start. ******
****** Test end. ******
Loop count is 1000000, time elapse 71635025ns.
****** Test start. ******
****** Test end. ******
Loop count is 10000000, time elapse 716353121ns.
修改为4byte对齐重新测试,结果如下:
#pragma pack(4)
typedef struct {
uint8_t a;
uint32_t b;
} tstTestAlignType;
#pragma pack()
tstTestAlignType stTest;
****** Test start. ******
****** Test end. ******
Loop count is 100000, time elapse 6061616ns.
****** Test start. ******
****** Test end. ******
Loop count is 1000000, time elapse 60613542ns.
****** Test start. ******
****** Test end. ******
Loop count is 10000000, time elapse 606140942ns.
对比结果:
| Loop Count | Align 1byte Use Time | Align 4byte Use Time | 效率提升 |
|---|---|---|---|
| 100000 | 7162903ns | 6061616ns | 15.37% |
| 1000000 | 71635025ns | 60613542ns | 15.38% |
| 10000000 | 716353121ns | 606140942ns | 15.38% |
根据测试结果可知在STM32中字节对齐对访问效率影响较大,当然在不同硬件平台上的影响会有差异,因此在高频的数据处理应用中,定义结构体时应作仔细考虑。

本文详细探讨了C语言中的字节对齐原理、默认规则及其作用,以及如何通过编译选项改变对齐规则。通过STM32的测试实例,展示了字节对齐对CPU访问速度的显著影响,特别是在高频数据处理中合理选择对齐的重要性。
1370

被折叠的 条评论
为什么被折叠?



