7

我试图了解未对齐内存访问 (UMA) 如何在现代处理器(即 x86-64 和 ARM 架构)上工作。我知道我可能会遇到 UMA 的问题,从性能下降到 CPU 故障。我阅读posix_memalign并缓存行。

当我的请求超出页面边界时,我找不到现代系统/硬件如何处理这种情况?

这是一个例子:

  1. malloc()有一块 8KB 的内存。
  2. 假设我malloc()没有足够的内存和sbrk()8KB。
  3. 内核获取两个内存页(每个4KB)并将它们映射到我进程的虚拟地址空间(假设这两个页在内存中不是一个接一个)
  4. movq (offset + $0xffc), %rax我从第 4092 个字节开始请求 8 个字节,这意味着我想要从第一页末尾开始的 4 个字节和从第二页开头开始的 4 个字节。

物理内存:

---|---------------|---------------|-->
   |... 4b|        |        |4b ...|-->

我需要在页面边界处拆分的 8 个字节。

x86-64 和 ARM 上的 MMU 如何处理这个问题?内核 MM 中是否有任何机制可以以某种方式为这种请求做准备?有什么保护措施malloc吗?处理器是做什么的?他们获取两页吗?

我的意思是要完成这样的请求,MMU 必须将一个虚拟地址转换为两个物理地址。它如何处理这样的请求?

如果我是一名软件程序员,我应该关心这些事情吗?为什么?

我目前正在阅读来自 google、SO、drepper 的 cpumemory.pdf 和 gorman 的 Linux VMM 书的大量链接。但它是信息的海洋。如果您至少向我提供一些我可以使用的指针或关键字,那就太好了。

4

3 回答 3

5

我对英特尔架构的内容并不太熟悉,但是 ARM 架构在“未对齐的数据访问限制”下的一个要点中总结了这个特定的细节:

  • 执行未对齐访问的操作可以在它进行的任何内存访问时中止,并且可以在多次访问时中止。这意味着跨页边界发生的未对齐访问可能会在边界的任一侧产生中止。

因此,除了可能从单个操作中产生两个页面错误之外,它只是另一个未对齐的访问。当然,这仍然假设“只是另一个未对齐访问”的所有警告 - 即它仅在普通(非设备)内存上有效,仅适用于某些加载/存储指令,不能保证原子性并且可能很慢 - 微架构将可能从多个对齐的访问中合成一个未对齐的访问1,这意味着多个 MMU 转换,如果它跨越行边界,可能会出现多个缓存未命中等。

换个角度看,如果未对齐的访问没有跨越页面边界,这意味着如果第一个“子访问”的对齐地址转换为 OK,那么任何后续部分的对齐地址肯定会命中在 TLB 中。MMU 本身并不关心——它只是翻译处理器给它的一些地址。除非 MMU 引发页面错误,否则内核甚至不会出现,即使这样,它也与任何其他页面错误没有什么不同。

我已经快速浏览了英特尔手册,他们的答案并没有让我大吃一惊——但是在“数据类型”一章中,他们确实指出:

[...] 处理器需要两次内存访问才能进行非对齐访问;对齐访问只需要一次内存访问。

所以如果不是大致相同(即每个对齐访问一个翻译),我会感到惊讶。

现在,这是大多数应用程序级程序员不应该担心的事情,只要他们表现自己 - 在汇编语言之外,实际上很难发生未对齐的访问。可能的罪魁祸首是类型双关语指针和搞乱结构包装,这两件事在 99% 的情况下都没有理由接近,而另外 1% 的情况仍然几乎可以肯定是错误的事情。


[1] ARM 架构伪代码实际上将非对齐访问指定为一系列单独的字节访问,但我希望实现实际上将其优化为适当的更大对齐访问。

于 2014-05-11T18:41:56.683 回答
2

所以架构并不重要,除了 x86 传统上不会直接告诉你 mips 和 arm 传统上在哪里生成数据中止,而不是试图让它工作。

无关紧要的是所有处理器都有固定数量的引脚固定大小(最大)数据总线固定大小(最大)地址总线,“现代处理器”往往具有超过 8 位宽的数据总线,但地址上的单元仍然是一个 8 位字节,因此存在未对齐的机会。如果架构允许,在特定传输中大于一个字节的任何内容都有可能不对齐。

传输通常以某些字节和/或总线宽度为单位。例如,在 ARM amba/axi 总线上,长度字段以总线宽度、32 或 64 位、4 或 8 字节为单位。不,它不会以 4Kbytes 为单位......

(是的,这是基本的,我假设您了解所有这些)。

无论是 16 位还是 128 位,未对齐的惩罚来自额外的总线周期,如今每个额外的总线时钟。因此,对于 ARM 16 位未对齐传输(哪个 arm 将在其较新的内核上支持而不会出现故障),这意味着您需要读取 128 位而不是 64 位,64 位以获得 16 位不是惩罚,因为 64 位是总线的最小尺寸转移。每次传输,无论是数据总线的单个宽度还是多个数据总线都有多个与之相关的时钟周期,假设有 6 个时钟周期来执行对齐的 16 位读取,那么理想情况下,执行未对齐的 16 位需要 7 个周期。看起来很小,但它确实加起来。

缓存有很大帮助,因为缓存的 dram 端将设置为使用总线宽度的倍数,并且将始终对缓存提取和驱逐进行对齐访问。非缓存访问将遵循同样的痛苦,除了 dram 端不是几个时钟而是几十到几百个时钟的开销。

对于随机访问,单个 16 位读取不仅跨越总线宽度边界而且恰好跨越缓存线边界,这不仅会在处理器端产生一个额外的时钟,而且在最坏的情况下,它可能会导致额外的缓存线提取,即几十到几百个额外的时钟周期。如果您正在遍历一系列碰巧未对齐的事物(结构/联合可能是一个示例,具体取决于编译器和代码),那么无论如何都会发生额外的缓存行提取,如果事物数组有点过时一端或两端,那么如果阵列对齐,您可能仍然会产生一两个更多的缓存行提取,而这些缓存行是您可以避免的。

这真的是读取的关键是在对齐区域之前或之后,您可能必须为您溢出的每一侧的每个区域进行转移。

写作有好有坏。随机读取速度较慢,因为事务必须停止,直到答案回来。对于随机写入,内存控制器拥有它需要的所有信息,包括地址、数据、字节掩码、传输类型等。所以它很容易忘记处理器已经完成它的工作,并且可以从它的角度调用事务完成,并且继续前行。自然而然地把太多的这些组合起来,或者对刚刚写入的东西进行读取,然后由于除了当前事务之外的先前写入的完成,处理器会停止。

例如,未对齐的 16 位写入不仅会产生额外的读取周期,而且假设 32 或 64 位宽的总线每个位置一个字节,因此您必须对最近的内存(缓存或德拉姆)。因此,取决于处理器和内存控制器如何实现它,它可以是两个单独的读取-修改-写入事务(不太可能,因为这会产生两倍的开销),或者是双倍宽度读取、修改这两个部分和双倍宽度读取。在开销之上产生两个额外的时钟,开销也加倍。如果它是对齐的总线宽度写入,则不需要读取-修改-写入,则保存读取。

我也最熟悉ARM。Arm 传统上会通过中止来惩罚未对齐的访问,您可以将其关闭,并且您将获得总线的轮换而不是溢出,这将产生一些不错的免费赠品字节序交换。更现代的手臂核心将容忍并实施未对齐的传输。了解例如,针对非 64 位对齐地址存储多个 4 个或更多寄存器不被视为未对齐访问,即使它是对既不是 64 位也不是 128 位对齐的地址的 128 位写入。在这种情况下,处理器所做的是将其分为 3 次写入、对齐的 32 位写入、对齐的 64 位写入和对齐的 32 位写入。内存控制器不必处理未对齐的东西。这适用于诸如存储多个合法的事情。无论如何,我熟悉的核心不会执行超过 2 的写入长度,一个 8 寄存器存储多个,不是 4 写入的单个长度,它是 2 单独的两次写入长度。但是加载 8 个寄存器的倍数,只要它在 64 位地址上对齐,就是单个长度为 4 的事务。我很确定,由于总线端没有用于读取的屏蔽,所以一切都以总线宽度为单位,没有理由在一个不是 64 位对齐到 3 个事务中的地址上打破 4 个寄存器加载倍数,只需读取 3 的长度。当处理器读取单个字节时,您无法从总线上看出您所看到的只是 64 位读取的 AFAIK。处理器剥离字节通道。如果处理器/总线确实关心它是 arm、x86、mips 等,那么您肯定会看到单独的传输。

每个人都这样做吗?没有较旧的处理器(不考虑 arm 或 x86)会给内存控制器带来更多负担。我不知道现代 x86 和 mips 等是做什么的。

您的 malloc 示例。首先,您不会看到 4Kbytes 的单总线传输,无论如何,4k 将被分解成可消化的位。首先,它必须对内存管理单元执行一对多的总线周期才能找到物理地址和其他属性(这些答案可以被缓存以使它们更快,但有时它们必须一路走慢 DRAM)因此,对于该示例,唯一重要的传输是分割 4k 边界的对齐传输,例如 16 位传输,以便 mmu 系统完全工作,唯一支持的方法是必须将其转换为两个独立的在这些物理地址空间中发生的 8 位传输,是的,这实际上使 mmu 查找周期、缓存/DRAM 总线周期等的所有内容加倍。除了那个边界之外,你的 8k 被分割并没有什么特别之处。您的大部分周期将在两个 4k 页面之一内,因此它看起来像任何其他随机访问,当然重复/顺序访问可以获得缓存的好处。

简短的回答是,无论您在哪个平台上,1) 平台都会中止未对齐的传输,或者 2) 在路径中的某处,由于比较的未对齐访问,路径中的某个位置会增加一个或多个(数十个/数百个)到对齐的访问。

于 2014-05-11T19:56:51.267 回答
0

物理页面是否相邻并不重要。现代 CPU 使用缓存。一次向/从 DRAM 传输一个完整的高速缓存行的数据。因此,DRAM 永远不会看到跨越 64B 边界的多字节读取或写入,更不用说页面边界了。

跨越页面边界的存储仍然很慢(在现代 x86 上)。我假设硬件通过在稍后的管道阶段检测到它来处理页面拆分情况,并触发执行两次 TLB 检查的重做。IDK 如果英特尔设计在管道中插入额外的微指令来处理它,或者什么。(即对延迟、分页吞吐量、所有内存访问的吞吐量、其他(例如非内存)微指令的吞吐量的影响)。

通常情况下,对于缓存行中的未对齐访问(从 Nehalem 开始)根本没有任何惩罚,对于不是页面拆分的缓存行拆分也有一点惩罚。平均分配显然比其他分配便宜。(例如,一个 16B 的负载,从一个高速缓存行获取 8B,从另一个高速缓存行获取 8B)。

无论如何,DRAM 永远不会直接看到未对齐的访问。AFAIK,没有理智的现代设计只有直写缓存,因此 DRAM 仅在刷新缓存线时才看到写入,此时一个未对齐的访问会弄脏两个缓存线的事实不可用。缓存甚至不记录哪些字节是脏的;他们只是在需要时将整个 64B 突发写入到下一层(或最后一层到 DRAM)。

可能有一些 CPU 设计不是这样工作的,但 Intel 和 AMD 的设计也是这样。


警告:加载/存储到不可缓存的内存区域可能会产生较小的存储,但可能仍仅在单个缓存行内。(在 x86 上,此问题适用于 MOVNT 非临时存储,它们使用写入组合存储缓冲区但以其他方式绕过缓存)。

跨越页面边界的不可缓存的未对齐存储可能仍被拆分为单独的存储(因为每个部分都需要单独的 TLB 转换)。


警告 2:我没有对此进行事实核查。不过,我确信对于“正常”加载/存储到“正常”内存区域的 DRAM 的整个缓存行对齐访问。

于 2016-02-22T10:39:50.820 回答