什么是数据对齐? 当在C中进行类型转换指针时,为什么以及何时应该担心?
我找不到一个体面的文档来解释对齐系统如何工作以及为什么某些类型比其他类型更严格一致。
我会试着解释一下。
什么是数据对齐?
您计算机中的体系结构由处理器和内存组成。 内存按单元格组织,因此:
0x00 | data | 0x01 | ... | 0x02 | ... |
每个存储器单元具有指定的大小,它可以存储的位数。 这取决于架构。
在C / C ++程序中定义变量时,程序将占用一个或多个不同的单元格。
例如
int variable = 12;
假设每个单元格包含32位且int
类型大小为32位,那么在内存中的某个位置:
variable: | 0 0 0 c | // c is hexadecimal of 12.
当您的CPU必须对该变量进行操作时,需要将其置于其寄存器中。 CPU可以从存储器中取出少量位的“ 1个时钟 ”,该大小通常称为WORD 。 此维度也取决于体系结构。
现在假设你有一个变量,由于一些偏移而存储在两个单元格中。
例如,我有两个不同的数据要存储(我将使用“ 字符串表示更清晰 ”):
data1: "ab" data2: "cdef"
所以内存将以这种方式组成(2个不同的单元格):
|abcd| |ef 0 0|
也就是说, data1
仅占用单元的一半,因此data2
占据剩余部分和第二单元的一部分。
现在假设您想要读取数据2。 CPU需要2个时钟才能访问数据,因为在一个时钟内它读取第一个单元,而在另一个时钟内它读取第二个单元中的剩余部分。
如果我们根据这个内存示例对齐 data2
,我们可以在第二个单元格中引入一种填充和移位数据2。
|ab 0 0| |cdef| --- padding
以这种方式,CPU将仅丢失“ 1个时钟 ”以访问数据2。
对齐系统的作用
对齐系统只是引入了填充以便将数据与系统的内存对齐,请记住根据体系结构。 当数据在存储器中对齐时,您不会浪费CPU周期来访问数据。
这是出于性能原因(99%的次数)。
这是“实现定义”,即对齐要求不是语言规范的一部分。
不同的CPU对对齐有不同的要求。 有些人无法解决不平坦地址上的16位值问题。 有些无法解决浮点值,除非与可被大小整除的地址对齐,有些可能。 等等。 有些人会比未正确对齐的数据对象更慢地访问未对齐的数据对象,有些则会因未对齐的访问而跳过。
这就是为什么语言标准没有详细说明哪种类型需要以哪种方式对齐(因为它不能),而是将其留给“实现” – 在这种情况下是编译器后端。
如果您对指针进行类型转换,则可能会强制代码在无法寻址的地址处寻址给定对象。 您需要确保“旧”类型的对齐要求至少与“新”类型的对齐要求一样严格。
在C ++ (C ++ 11向上)中,您可以使用alignof运算符来告诉您给定类型的对齐要求。 您还可以使用alignas运算符对给定类型或对象强制执行更严格的对齐。
在C (C11向上)中,您将获得_Alignof和_Alignas运算符,其中
包装到alignof
/ alignas
便利宏中 。 (谢谢,Lundin – C11不是我的强项。)
一些系统可以部分地访问存储器,例如32位字(4字节)。 这是硬件限制。 这意味着进入内存控制器的实际地址应该可以被4整除(因为它仍在寻址字节)。 因此,一旦你尝试在一个不能被4整除的地址上找到一个单词,就会有两个选项 – 编译器会尝试生成一些奇特的代码来组成两个内存访问中的单词,但情况并非总是如此。 有时它只会生成一个代码来访问给定地址的4个字节。 然后处理器将因数据对齐错误而失败。
这导致了语言的限制。
考虑代码(一个坏的):
uint8_t a[] = {1,2,3,4,5,6}; uint32_t b = *(uint32_t*)&a[1];
并假设a
与四边界可整除。 然后第二行试图从它的第二个元素的地址中读出一个单词,即一个不能被4整除的地址。 这将导致对齐错误。 但在C
, 严格的别名规则是禁止的。