【问题标题】:What Every Programmer Should Know About Memory?每个程序员都应该知道关于内存的什么?
【发布时间】:2011-12-28 21:43:14
【问题描述】:

我想知道 Ulrich Drepper 的 2007 年 What Every Programmer Should Know About Memory 有多少仍然有效。我也找不到比 1.0 更新的版本或勘误表。

(Ulrich Drepper 自己网站上的 PDF 格式:https://www.akkadia.org/drepper/cpumemory.pdf

【问题讨论】:

  • 有人知道我是否可以将这篇文章以 mobi 格式下载到某个地方,以便在 kindle 上轻松阅读?由于缩放/格式问题,“pdf”很难阅读
  • 这不是手机,但 LWN 将报纸作为一组更易于在手机/平板电脑上阅读的文章运行。第一个是lwn.net/Articles/250967

标签: optimization memory x86 cpu-architecture cpu-cache


【解决方案1】:

据我所知,Drepper 的内容描述了有关内存的基本概念:CPU 缓存如何工作、什么是物理和虚拟内存以及 Linux 内核如何处理该动物园。在某些示例中可能存在过时的 API 引用,但没关系;这不会影响基本概念的相关性。

因此,任何描述基本事物的书籍或文章都不能称为过时。 《每个程序员应该知道的关于内存的知识》绝对值得一读,但是,我不认为它适合“每个程序员”。它更适合系统/嵌入式/内核的家伙。

【讨论】:

  • 是的,我真的不明白为什么程序员需要知道 SRAM 和 DRAM 在模拟级别上的工作原理——这在编写程序时没有多大帮助。真正需要这些知识的人,最好花时间阅读有关实际时间等细节的手册。但是对于那些对硬件低级别的东西感兴趣的人呢?也许没用,但至少很有趣。
  • 如今的性能 == 内存性能,因此了解内存是任何高性能应用程序中最重要的事情。这使得这篇论文对于参与以下领域的任何人都是必不可少的:游戏开发、科学计算、金融、数据库、编译器、大型数据集的处理、可视化、任何必须处理大量请求的东西......所以是的,如果你在一个应用程序中工作大部分时间都是闲置的,就像一个文本编辑器,除非你需要快速做一些事情,比如找一个单词、数单词、拼写检查……哦等等……没关系。
【解决方案2】:

从我的快速浏览来看,它看起来非常准确。需要注意的一件事是“集成”和“外部”内存控制器之间的差异部分。自从 i7 系列发布以来,Intel CPU 都是集成的,而 AMD 自 AMD64 芯片首次发布以来一直使用集成内存控制器。

自从撰写本文以来,并没有太大变化,速度提高了,内存控制器变得更加智能(i7 将延迟写入 RAM,直到感觉要提交更改),但不是一个整体很多已经改变了。至少不是软件开发人员会关心的任何方式。

【讨论】:

  • 我很想接受你们俩。但我赞成你的帖子。
  • 可能与 SW 开发人员相关的最重大变化是预取线程是一个坏主意。 CPU 足够强大,可以通过超线程运行 2 个完整线程,并且具有更好的硬件预取。 SW 预取通常是 很多 不太重要,特别是对于顺序访问。看我的回答。
【解决方案3】:

PDF 格式的指南位于https://www.akkadia.org/drepper/cpumemory.pdf

它仍然非常出色且强烈推荐(由我推荐,我认为由其他性能调整专家推荐)。如果 Ulrich(或其他任何人)编写 2017 年更新会很酷,但这将是很多工作(例如重新运行基准测试)。另请参阅tag wiki 中的其他 x86 性能调整和 SSE/asm(和 C/C++)优化链接。 (Ulrich 的文章不是针对 x86 的,但他的大多数(所有)基准测试都是在 x86 硬件上进行的。)

关于 DRAM 和缓存如何工作的底层硬件细节仍然适用。 DDR4 使用the same commands,如 DDR1/DDR2(读/写突发)所述。 DDR3/4 的改进并不是根本性的变化。 AFAIK,所有独立于架构的东西仍然普遍适用,例如到 AArch64 / ARM32。

有关内存/L3 延迟对单线程带宽影响的重要详细信息,另请参阅 the Latency Bound Platforms section of this answerbandwidth <= max_concurrency / latency,这实际上是现代多核 CPU 上单线程带宽的主要瓶颈,例如至强。但是四核 Skylake 桌面可以通过单线程接近最大化 DRAM 带宽。该链接有一些关于 NT 商店与 x86 上的普通商店的非常好的信息。 Why is Skylake so much better than Broadwell-E for single-threaded memory throughput? 是一个总结。

因此,Ulrich 在 6.5.8 使用所有带宽 中关于在其他 NUMA 节点以及您自己的节点上使用远程内存的建议在现代硬件上适得其反,因为现代硬件的内存控制器比单个节点的带宽更多核心可以使用。很可能您可以想象这样一种情况:在同一个 NUMA 节点上运行多个需要大量内存的线程以实现低延迟的线程间通信,但让它们使用远程内存来处理高带宽、对延迟不敏感的东西。但这很模糊,通常只是在 NUMA 节点之间划分线程并让它们使用本地内存。由于最大并发限制(见下文),每核带宽对延迟很敏感,但一个插槽中的所有核心通常会使该插槽中的内存控制器饱和。


(通常)不要使用软件预取

一个主要的变化是硬件预取比 Pentium 4 上的好很多,并且可以识别跨步访问模式,步幅相当大,并且可以识别多个流一次(例如,每 4k 页一个前进/后退)。 Intel's optimization manual 描述了其 Sandybridge 系列微架构的各个级别缓存中的硬件预取器的一些细节。 Ivybridge 和更高版本具有下一页硬件预取,而不是等待新页面中的缓存未命中来触发快速启动。我认为 AMD 在他们的优化手册中有一些类似的东西。请注意,英特尔的手册也充满了旧的建议,其中一些只对 P4 有用。特定于 Sandybridge 的部分当然对于 SnB 是准确的,但是例如un-lamination of micro-fused uops changed in HSW and the manual doesn't mention it.

这些天通常的建议是从旧代码中删除所有 SW 预取,并且仅在分析显示缓存未命中(并且您没有使内存带宽饱和)时才考虑将其放回原处。在二分搜索的 next 步骤的两侧预取仍然会有所帮助。例如一旦您决定接下来要查看哪个元素,请预取 1/4 和 3/4 元素,以便它们可以与加载/检查中间并行加载。

我认为,使用单独的预取线程 (6.3.4) 的建议已经完全过时了,而且只在 Pentium 4 上表现出色。P4 具有超线程(2 个逻辑内核共享一个物理内核) ),但没有足够的跟踪缓存(和/或乱序执行资源)来获得在同一核心上运行两个完整计算线程的吞吐量。但是现代 CPU(Sandybridge 系列和 Ryzen)更加更强大,应该运行真正的线程或不使用超线程(让其他逻辑核心空闲,以便单独线程拥有全部资源,而不是对罗布)。

软件预取一直是“脆弱的”:获得加速的正确魔法调整数取决于硬件的细节,也可能是系统负载。太早了,它在需求负载之前就被驱逐了。为时已晚,也无济于事。 This blog article 显示了在 Haswell 上使用 SW 预取来预取问题的非顺序部分的有趣实验的代码 + 图表。另请参阅How to properly use prefetch instructions?。 NT 预取很有趣,但更脆弱,因为从 L1 的早期驱逐意味着您必须一直到 L3 或 DRAM,而不仅仅是 L2。如果您需要每一次性能下降,并且您可以针对特定的机器进行调整,SW 预取值得关注以进行顺序访问,但它可能仍然会放缓,如果你有足够的 ALU 工作要做,同时接近内存瓶颈。


缓存行大小仍为 64 字节。 (L1D 读/写带宽非常高,现代 CPU 每个时钟可以执行 2 个向量加载 + 1 个向量存储,如果它全部在 L1D 中命中。请参阅How can cache be that fast?。)使用 AVX512,行大小 =矢量宽度,因此您可以在一条指令中加载/存储整个缓存行。因此,对于 256b AVX1/AVX2,每个未对齐的加载/存储都会跨越一个缓存线边界,而不是每隔一个,这通常不会减慢循环遍历不在 L1D 中的数组的速度。

如果地址在运行时对齐,未对齐的加载指令的惩罚为零,但如果编译器(尤其是 gcc)知道任何对齐保证,则在自动向量化时会生成更好的代码。实际上,未对齐的操作通常很快,但页面拆分仍然会受到伤害(不过在 Skylake 上要少得多;与 100 相比,只有大约 11 个额外的周期延迟,但仍然是吞吐量损失)。


正如 Ulrich 预测的那样,如今每个多插槽系统都是 NUMA:集成内存控制器是标准配置,即没有外部北桥。但 SMP 不再意味着多插槽,因为多核 CPU 很普遍。从 Nehalem 到 Skylake 的 Intel CPU 都使用大型 inclusive L3 缓存作为内核之间一致性的支持。 AMD CPU 是不同的,但我不太清楚细节。

Skylake-X (AVX512) 不再具有包容性 L3,但我认为仍然有一个标签目录,可以让它检查芯片上任何地方缓存的内容(如果有的话),而无需实际向所有内核广播窥探。 SKX uses a mesh rather than a ring bus,不幸的是,延迟通常比以前的多核 Xeon 还要糟糕。

基本上所有关于优化内存放置的建议仍然适用,只是当您无法避免缓存未命中或争用时发生的具体情况各不相同。


6.4.2 原子操作:基准测试显示 CAS 重试循环比硬件仲裁的lock add 差 4 倍,这可能仍然反映了最大争用情况。但是在真正的多线程程序中,同步保持在最低限度(因为它很昂贵),所以争用很少,并且 CAS 重试循环通常会成功而无需重试。

C++11 std::atomic fetch_add 将编译为lock add(或lock xadd,如果使用了返回值),但是使用CAS的算法做一些不能用@做的事情987654358@ed 指令通常不是灾难。使用 C++11 std::atomic 或 C11 stdatomic 而不是 gcc 旧版 __sync built-ins 或更新的 __atomic built-ins 除非您想混合对同一位置的原子和非原子访问...

8.1 DWCAS (cmpxchg16b):你可以诱使 gcc 发射它,但如果你想要有效加载一半的对象,你需要丑陋的 union hacks: How can I implement ABA counter with c++11 CAS?。 (不要将 DWCAS 与 DCAS of 2 separate memory locations 混淆。DWCAS 无法对 DCAS 进行无锁原子模拟,但事务性内存(如 x86 TSX)使之成为可能。)

8.2.4 事务内存:经过几次错误启动(由于很少触发的错误而被微码更新释放然后禁用),英特尔在最新型号的 Broadwell 和所有Skylake CPU。设计仍然是what David Kanter described for Haswell。有一种锁省略方法可以使用它来加速使用(并且可以回退到)常规锁的代码(尤其是对容器的所有元素使用单个锁,因此同一关键部分中的多个线程通常不会发生冲突),或者编写直接了解事务的代码。

更新:现在英特尔通过微码更新禁用了后续 CPU(包括 Skylake)上的锁定省略。如果操作系统允许,TSX 的 RTM (xbegin / xend) 非透明部分仍然可以工作,但 TSX 总体上正在严重变成Charlie Brown's football


7.5 Hugepages:匿名透明的hugepages 在Linux 上运行良好,无需手动使用hugetlbfs。使用 2MiB 对齐进行分配 >= 2MiB(例如,posix_memalign, or an aligned_alloc 不会强制愚蠢的 ISO C++17 要求在 size % alignment != 0 时失败)。

默认情况下,2MiB 对齐的匿名分配将使用大页面。一些工作负载(例如,在进行大分配后继续使用一段时间)可能会受益于
echo defer+madvise >/sys/kernel/mm/transparent_hugepage/defrag 让内核在需要时对物理内存进行碎片整理,而不是回退到 4k 页. (见the kernel docs)。在进行大量分配(最好仍然使用 2MiB 对齐)后使用madvise(MADV_HUGEPAGE) 以更强烈地鼓励内核现在停止并进行碎片整理。 defrag = always 对于大多数工作负载来说过于激进,并且会花费更多时间来复制页面而不是节省 TLB 未命中。 (kcompactd could maybe be more efficient.)

顺便说一句,英特尔和 AMD 将 2M 页面称为“大页面”,而“巨大”仅用于 1G 页面。 Linux 对大于标准大小的所有内容都使用“hugepage”。

(32 位模式传统(非 PAE)页表只有 4M 页作为下一个最大大小,只有 2 级页表具有更紧凑的条目。下一个大小应该是 4G,但这就是整个地址空间,翻译的“级别”是 CR3 控制寄存器,而不是页面目录条目。IDK,如果这与 Linux 的术语有关。)


附录 B:Oprofile:Linux perf 已大部分取代 oprofileperf list / perf stat -e event1,event2 ... 为大多数编程硬件性能计数器的有用方法命名。

perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,\
branches,branch-misses,instructions,uops_issued.any,\
uops_executed.thread,idq_uops_not_delivered.core -r2 ./a.out

几年前,需要the ocperf.py wrapper 将事件名称转换为代码,但如今perf 已内置该功能。

有关使用它的一些示例,请参阅Can x86's MOV really be "free"? Why can't I reproduce this at all?

【讨论】:

  • 非常有启发性的答案和指点!这显然值得更多投票!
  • @Peter Cordes 还有其他推荐阅读的指南/论文吗?我不是一个高性能程序员,但我想了解更多关于它的知识,并希望能学到一些可以融入我日常编程的实践。
  • @user3927312: agner.org/optimize 是专门针对 x86 的低级内容的最佳和最连贯的指南之一,但一些一般性的想法适用于其他 ISA。除了 asm 指南,Agner 还提供了一个优化的 C++ PDF。有关其他性能/CPU 架构链接,请参阅stackoverflow.com/tags/x86/info。我还写了一些关于通过帮助编译器为关键循环制作更好的 asm 来优化 C++ 的文章,值得看看编译器的 asm 输出:C++ code for testing the Collatz conjecture faster than hand-written asm?
  • 次要的挑剔 - 2 MiB 页面不是 80x86 上的“巨大”页面。对于 80x86 页面大小为 4 KiB、大(2 MiB 或 4 MiB)和大(1 GiB);并且这个术语(普通/正常,大而巨大)被所有东西(Windows,Linux等)共享。 AMD 还开始做“将四个物理上和线性连续的页面合并为一个 TLB 条目”的事情,创建一种“伪 16 KiB 页面”大小(中等页面?我不知道)。
  • @PeterCordes:“大页面”是英特尔和 AMD 一直称为 2 MiB(和 4 MiB)页面的内容。 Windows 也称它们为大页面(例如,VirtualAlloc()MEM_LARGE_PAGES 标志)。 Linux 似乎支持一个或另一个,但不能同时支持两者,并且对任何一种情况都使用相同的词。请注意,瘫痪的操作系统是相对令人震惊的(Windows 根本不支持 1 GiB 页面,需要特殊权限才能使用 2 MiB 页面,不允许 2 MiB 页面“可分页”;Linux 有一个 2独立的系统,用户空间无法选择)
猜你喜欢
  • 2011-02-17
  • 1970-01-01
  • 2010-11-09
  • 1970-01-01
  • 2010-11-09
  • 1970-01-01
  • 2010-09-05
相关资源
最近更新 更多