【问题标题】:Memory addressing and stack alignment - have I understood correctly?内存寻址和堆栈对齐 - 我理解正确吗?
【发布时间】:2015-11-15 15:33:23
【问题描述】:

我正在尝试更深入地研究内存分配、寻址,并且我还遇到了堆栈对齐的概念,通常是内存对齐。我想了解我是否正确掌握了所有概念。我的问题都涉及当今的计算机和处理器,例如我们笔记本电脑上的处理器。我想强调一下,我阅读了很多关于 stackoverflow 的其他问题,而我的大部分实际知识都来自它们。

我的第一个疑问与记忆词的概念有关。内存字不仅定义了寄存器和总线大小,还定义了基本的内存单元(例如,64 位架构上的 64 位,32 位架构上的 32 位 ecc)。但是,据我所知,每个地址都准确引用 1 字节 内存,而不管内存字的大小。所以我们可以说每个字节都有自己的地址。但是:

1) CPU 不能访问单个字节但它访问包含该字节的整个字是否正确?因此,如果请求特定字节(例如,我访问 char 的直接地址),它会访问整个单词并进行一些计算以删除其他部分并返回确切的字节?

2) 那么 CPU 实际上只能访问内存字单元并且每个内存字从偶数地址开始,是单元本身的倍数,这是否正确?

因此,例如,在 64 位架构上,内存字为 8 个字节,因此(示例)地址 0x2710(以 10 为基数的 10000)将是内存字的开头。如果我尝试访问 0x2711,CPU 将从 0x2710 访问到 0x2717,然后只提取单个字节。对吗?!

第二。正如我之前所说,我遇到了内存对齐。一开始它让我有些困惑。请帮助我理解我是否做对了。该问题本质上与性能有关,或者在某些情况下,与需要 16 字节对齐的 SSE 特定指令有关。例如,在第一种情况下,如果(在 64 位架构上)跨 2 个内存字存储 8 字节数据(例如,一个 long int),则 CPU 需要 2 次访问而不是一次访问。

3) 所以,举个例子:

0x2710 | .... |
0x2711 | .... |
0x2712 | .... |
0x2713 | .... |
0x2714 | data |
0x2715 | data |
0x2716 | data |
0x2717 | data |
0x2718 | data |
0x2719 | data |
0x2720 | data |
0x2721 | data |
0x2722 | .... |
0x2723 | .... |
0x2724 | .... |
0x2725 | .... |

在这种情况下,内存未对齐。正确的?通过对齐,CPU 将仅存储来自 0x2710 的数据,或者,如果占用到 0x2713,它将插入填充,然后存储来自 0x2718 的 8 字节数据。对吧?

4) 因此,内存对齐基本上包括仅从地址开始的多字节数据,这些数据是所需字节单元的倍数(通常是内存字本身,还有其他自定义单元 - 例如,在 GCC 上使用 mpreferred-stack-boundaries)。我说“多字节”数据是因为如果数据只有一个字节,它总是只适合一个单词。这一切都正确吗?

5) 编译器应用内存对齐?因此,它是在二进制(汇编)代码中应用还是在存储数据时由 CPU 以某种方式应用? 而且,这不是很大的内存浪费吗?我的意思是,如果总是应用它,每个多字节数据也可能意味着填充!我可能会浪费大量内存空间!

就是这样!谢谢,真的提前谢谢!

【问题讨论】:

  • 我开始写回复,但一半的答案是“取决于 CPU”。 en.wikipedia.org/wiki/Data_structure_alignment 似乎很好地解释了其余部分。
  • 啊哈哈哈谢谢!我已经阅读了维基百科页面。我知道它非常依赖 CPU,我只是想了解,一般来说,我写的内容是否适用于现代处理器。

标签: c memory-management stack memory-address memory-alignment


【解决方案1】:

数据对齐是一种硬件架构优化,是特定于 CPU 的。一次提取从内存中提取 8 个字节可以更快更简单,然后丢弃额外的数据并在 CPU 内部移动数据。它还可以通过忽略地址的低 3 位(0-7)来使数据总线更简单,节省 CPU/MMU 和内存总线之间的 3 条信号线。更少的数据总线意味着 PCB 上的信号布线更容易,RF 噪声更少。

但是,如果 CPU 使用 8 字节对齐并将 8 字节值存储在未对齐的地址中,则现在访问该值需要 2 次取指,从而导致执行性能不佳。如果程序员/编译器知道数据对齐,他/它可以安排数据以避免双重获取。这可能会浪费一些内存来节省 CPU 周期,如果内存便宜而时间不便宜,这很好。或者,如果内存不便宜,程序员可以使用#pragma pack(1) 覆盖默认数据对齐,这会告诉编译器忽略数据对齐。

堆栈通常是对齐的,以便使用通用指令更容易推送和弹出。在这种情况下,它用于以浪费少量内存为代价让生活更简单。

3) CPU 不决定将数据存储在哪里,程序员/编译器(有时是操作系统)做出决定。 CPU 完全有能力从任何地址读取任何大小的数据,但不一定在单个操作中。对齐不佳的数据将需要更多的获取和更多的时间。一些 CPU 会因未对齐的操作(摩托罗拉 68000 和许多低成本微控制器)而出错,但大多数带有 MMU 的 CPU 会在内部处理它。

4) 不完全是。重要的是多字节数据不跨越对齐边界。在 8 字节与 2 字节值对齐的情况下,该值可以存储在地址 0x1000、0x1001、0x1002、0x1003、0x1004、0x1005、0x1006 处,而无需多次提取。仅将其存储在 0x1007 会导致问题,因为 CPU 需要获取 0x1000[..0x1007] 和 0x1008[..0x100F] 才能读取整个值。

5) 是的,可能会浪费一些内存,但不会浪费太多。当您只需要 1 时读取 8 个字节不会影响性能。如果您的代码有八个 char 值,编译器将排列它们,使它们都在同一个 8 字节字中。这样不会浪费空间,也不会影响性能。

【讨论】:

  • 您好,谢谢!你让我有点困惑! :) 1) 如果我理解,数据对齐使 CPU 性能更高,但由程序员/编译器正确“存储”数据(=对齐)。 2)你写了“CPU完全有能力从任何地址读取任何大小的数据”,但我在CPU只能寻址内存字的所有地方都读到了。它可以读取单个字节,但首先传输整个字然后提取字节,你是这个意思吗? 3) 在 4) 上,您说仅将其存储在 0x1007 会导致问题。我不明白为什么:从 0x1000 到 0x1007 仍然是同一个词。
  • [CONTINUE] 我的意思是第一个词从 0x1000 到 0x1007,第二个词从 0x1008 到 0x100e。因此,如果 8 字节数据存储在从 0x1000 到 0x1007(包括在内)中,它们仍然在一个字中(=> 一次访问)。但是,如果我什至从 0x1001 开始存储它们,则需要两次访问。不?我错过了什么吗?!最后,我的其他观点是否正确?我的意思是 - 每个地址 = 内存中的 1 个字节 - CPU 可以直接访问而无需进一步操作,只需一个内存字
  • 是的,跨对齐边界写入值需要两次内存访问。这与说 CPU 无法在一次 fetch 中读取值相同,但没有暗示 CPU 根本无法读取该值。这是一个微妙但重要的一点:跨对齐边界存储值是完全有效的,但是会降低性能。这就是为什么你应该意识到它。
  • 好的,我知道了。我唯一不明白的是,为什么在你的例子中,只从 0x1007 存储它会导致问题,而不是在所有不同于 0x1000 的以前的地址中。
  • 一个多月后,我再次阅读了您的答案,我看到您写的是“使用 2 字节值”。我之前错过了。所以现在很清楚为什么只将它存储在 0x1007 会是一个问题:)
【解决方案2】:

每个平台都有一组称为 ABI 或应用程序二进制接口的约定。它们通常记录在平台开发人员提供的文档中。这些约定涵盖了许多主题,对齐规则就是其中之一。在给定的硬件架构上可能存在多个平台;一个例子是 x64,其中有两个主要的 ABI,Microsoft ABI(在 Windows 上使用)和 System V ABI(在 Linux 上使用)。

对齐规则通常由硬件决定。例如,某些硬件架构显然无法在 CPU 内核和内存之间传输未对齐的数据。一些硬件架构虽然能够这样做,但每次这样的传输都会导致性能损失。

为了生成符合目标平台 ABI 的程序,编译器工具链与操作系统配合。例如,操作系统保证可执行文件部分应始终加载到满足 ABI 规定的最严格对齐要求的地址。当链接器生成包含对齐对象的部分时,它依赖于它。 C 编译器应使用对齐要求注释目标文件中的部分,以便链接器在从多个编译单元组成单个文件时可以使用该信息对内容进行相应的布局。

在堆栈方面,可能存在不同的策略。在某些平台上,总是要求函数以最严格的对齐要求的倍数吃掉堆栈。如果编译器可能依赖它,它将相应地布置函数的堆栈帧。

但是,在某些平台上,堆栈对齐要求并不那么严格。例如,一个 SSE 数据类型以 32 字节对齐,但感觉对于 every 函数来说,以 32 字节的倍数吃完这个要求太奢侈了:该类型相对很少使用。这意味着,在编译将__m256 放在堆栈上的函数时,编译器通常可能不依赖于堆栈在函数启动时足够对齐。然后编译器将在 prolog 中插入一个代码来检查它是否是,如果不是,则另外增加堆栈。显然,这是一种权衡:如果您需要更严格的对齐,您的程序开始浪费太多的堆栈空间,如果要求过于宽松,编译器将需要发布对齐代码,这会夸大代码并影响性能。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-04
    • 1970-01-01
    • 2021-03-03
    • 2021-05-19
    • 2018-07-18
    • 2011-06-16
    • 2018-09-27
    相关资源
    最近更新 更多