207

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

(也可以在 Ulrich Drepper 自己的网站上找到 PDF 格式:https ://www.akkadia.org/drepper/cpumemory.pdf )

4

3 回答 3

226

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

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

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

有关内存/L3 延迟对单线程带宽的影响的重要详细信息,另请参阅此答案延迟绑定平台部分bandwidth <= max_concurrency / latency:这实际上是现代多核 CPU(如 Xeon)上单线程带宽的主要瓶颈. 但是四核 Skylake 桌面可以通过单线程接近最大化 DRAM 带宽。该链接有一些关于 NT 商店与 x86 上的普通商店的非常好的信息。 为什么 Skylake 在单线程内存吞吐量方面比 Broadwell-E 好这么多?是一个总结。

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


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

一个主要的改变是硬件预取比奔腾 4 上的好得多,并且可以识别跨步访问模式,步幅相当大,并且一次可以识别多个流(例如,每 4k 页一个向前/向后)。 英特尔的优化手册描述了其 Sandybridge 系列微架构的各种缓存级别中的硬件预取器的一些细节。Ivybridge 和更高版本具有下一页硬件预取,而不是等待新页面中的缓存未命中来触发快速启动。我认为 AMD 在他们的优化手册中有一些类似的东西。请注意,英特尔的手册也充满了旧的建议,其中一些只对 P4 有用。特定于 Sandybridge 的部分当然对于 SnB 是准确的,但是例如在 HSW 中改变了微熔 uops 的非层压,手册没有提到它

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

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

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


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

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


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

Skylake-X (AVX512) 不再具有包容性的 L3,但我认为仍然有一个标签目录,可以让它检查芯片上任何地方缓存的内容(如果有的话),而无需实际向所有内核广播窥探。 不幸的是, SKX 使用网格而不是环形总线,其延迟通常比以前的多核 Xeon 更差。

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


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

C++11std::atomic fetch_add将编译为 a lock add(或者lock xadd如果使用返回值),但是使用 CAS 的算法来做一些用locked 指令无法完成的事情通常不是灾难。使用C++11std::atomic或 C11stdatomic而不是 gcc 旧版__sync内置插件或较新的__atomic内置插件,除非您想混合对同一位置的原子和非原子访问......

8.1 DWCAS ( cmpxchg16b):你可以诱使 gcc 发射它,但是如果你想要有效地加载一半的对象,你需要丑陋的unionhacks:如何使用 c++11 CAS 实现 ABA 计数器?. (不要将 DWCAS 与2 个独立内存位置的 DCAS 混淆。DWCAS 无法进行 DCAS 的无锁原子模拟,但事务性内存(如 x86 TSX)使其成为可能。)

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

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


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

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

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

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


附录 B:Oprofile:Linuxperf已基本取代oprofile. perf 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

几年前,需要ocperf.py包装器将事件名称转换为代码,但现在perf已经内置了该功能。

有关使用它的一些示例,请参阅x86 的 MOV 真的可以“免费”吗?为什么我根本无法重现这个?.

于 2017-12-08T12:32:38.020 回答
133

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

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

于 2011-11-14T18:40:44.270 回答
76

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

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

于 2011-11-14T18:40:52.377 回答