数据结构对齐

数据结构对齐是程式编译后资料在记忆体内的布局与使用方式。包括三方面内容:数据对齐数据结构填充(padding)与包入(packing)。

现代计算机CPU一般是以32位元64位元大小作地址对齐,以32位元架构的计算机举例,每次以连续的4字节为一个区间,第一个字节的位址位在每次CPU抓取资料大小的边界上,除此之外,如果要访问的变量没有对齐,可能会触发总线错误

当资料小于计算机的字(word)尺寸,可能把几个资料放在一个字中,称为包入(packing)。

许多编程语言自动处理数据结构对齐。Ada语言[1][2] PL/I,[3] Pascal,[4] 某些C语言C++实现, D语言,[5] Rust,[6]汇编语言允许特别控制对齐的方式。

定义

内存地址a被称为n字节对齐an的倍数(n应是2的),也可以理解为当被访问的数据长度为n 字节时,数据地址为n字节对齐。如果内存未对齐,称作misaligned

内存指针是对齐的,如果它所指的数据是对齐的。指向聚合数据(aggregate data,如struct或数组)是对齐的,当且仅当它的每个组成数据是对齐的。

体系结构

RISC

大多数RISC处理器在加载或存储指令访问错位的地址时,会产生一个对齐错误。这允许操作系统使用其他指令来模拟错位的访问。例如,对齐错误处理程序可以使用字节加载或存储(其总是对齐的)来模拟更大的加载或存储指令。

一些架构,如MIPS架构有特殊的无对齐加载和存储指令。一条无对齐的加载指令从具有最低字节地址的内存字中获取字节,另一条指令从具有最高字节地址的内存字中获取字节。同样的,store-high和store-low指令分别在较高和较低的内存字中存储相应的字节。

DEC Alpha架构对不对齐的加载和存储采用了两步法。第一步是将上层和下层的内存字加载到独立的寄存器中。第二步是使用类似于MIPS指令的特殊低/高指令来提取或修改内存字。通过将修改后的内存字存储到内存中,就完成了一个无对齐存储。造成这种复杂性的原因是,最初的Alpha架构只能读取或写入32位或64位的值。这被证明是一个严重的限制,经常导致代码臃肿和性能不佳。为了解决这个限制,在最初的架构中加入了一个名为 "字节字扩展"(BXW)的扩展。它包括字节和字的加载和存储指令。

因为这些指令比正常的内存加载和存储指令更大、更慢,所以只有在必要时才可以使用它们。一些C和C++编译器有一个 "无对齐 "属性,可以应用于需要无对齐指令的指针。

x86

x86体系架构最初是不要求内存对齐。一些SSE2指令要求数据是128比特(16字节)对齐。有些CPU指令用于未对齐访问如MOVDQU。读写内存操作仅在对齐时才是原子的。

C语言struct在x86上的对齐

C语言数据结构内的成员按照先后顺序在内存中存储。

默认对齐

结构体中每个成员的类型通常有一个默认的对齐方式,也就是说,除非程序员另有要求,否则它将在一个预先确定的边界上对齐。以下典型的对齐方式对微软Visual C++)、Borland/CodeGear英语CodeGearC++Builder)、Digital Mars英语Digital Mars(DMC)和GNUGCC)的编译器在为32位x86编译时有效。

一个char(一个字节)变量将被1字节对齐。

一个short(两个字节)变量将是2字节对齐的。

一个int(四个字节)变量将是4字节对齐的。

一个long(四个字节)变量将被4字节对齐。

一个float(四个字节)变量将是4字节对齐的。

一个double(8个字节)变量在Windows上是8字节对齐的,在Linux上是4字节对齐的(用-malign-double编译时选项是8字节)。

一个long long(8个字节)变量将被4字节对齐。

一个long double(C++Builder和DMC为10个字节,Visual C++为8个字节,GCC为12个字节)变量在C++Builder上将是8字节对齐,DMC为2字节对齐,Visual C++为8字节对齐,GCC为4字节对齐。

任何指针(四个字节)都将是4字节对齐的。(例如:char*, int*)

与32位系统相比,LP64 64位系统在对齐方面唯一值得注意的区别是。

一个long(八个字节)变量将是8字节对齐的。

一个double(8个字节)变量将是8字节对齐的。

一个long long(8个字节)变量将是8字节对齐的。

一个long double(在Visual C++中是8个字节,在GCC中是16个字节)变量在Visual C++中是8字节对齐的,在GCC中是16字节对齐的。

任何指针(八个字节)变量都将是8字节对齐的。

有些数据类型取决于实现方式。

指定对齐

一些编译器(Microsoft,[7] Borland, GNU,[8]等等)使用#pragma directive指定对齐的包入(packing)。例如:

#pragma pack(push)  /* push current alignment to stack */
#pragma pack(1)     /* set alignment to 1 byte boundary */

struct MyPackedData
{
    char Data1;
    long Data2;
    char Data3;
};

#pragma pack(pop)   /* restore original alignment from stack */

这个结构在32位系统的大小为6字节。

缺省packing与#pragma pack

Microsoft编译器的项目缺省packing(编译选项/Zp)与#pragma pack指令。#pragma pack指令仅能减少packing尺寸。[9]

参见

参考文献

  1. ^ Ada Representation Clauses and Pragmas. GNAT Reference Manual 7.4.0w documentation. [2015-08-30]. (原始内容存档于2015-10-13). 
  2. ^ F.8 Representation Clauses. SPARCompiler Ada Programmer's Guide (PDF). [2015-08-30]. (原始内容存档 (PDF)于2021-12-16). 
  3. ^ IBM System/360 Operating System PL/I Language Specifications (PDF). IBM. July 1966: 55–56 [2017-11-21]. C28-6571-3. (原始内容存档 (PDF)于2019-05-29). 
  4. ^ Niklaus Wirth. The Programming Language Pascal (Revised Report) (PDF): 12. July 1973 [2017-11-21]. (原始内容 (PDF)存档于2015-03-15). 
  5. ^ Attributes - D Programming Language: Align Attribute. [2012-04-13]. (原始内容存档于2012-04-09). 
  6. ^ The Rustonomicon - Alternative Representations. [2016-06-19]. (原始内容存档于2016-05-09). 
  7. ^ pack. [2017-11-21]. (原始内容存档于2017-03-28). 
  8. ^ 6.58.8 Structure-Packing Pragmas. [2017-11-21]. (原始内容存档于2017-01-08). 
  9. ^ Working with Packing Structures. MSDN Library. Microsoft. 2007-07-09 [2011-01-11]. (原始内容存档于2012-10-18). 

外部链接