PDF 格式的指南位于https://www.akkadia.org/drepper/cpumemory.pdf。
它仍然非常出色且强烈推荐(由我推荐,我认为由其他性能调整专家推荐)。如果 Ulrich(或其他任何人)编写 2017 年更新会很酷,但这将是很多工作(例如重新运行基准测试)。另请参阅x86tag 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 answer:bandwidth <= 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 已大部分取代 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
几年前,需要the ocperf.py wrapper 将事件名称转换为代码,但如今perf 已内置该功能。
有关使用它的一些示例,请参阅Can x86's MOV really be "free"? Why can't I reproduce this at all?。