每个程序员都应该了解内存什么?


163

我想知道从2007年起,Ulrich Drepper的“每个程序员应该了解的内存 ”中有多少仍然有效。我也找不到比1.0或勘误表新的版本。


1
有人知道我是否可以在某处下载mobi格式的本文,以便在kindle上轻松阅读?“PDF”是非常困难的,因为与变焦问题读/格式化
javapowered

1
它不是移动设备,但是LWN将本文作为一组文章进行了整理,便于在手机/平板电脑上阅读。首先是在lwn.net/Articles/250967
Nathan

Answers:


111

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

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


3
是的,我真的不明白为什么程序员应该需要知道SRAM和DRAM如何在模拟级别上工作-这在编写程序时无济于事。真正需要这些知识的人,最好花些时间阅读手册,了解有关实际时序的详细信息,等等。但是对于对硬件基础知识感兴趣的人呢?可能没有用,但至少很有趣。
Voo

47
如今的表现==内存性能,所以了解内存任何高性能应用最重要的事情。这使得本文对于参与其中的任何人都至关重要:游戏开发,科学计算,金融,数据库,编译器,大型数据集的处理,可视化,任何必须处理大量请求的内容……所以,是的,如果您在应用程序中工作在大多数情况下,它是闲置的,就像文本编辑器一样,直到您需要快速执行一些操作(如查找单词,对单词进行计数,进行拼写检查)之前,论文完全没有兴趣...等等...没关系。
gnzlbg

144

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

通常,它仍然是出色的,并受到强烈推荐(对我而言,以及其他性能调整专家认为)。如果乌尔里希(或其他任何人)编写了2017年的更新,那将很酷,但这将是很多工作(例如,重新运行基准测试)。另请参见中的其他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带宽。该链接提供了有关x86上的NT存储与普通存储的一些非常好的信息。 为什么Skylake在单线程内存吞吐量方面比Broadwell-E好得多?是一个摘要。

因此,Ulrich在6.5.8利用所有带宽中关于在其他NUMA节点以及您自己的NUMA节点上使用远程内存的建议在现代硬件上适得其反,在现代硬件上,内存控制器的带宽超过了单个内核可以使用的带宽。很可能您可以想象这样一种情况:在同一个NUMA节点上运行多个需要大量内存的线程以进行低延迟线程间通信是有净收益的,但是让它们将远程内存用于对高带宽不敏感的事物。但这很晦涩,通常只在NUMA个节点之间分配线程,并让它们使用本地内存。由于最大并发限制(请参阅下文),每核带宽对延迟很敏感(但请参见下文),但是一个插槽中的所有内核通常可以使该插槽中的内存控制器饱和。


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

所更改一个重要的事情是,硬件预取比奔腾4更好,可以一次(例如,一个前进/每4K向后翻页)承认跨入访问模式达到一个相当大的跨度和多个流。 英特尔的优化手册针对其Sandybridge系列微体系结构在各种缓存级别中描述了硬件预取器的一些详细信息。Ivybridge和更高版本具有下一页硬件预取功能,而不是等待新页面中的高速缓存未命中以触发快速启动。我认为AMD在其优化手册中也有类似的内容。请注意,英特尔手册中也充满了旧建议,其中一些建议仅适用于P4。对于SandB,特定于Sandybridge的部分当然是准确的,但例如HSW改变了微熔合体的非层压结构,手册中没有提及

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

我认为使用单独的预取线程(6.3.4)的建议完全过时了,并且仅在Pentium 4上才是好的。P4具有超线程(2个逻辑内核共享一个物理内核),但是跟踪缓存不足(并且(或无序执行资源)以在同一内核上运行两个完整的计算线程来获得吞吐量。但是现代的CPU(Sandybridge-family和Ryzen)要强大得多,应该运行真实线程或不使用超线程(将另一个逻辑核心保持空闲状态,以便单独线程拥有全部资源而不是对ROB进行分区)。

软件预取一直很“脆弱”:获得加速的正确魔术调整数字取决于硬件的详细信息,可能还取决于系统负载。太早了,它在需求负载之前就被逐出了。太晚了,它没有帮助。 这篇博客文章显示了代码+图形,这是在Haswell上使用SW预取来预取问题的非顺序部分的有趣实验。另请参阅如何正确使用预取指令?。NT预取很有趣,但更易碎,因为从L1驱逐早期意味着您必须一直到L3或DRAM,而不仅仅是L2。如果您需要性能的最后降低,并且可以针对特定计算机进行调优,则值得考虑使用SW预取来进行顺序访问,但是它可以如果您在接近内存瓶颈时有足够的ALU工作要做,则可能仍然会减慢速度。


缓存行大小仍然是64个字节。(L1D的读/写带宽非常高,如果所有CPU都命中L1D,那么现代CPU可以在每个时钟上执行2个矢量加载+ 1个矢量存储。请参阅如何快速地进行缓存?。)使用AVX512,行大小=矢量宽度,因此您可以在一条指令中加载/存储整个缓存行。因此,每个未对齐的加载/存储都跨越高速缓存行边界,而不是跨越256b AVX1 / AVX2,这通常不会减慢不在L1D中的阵列的循环速度。

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


正如乌尔里希(Ulrich)预测的那样,如今每个多插槽系统都是NUMA:集成存储控制器是标准的,即没有外部北桥。但是SMP不再意味着多路插槽,因为多核CPU已普及。英特尔CPU从Nehalem处理器来SKYLAKE微架构已经使用了一个大的L3缓存为内核之间一致性的逆止器。AMD CPU不同,但是我不清楚细节。

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

基本上,关于优化内存放置的所有建议仍然适用,只是无法避免缓存丢失或争用时发生的确切变化的详细信息。


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

C ++ 11 std::atomic fetch_add可以编译为lock add(或lock xadd使用返回值),但是使用CAS执行locked指令无法完成的操作的算法通常不会造成灾难。除非要混合对同一位置的原子访问和非原子访问,否则请使用C ++ 11std::atomic或C11 stdatomic代替gcc旧式__sync内置插件或较新的__atomic内置插件 ...

8.1 DWCAS(cmpxchg16b:您可以哄骗gcc发出它,但是如果您只想有效负载一半对象,就需要丑陋的union技巧:如何用c ++ 11 CAS实现ABA计数器?。(不要将DWCAS与2个单独的内存位置的DCAS混淆。DWCAS无法实现DCAS的无锁原子仿真,但是事务性内存(如x86 TSX)可以实现。)

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


7.5 Hugepages:匿名透明的大页面在Linux上可以很好地工作,而无需手动使用hugetlbfs。使用2MiB对齐方式使分配> = 2MiB(例如posix_memalign,或者aligned_alloc不强制愚蠢的ISO C ++ 17要求失败的size % alignment != 0)。

默认情况下,与2MiB对齐的匿名分配将使用大页面。某些工作负载(例如,在分配大容量
echo always >/sys/kernel/mm/transparent_hugepage/defrag内存后会继续使用一段时间)可能会从使内核在需要时对物理内存进行碎片整理中受益,而不是退回到4k页。(请参阅内核文档)。或者,madvise(MADV_HUGEPAGE)在进行较大分配后使用(最好仍然使用2MiB对齐方式)。


附录B:Oprofile:Linux perf已基本被取代oprofile。有关特定于某些微体系结构的详细事件,请使用ocperf.pywrapper。例如

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

有关使用它的一些示例,请参见x86的MOV真的可以“免费”吗?为什么我根本无法复制?


3
非常有启发性的答案和指示!这显然值得更多的选票!
claf

@Peter Cordes您还建议阅读其他指南/论文吗?我不是一个高性能的程序员,但是我想了解更多有关它的信息,并希望能将一些可以融入我日常编程的实践。
user3927312

4
@ user3927312:agner.org/optimize是专门针对x86的低级内容的最佳,最一致的指南之一,但其中一些一般性想法适用于其他ISA。除asm指南外,Agner还具有优化的C ++ PDF。有关其他性能/ CPU体系结构链接,请参见stackoverflow.com/tags/x86/info。我还写了一些有关优化C ++的方法,通过值得一看的编译器的asm输出来帮助编译器为关键循环提供更好的asm:用于测试Collat​​z猜想的C ++代码比手写的asm更快?
彼得·科德斯

74

从我的快速浏览来看,它看起来很准确。要注意的一件事是“集成”和“外部”存储控制器之间差异的部分。自从i7系列产品发布以来,英特尔CPU已全部集成在一起,并且自AMD64芯片首次发布以来,AMD一直在使用集成内存控制器。

自从撰写本文以来,并没有改变很多,速度也提高了,内存控制器变得更加智能(i7会将写入RAM的时间推迟到感觉提交更改之前),但是并没有改变很多。至少不是软件开发人员会关心的任何方式。


5
我本来想接受你们两个的。但是我赞成你的帖子。
Framester

5
与SW开发人员相关的最主要的更改可能是预取线程是一个坏主意。CPU足够强大,可以通过超线程运行2个全线程,并且具有更好的硬件预取。通常,SW预取的重要性小得多,尤其是对于顺序访问而言。看我的答案。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.