什么是“堆栈对齐”?

63
什么是栈对齐? 为什么要使用它? 编译器设置可以控制它吗?
此问题的细节源于尝试在msvc中使用ffmpeg库时遇到的问题,但我真正感兴趣的是什么是“栈对齐”的解释。
详情如下:
当运行连接到avcodec的msvc编译程序时,我会收到以下错误:“编译器未对齐堆栈变量。Libavcodec已经编译错误”,随后在avcodec.dll中崩溃。 avcodec.dll没有使用msvc编译,因此我无法看到内部发生了什么。 当运行ffmpeg.exe并使用相同的avcodec.dll时,一切都运转良好。 ffmpeg.exe没有使用msvc进行编译,它是使用gcc / mingw(与avcodec.dll相同)进行编译的。
谢谢,

1
既然其他人已经解释了堆栈对齐是什么以及为什么要使用它,我只想就“编译器设置是否可以控制它?”这个问题发表一下我的看法。请参见此问题 - andreee
4个回答

156

内存中变量的对齐方式(简史)。

过去计算机有一个8位数据总线。这意味着每个时钟周期可以处理8位信息。那时候还不错。

然后出现了16位计算机。由于向下兼容性和其他问题,保留了8位字节,并引入了16位字。每个字是2个字节。每个时钟周期可以处理16位信息。但这带来了一个小问题。

让我们看一下内存映射:

+----+
|0000| 
|0001|
+----+
|0002|
|0003|
+----+
|0004|
|0005|
+----+
| .. |

每个地址都有一个可以单独访问的字节。但是只能在偶数地址处获取字。因此,如果我们在0000处读取一个字,我们读取0000和0001处的字节。但是如果我们想要读取位置0001处的字,我们需要进行两次读取操作。首先读取0000,0001,然后读取0002,0003,我们只保留0001,0002。

当然,这会花费额外的时间,而且人们并不欣赏这种方式。因此他们发明了对齐方式。因此,我们将字变量存储在字边界上,将字节变量存储在字节边界上。

例如,如果我们有一个包含一个字节字段(B)和一个字字段(W)的结构体(并且使用非常简单的编译器),我们得到以下结果:

+----+
|0000| B
|0001| W
+----+
|0002| W
|0003|
+----+

这并不好玩。但是当使用单词对齐时,我们发现:

+----+
|0000| B
|0001| -
+----+
|0002| W
|0003| W
+----+

在这里,为了提高访问速度而牺牲了内存。

可以想象,当使用双字(4字节)或四字(8字节)时,这一点更加重要。这就是为什么在大多数现代编译器中,编译程序时可以选择使用哪种对齐方式。


10
栈对齐的描述很好! - Shaun Bouckaert
2
这很好地解释了为什么单词数组应该对齐。因为访问特定元素否则需要两次读取。但在包含字节和单词的结构的示例中:如果您读取完整结构,则在两种情况下都必须读取两个单词。 - Michael Lehn
3
@ToonKrijthe 为什么呢?机器可以以2字节的块读取内存,在您的示例中(如0000、0001或0002、0003),只需一次读取操作。因此,如果我的地址寄存器指向0001(奇地址而不是偶地址),那么我可以直接在一次读取操作中从那里读取2个字节(即0001和0002),对吗? - User 10482
2
@User10482 去读一下PDF《每个程序员都应该知道的关于内存的知识》,第7页,图2.7。内存不像你在脑海中想象的那样以行形式存储,它是一个矩阵,因此你可以一次读取整行,但如果没有对齐,你需要进行两次读取访问。 - user2164703
2
@User10482 可能有用 - Vencat
显示剩余6条评论

13
一些CPU体系结构需要特定的各种数据类型对齐方式,如果您不遵守此规则,将会引发异常。在标准模式下,x86不要求基本数据类型进行对齐,但可能会受到性能惩罚(请查看www.agner.org以获取低级优化技巧)。
然而,SSE指令集(通常用于高性能)音频/视频处理具有严格的对齐要求,如果您尝试在未对齐的数据上使用它,则会引发异常(除非您使用某些处理器上速度较慢的未对齐版本)。
您的问题可能是一个编译器希望调用者保持堆栈对齐,而另一个编译器希望callee在必要时对齐堆栈。 编辑:至于为什么会出现异常,DLL中的例程可能希望在一些临时堆栈数据上使用SSE指令,并因为两个不同的编译器不同意调用约定而失败。

12
据我所知,堆栈对齐是指将变量放置在堆栈上,使其“对齐”到特定字节的数量。因此,如果您使用16位堆栈对齐,堆栈上的每个变量都将从当前函数内的堆栈指针的2字节倍数开始。

这意味着如果您使用小于2字节的变量,例如char(1字节),则它与下一个变量之间会有8位未使用的“填充”。这样可以基于变量位置进行某些优化假设。

调用函数时,向下一个函数传递参数的一种方法是将它们放置在堆栈上(而不是直接放入寄存器中)。这里是否正在使用对齐非常重要,因为调用函数将变量放置在堆栈上,由偏移量读取调用函数。如果调用函数对齐了变量,并且被调用函数希望它们不对齐,则被调用函数将无法找到它们。

似乎msvc编译的代码对于变量对齐存在争议。尝试关闭所有优化后再编译。


1
sizeof(char)始终为1字节,至少为8位...而不是字节。对齐取决于编译器平台,(x86,无论如何)通常为32位体系结构的4字节,64位体系结构的8字节。 - snemarch
1
谢谢,确实是个脑抽,我在字节大小上犯了错误 :P。我之前选的是16字节作为任意示例,但用更小的例子会更清晰明了。 - Shaun Bouckaert
不,栈对齐是指维护堆栈指针本身的对齐。堆栈上的单字节局部变量可以位于任何地址。如果只有一个,则在下一个变量之前会填充空间,因为大多数ABIs将基本类型(如int)与其自身宽度(自然对齐)对齐。仅在栈上传递单字节对象时才会将其填充到“堆栈宽度”或插槽(单个push指令的大小)。 - Peter Cordes

2
据我所知,编译器通常不会对堆栈上的变量进行对齐。该库可能依赖于一些编译器选项,在您的编译器上不受支持。正常的解决方法是将需要对齐的变量声明为静态变量,但如果您在其他人的代码中这样做,您需要确保这些变量在函数中稍后被初始化而不是在声明中。
// Some compilers won't align this as it's on the stack...
int __declspec(align(32)) needsToBe32Aligned = 0;
// Change to
static int __declspec(align(32)) needsToBe32Aligned;
needsToBe32Aligned = 0;

或者,找到一个编译器开关来对齐堆栈上的变量。显然,我在这里使用的“__declspec”对齐语法可能不是您的编译器使用的。


1
编译器确实会将变量在栈上按照ABI中指定类型的对齐保证/要求进行对齐。通常情况下,这意味着自然对齐:对齐=宽度,因此4字节的“int”获得4字节对齐。通过始终保持堆栈指针本身的16字节对齐,可以在堆栈上以16、8、4或2对齐变量而无需额外成本。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接