对齐究竟如何影响内存布局和新布局的行为?
How exactly does alignment impact memory layout and the bahaviour of placement new?
我们阅读了很多关于对齐及其重要性的文章,例如放置 new
用法,但我想知道 - 它究竟如何改变内存布局?
显然,如果我们这样做
char buffer[10];
std::cout << sizeof buffer;
和
alignas(int) char buffer[10];
std::cout << sizeof buffer;
我们得到相同的结果,即10
。
但是行为不可能完全一样,是吗?怎么区分呢?我试图寻求答案和 运行 godbolt,测试以下代码:
#include <memory>
int main() {
alignas(int) char buffer[10];
new (buffer) int;
}
在 GCC 8.2 下并且没有优化,导致以下汇编:
operator new(unsigned long, void*):
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov QWORD PTR [rbp-16], rsi
mov rax, QWORD PTR [rbp-16]
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
lea rax, [rbp-12]
mov rsi, rax
mov edi, 4
call operator new(unsigned long, void*)
mov eax, 0
leave
ret
让我们通过删除 alignas(int)
部分来稍微更改代码。现在,生成的程序集略有不同:
operator new(unsigned long, void*):
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov QWORD PTR [rbp-16], rsi
mov rax, QWORD PTR [rbp-16]
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
lea rax, [rbp-10]
mov rsi, rax
mov edi, 4
call operator new(unsigned long, void*)
mov eax, 0
leave
ret
值得注意的是,它仅在 lea
指令上有所不同,其中第二个参数是 [rbp-10]
而不是我们在 alignas(int)
版本中使用的 [rbp-12]
。
请注意,我通常不懂汇编。我不会写汇编,但我能读懂它。据我了解,差异只是改变了内存地址的偏移量,它将保存我们的位置-new
ed int
.
但是它实现了什么?为什么我们需要那个?假设我们有一个 buffer
数组的 'generic' 表示如下:
[ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ]
现在,我假设,在放置 new
ing int
(有或没有对齐)之后,我们最终会得到像这样:
[x] [x] [x] [x] [ ] [ ] [ ] [ ] [ ] [ ]
其中 x
表示 int
的单个字节(我们假设 sizeof(int) == 4
)。
但我一定是漏掉了什么。还有更多,我不知道是什么。通过将 buffer
对齐到 int
同花对齐,我们到底实现了什么?如果我们不这样对齐会怎样?
在某些体系结构上,类型必须对齐才能使操作正常进行。例如,int
的地址可能需要是 4 的倍数。如果它不是这样对齐的,那么对内存中的整数进行操作的 CPU 指令将无法工作。
即使数据未很好对齐时一切工作,对齐对于性能仍然很重要,因为它确保整数等不会跨越缓存边界。
当您将 char
缓冲区与整数边界对齐时,它不会影响 placement new 的工作方式。它只是确保您可以使用 placement new 将 int
放在缓冲区的开头,而不会违反任何对齐约束。它通过确保缓冲区的地址是 4 字节的倍数来实现。
我们阅读了很多关于对齐及其重要性的文章,例如放置 new
用法,但我想知道 - 它究竟如何改变内存布局?
显然,如果我们这样做
char buffer[10];
std::cout << sizeof buffer;
和
alignas(int) char buffer[10];
std::cout << sizeof buffer;
我们得到相同的结果,即10
。
但是行为不可能完全一样,是吗?怎么区分呢?我试图寻求答案和 运行 godbolt,测试以下代码:
#include <memory>
int main() {
alignas(int) char buffer[10];
new (buffer) int;
}
在 GCC 8.2 下并且没有优化,导致以下汇编:
operator new(unsigned long, void*):
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov QWORD PTR [rbp-16], rsi
mov rax, QWORD PTR [rbp-16]
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
lea rax, [rbp-12]
mov rsi, rax
mov edi, 4
call operator new(unsigned long, void*)
mov eax, 0
leave
ret
让我们通过删除 alignas(int)
部分来稍微更改代码。现在,生成的程序集略有不同:
operator new(unsigned long, void*):
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov QWORD PTR [rbp-16], rsi
mov rax, QWORD PTR [rbp-16]
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
lea rax, [rbp-10]
mov rsi, rax
mov edi, 4
call operator new(unsigned long, void*)
mov eax, 0
leave
ret
值得注意的是,它仅在 lea
指令上有所不同,其中第二个参数是 [rbp-10]
而不是我们在 alignas(int)
版本中使用的 [rbp-12]
。
请注意,我通常不懂汇编。我不会写汇编,但我能读懂它。据我了解,差异只是改变了内存地址的偏移量,它将保存我们的位置-new
ed int
.
但是它实现了什么?为什么我们需要那个?假设我们有一个 buffer
数组的 'generic' 表示如下:
[ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ]
现在,我假设,在放置 new
ing int
(有或没有对齐)之后,我们最终会得到像这样:
[x] [x] [x] [x] [ ] [ ] [ ] [ ] [ ] [ ]
其中 x
表示 int
的单个字节(我们假设 sizeof(int) == 4
)。
但我一定是漏掉了什么。还有更多,我不知道是什么。通过将 buffer
对齐到 int
同花对齐,我们到底实现了什么?如果我们不这样对齐会怎样?
在某些体系结构上,类型必须对齐才能使操作正常进行。例如,int
的地址可能需要是 4 的倍数。如果它不是这样对齐的,那么对内存中的整数进行操作的 CPU 指令将无法工作。
即使数据未很好对齐时一切工作,对齐对于性能仍然很重要,因为它确保整数等不会跨越缓存边界。
当您将 char
缓冲区与整数边界对齐时,它不会影响 placement new 的工作方式。它只是确保您可以使用 placement new 将 int
放在缓冲区的开头,而不会违反任何对齐约束。它通过确保缓冲区的地址是 4 字节的倍数来实现。