每个程序员应该了解的内存?

时间:2011-11-14 18:30:09

标签: optimization memory-management x86 cpu-architecture micro-optimization

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

3 个答案:

答案 0 :(得分:86)

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

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

答案 1 :(得分:76)

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。

另请参阅the Latency Bound Platforms section of this answer,了解有关内存/ L3延迟对单线程带宽影响的重要详细信息:bandwidth <= max_concurrency / latency,这实际上是现代多线程上单线程带宽的主要瓶颈 - 像Xeon一样的核心CPU。 (但是四核Skylake桌面可以通过单个线程接近最大化DRAM带宽)。该链接有一些关于NT商店与x86上正常商店的非常好的信息。

因此,Ulrich在 6.5.8利用所有带宽(通过在其他NUMA节点上使用远程内存以及您自己的节点)的建议在内存控制器具有的现代硬件上适得其反比单核更多的带宽可以使用。好吧,你可以想象一种情况,即在同一个NUMA节点上运行多个需要大量内存的线程以实现低延迟线程间通信有一些好处,但让它们使用远程内存来实现高带宽而不是延迟敏感东西。但这很模糊;当你可以使用本地时,通常不是故意使用远程内存,只需在NUMA节点之间划分线程并让它们使用本地内存。

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

改变的一个主要方面是硬件预取 比P4 更好,并且可以识别跨越相当大步幅的跨步访问模式,以及一次多个流(例如,每4k页向前/向后一个)。 Intel's optimization manual描述了用于Sandybridge家族微体系结构的各种级别缓存中HW预取程序的一些细节。 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-family和Ryzen)很多更强大,应该运行真正的线程或不使用超线程(让其他逻辑核心空闲,以便独奏线程拥有完整的资源。)

软件预取一直很脆弱&#34; :获得加速的正确魔法调整数取决于硬件的细节,也可能是系统负载。太早了,它在需求负荷之前被逐出。太晚了,它没有任何帮助。 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,行size =向量宽度,因此您可以在一条指令中加载/存储整个缓存行。 (因此,每个未对齐的加载/存储都会跨越缓存行边界,而不是其他每个用于256b AVX1 / AVX2,这通常不会减慢在L1D中没有的数组上的循环。)

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

正如Ulrich预测的那样,现在每个多插座系统都是NUMA:集成内存控制器是标准配置,即没有外部北桥。但是SMP不再意味着多插槽,因为多核CPU很普遍。 (从Nehalem到Skylake的Intel CPU使用大型包含 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执行某项操作的算法无法通过lock指令完成通常不是灾难。使用C++11 std::atomic或C11 stdatomic代替gcc legacy __sync built-ins或更新__atomic built-ins,除非您想将原子访问和非原子访问混合到同一位置......

8.1 DWCAS(cmpxchg16b:你可以哄骗gcc发射它,但如果你想要有效加载只有一半的对象,你需要丑陋的union黑客: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。有一种锁定方式可以使用它来加速使用(并且可以回退到)常规锁定的代码(特别是对于容器的所有元素使用单个锁定,因此同一个关键部分中的多个线程通常会#39; t碰撞),或编写直接了解交易的代码。

7.5 Hugepages :匿名透明的大页面在Linux上运行良好,无需手动使用hugetlbfs。使用2MiB对齐进行分配&gt; = 2MiB(例如,posix_memalign, or an aligned_allocsize % alignment != 0}时不强制执行愚蠢的ISO C ++ 17要求失败。

默认情况下,2MiB对齐的匿名分配将使用大页面。一些工作负载(例如,在制作它们之后继续使用大量分配)可能会受益于 echo always >/sys/kernel/mm/transparent_hugepage/defrag 让内核在需要时对物理内存进行碎片整理,而不是回落到4k页。 (见the kernel docs)。或者,在进行大量分配后使用madvise(MADV_HUGEPAGE)(最好仍然使用2MiB对齐)。

附录B:Oprofile :Linux perf大部分已取代oprofile。有关特定微架构的详细事件,use the ocperf.py wrapper。例如

ocperf.py 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

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

答案 2 :(得分:69)

从我的快速浏览一下,它看起来非常准确。需要注意的一点是,“集成”和“外部”内存控制器之间的区别。自从i7系列发布以来,英特尔CPU全部集成在一起,而AMD自从AMD64芯片首次发布以来就一直在使用集成内存控制器。

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