MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C语言结构体大小与对齐的影响因素

2022-05-031.2k 阅读

C语言结构体大小与对齐的影响因素

结构体大小的基本概念

在C语言中,结构体是一种用户自定义的数据类型,它允许将不同类型的数据组合在一起。结构体的大小,即该结构体变量在内存中所占用的字节数,并非简单地是其成员变量大小的总和,而是受到多种因素的影响,其中最重要的就是内存对齐。

首先来看一个简单的结构体示例:

#include <stdio.h>

struct SimpleStruct {
    char a;
    int b;
};

int main() {
    printf("Size of SimpleStruct: %zu\n", sizeof(struct SimpleStruct));
    return 0;
}

在上述代码中,char类型通常占用1个字节,int类型在常见的32位和64位系统中一般占用4个字节。如果简单相加,char(1字节) + int(4字节) = 5字节。然而,当我们运行这段代码时,会发现sizeof(struct SimpleStruct)的结果并非5,而是8。这就是内存对齐在起作用。

内存对齐的基本原理

内存对齐是一种将数据存储在内存中的优化策略,其目的主要有两个:提高内存访问效率和满足硬件平台的特定要求。

现代计算机的内存系统通常以块(例如4字节、8字节等)为单位进行访问。如果一个数据类型的起始地址能够被其自身大小整除,那么CPU在访问该数据时可以通过一次内存访问操作完成,从而提高访问效率。例如,一个4字节的int类型变量,如果它的起始地址是4的倍数,CPU就能高效地读取它。

不同的硬件平台对数据存储的起始地址有不同的要求。有些平台要求特定类型的数据必须存储在特定对齐边界的地址上,否则可能会导致硬件异常。例如,某些RISC架构的CPU要求int类型数据的起始地址必须是4的倍数。

结构体成员对齐规则

  1. 第一个成员的对齐 结构体的第一个成员在结构体变量的内存起始地址处开始存储,其对齐方式与其自身类型的对齐要求一致。例如:
struct FirstMemberAlign {
    char c;
    short s;
};

这里char类型的成员c会从结构体变量的起始地址开始存储,由于char类型的对齐要求是1字节对齐,所以它没有额外的对齐填充。

  1. 后续成员的对齐 从第二个成员开始,每个成员的存储地址必须是其自身对齐要求的整数倍。如果当前位置不满足对齐要求,则需要在成员之间插入填充字节。继续上面的例子,short类型通常要求2字节对齐。c占用1字节后,下一个地址不一定是2的倍数,所以可能需要插入1字节的填充,使得short类型的s存储在满足2字节对齐的地址上。

下面通过代码来验证:

#include <stdio.h>

struct MemberAlign {
    char c;
    short s;
    int i;
};

int main() {
    printf("Size of MemberAlign: %zu\n", sizeof(struct MemberAlign));
    return 0;
}

char(1字节),short(2字节),int(4字节),简单相加为7字节。但实际运行会发现sizeof(struct MemberAlign)为8字节。因为char之后,为了让short满足2字节对齐,插入了1字节填充。short占用2字节后,为了让int满足4字节对齐,又插入了1字节填充,总共就是8字节。

  1. 结构体整体对齐 结构体的整体大小必须是其最宽基本数据类型成员对齐要求的整数倍。如果不是,则需要在结构体的末尾添加填充字节。例如:
struct WholeAlign {
    char c;
    double d;
};

char(1字节),double(8字节),简单相加为9字节。但double是最宽的基本数据类型,其对齐要求是8字节,所以结构体整体大小必须是8的倍数。因此,该结构体实际大小为16字节,在char之后插入了7字节填充,结构体末尾又插入了7字节填充。

影响结构体大小与对齐的因素

编译器相关因素

  1. 默认对齐方式 不同的编译器有不同的默认对齐方式。例如,GCC编译器在32位系统下默认的对齐方式是4字节对齐,在64位系统下默认是8字节对齐。而Microsoft Visual C++编译器在32位和64位系统下默认的对齐方式通常也是8字节对齐。这些默认对齐方式会影响结构体的大小和成员的对齐。
// 在GCC 64位系统下
struct DefaultAlignGCC {
    char c;
    int i;
};
// 在Microsoft Visual C++ 64位系统下
struct DefaultAlignVC {
    char c;
    int i;
};

在GCC 64位系统下,struct DefaultAlignGCC的大小为8字节,因为int是4字节,默认8字节对齐,char后插入3字节填充。在Microsoft Visual C++ 64位系统下,struct DefaultAlignVC同样大小为8字节,原因也是为了满足默认的8字节对齐要求。

  1. 对齐指令 编译器通常提供了一些指令来改变默认的对齐方式。在GCC中,可以使用__attribute__((packed))来取消结构体的内存对齐,强制按照成员的自然顺序紧凑排列。例如:
struct PackedStruct {
    char c;
    int i;
} __attribute__((packed));

int main() {
    printf("Size of PackedStruct: %zu\n", sizeof(struct PackedStruct));
    return 0;
}

这里struct PackedStruct的大小就是char(1字节) + int(4字节) = 5字节,因为取消了对齐,没有填充字节。

在Microsoft Visual C++中,可以使用#pragma pack(n)指令来设置对齐字节数n。例如#pragma pack(1)表示1字节对齐,即紧凑排列,#pragma pack(4)表示4字节对齐。

#pragma pack(1)
struct VC_PackedStruct {
    char c;
    int i;
};
#pragma pack() // 恢复默认对齐

int main() {
    printf("Size of VC_PackedStruct: %zu\n", sizeof(struct VC_PackedStruct));
    return 0;
}

在上述代码中,struct VC_PackedStruct的大小为5字节,因为设置了1字节对齐,没有额外的填充。

数据类型相关因素

  1. 基本数据类型的对齐要求 不同的基本数据类型有不同的对齐要求。常见的基本数据类型及其对齐要求如下:
  • char:1字节对齐,因为其本身大小就是1字节,任何地址都满足其对齐要求。
  • short:通常2字节对齐,这是为了提高访问效率,因为CPU在访问2字节数据时,如果地址是2的倍数,可以更高效地操作。
  • int:在32位和64位系统中通常4字节对齐,同样是为了优化内存访问,4字节的整数数据如果存储在4的倍数地址上,CPU可以一次读取。
  • float:通常4字节对齐,其存储方式和访问特性决定了这样的对齐要求。
  • double:通常8字节对齐,由于double类型数据的存储和处理需要更高的精度和更大的存储空间,8字节对齐有助于提高访问效率。
struct DataTypeAlign {
    char c;
    short s;
    int i;
    float f;
    double d;
};

在这个结构体中,c(1字节),之后为了short的2字节对齐插入1字节填充,short(2字节),之后为了int的4字节对齐插入1字节填充,int(4字节),float(4字节),之后为了double的8字节对齐插入4字节填充,结构体整体大小为24字节(因为double是最宽类型,整体大小需是8的倍数)。

  1. 自定义数据类型(结构体嵌套)的对齐 当结构体中包含其他结构体成员时,嵌套结构体的对齐方式遵循其自身的对齐规则,同时外部结构体的对齐也要考虑嵌套结构体的对齐要求。例如:
struct InnerStruct {
    char c;
    int i;
};

struct OuterStruct {
    struct InnerStruct is;
    char c2;
};

struct InnerStruct本身大小为8字节(char后插入3字节填充,int 4字节)。struct OuterStruct中,is作为成员,其对齐要求和struct InnerStruct一致,is之后为了c2的1字节对齐不需要额外填充,struct OuterStruct整体大小为9字节,但由于最宽类型是int(4字节对齐要求),所以最终大小为12字节,在c2后插入3字节填充。

硬件平台相关因素

  1. 不同硬件平台的对齐要求差异 不同的硬件平台对数据对齐有不同的要求。例如,一些嵌入式系统的硬件平台可能要求更严格的对齐,以提高数据处理的效率和稳定性。在ARM架构的一些处理器中,虽然可以支持非对齐访问,但非对齐访问会导致性能下降,甚至在某些情况下会引发硬件异常。 在x86架构的处理器中,对非对齐访问有较好的兼容性,但仍然推荐按照对齐规则进行数据存储,以提高性能。例如,在一个需要频繁访问结构体成员的循环中,如果结构体成员没有正确对齐,在x86架构下虽然不会出错,但会增加内存访问的周期,降低程序的执行效率。

  2. 硬件平台对结构体大小和对齐的影响示例 假设我们有一个在特定嵌入式硬件平台上运行的程序,该平台要求int类型必须8字节对齐。

struct HardwareAlign {
    char c;
    int i;
};

在这个平台上,char(1字节)后需要插入7字节填充,使得int能够满足8字节对齐,所以struct HardwareAlign的大小为16字节。如果不按照这个硬件平台的对齐要求进行结构体设计,可能会导致程序运行出错或性能严重下降。

结构体大小与对齐对程序的影响

内存使用方面

  1. 内存浪费与优化 不合理的结构体对齐可能导致内存浪费。例如,在一个包含大量相同结构体变量的数组中,如果结构体大小因为对齐而比实际成员大小总和大很多,就会浪费大量内存。
struct MemoryWaste {
    char c1;
    double d;
    char c2;
};

// 假设有一个包含1000个这种结构体的数组
struct MemoryWaste arr[1000];

struct MemoryWaste中,char(1字节),之后为了double的8字节对齐插入7字节填充,double(8字节),char(1字节),之后为了结构体整体对齐(double是最宽类型,8字节对齐要求)插入7字节填充,结构体大小为24字节。但实际成员大小总和为1 + 8 + 1 = 10字节。如果有1000个这样的结构体,就浪费了(24 - 10) * 1000 = 14000字节的内存。

通过合理调整结构体成员顺序,可以优化内存使用。比如将上述结构体改为:

struct MemoryOptimize {
    char c1;
    char c2;
    double d;
};

char(1字节),char(1字节),之后为了double的8字节对齐插入6字节填充,结构体大小为16字节,相比之前节省了8字节。对于大量的结构体实例,内存节省效果显著。

  1. 内存布局对缓存命中率的影响 结构体的内存布局和对齐方式会影响缓存命中率。当CPU访问内存中的数据时,会先将数据从内存加载到缓存中。如果结构体成员的对齐不合理,可能导致不同成员分布在不同的缓存行中,从而增加缓存不命中的概率。 例如,一个结构体中有两个频繁访问的成员,由于对齐原因,它们被分配到不同的缓存行。当程序交替访问这两个成员时,就可能导致缓存频繁加载不同的缓存行,降低缓存命中率,进而影响程序性能。

程序性能方面

  1. 数据访问效率 合理的结构体对齐可以提高数据访问效率。如前面所述,当数据按照其对齐要求存储时,CPU可以通过更少的内存访问操作来获取数据。例如,对于一个包含int数组的结构体,如果int成员都满足4字节对齐,CPU可以一次读取4字节的数据,而不需要进行多次内存访问。
struct IntArrayStruct {
    int arr[10];
};

如果这个结构体的起始地址是4的倍数,那么CPU在访问arr数组中的元素时,可以高效地进行读取和写入操作。

  1. 函数调用与参数传递 在函数调用时,结构体作为参数传递也会受到对齐的影响。如果结构体参数没有正确对齐,在函数调用过程中可能需要进行额外的处理,如临时调整结构体的对齐,这会增加函数调用的开销。
void func(struct AlignParam {
    char c;
    int i;
} param) {
    // 函数体
}

int main() {
    struct AlignParam p;
    func(p);
    return 0;
}

如果struct AlignParam没有正确对齐,在将p传递给func函数时,可能需要在栈上进行一些额外的操作来确保参数的对齐,从而影响函数调用的性能。

总结结构体大小与对齐的实践要点

  1. 了解编译器和硬件平台 在编写C语言程序时,要清楚所使用的编译器的默认对齐方式以及目标硬件平台的对齐要求。不同的编译器和硬件平台可能有很大差异,这会直接影响结构体的大小和行为。可以查阅编译器文档和硬件平台手册来获取这些信息。

  2. 优化结构体设计 通过合理调整结构体成员的顺序,可以减少填充字节,优化内存使用。尽量将小的成员放在一起,大的成员放在后面,并且按照对齐要求相近的成员放在一起的原则进行设计。同时,在对内存使用非常敏感的场景下,可以考虑使用__attribute__((packed))(GCC)或#pragma pack(n)(Microsoft Visual C++)等指令来取消或调整对齐,但要注意可能对性能产生的影响。

  3. 考虑兼容性和可移植性 如果程序需要在不同的编译器和硬件平台上运行,要谨慎处理结构体的对齐。避免依赖特定编译器或平台的默认对齐方式,尽量采用可移植的对齐策略。可以通过宏定义等方式来根据不同的编译环境设置合适的对齐方式,以确保程序在各种平台上都能正确运行且性能良好。

总之,深入理解C语言结构体大小与对齐的影响因素,对于编写高效、可移植且内存使用合理的程序至关重要。在实际编程中,需要综合考虑各种因素,做出最合适的选择。