关于优化:每位程序员应该了解的内存?

What Every Programmer Should Know About Memory?

我想知道,每个程序员对2007年的内存应该了解多少ulrich drepper的内容仍然有效。我也找不到比1.0或勘误表更新的版本。


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

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


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

它仍然是一般优秀的,并且被高度推荐(由我和其他性能调优专家推荐)。如果乌尔里希(或其他任何人)写了2017年的更新,那将是很酷的,但那将是很多工作(例如重新运行基准)。还可以在x86标签wiki中看到其他x86性能调整和SSE/ASM(和C/C++)优化链接。(Ulrich的文章不是针对x86的,但他的大部分(全部)基准测试都是针对x86硬件的。)好的。

关于DRAM和缓存如何工作的低级硬件细节仍然适用。DDR4使用与DDR1/DDR2(读/写突发)相同的命令。DDR3/4的改进并不是根本性的改变。Afaik,所有与Arch无关的内容仍然普遍适用,例如AARCH64/ARM32。好的。

有关内存/L3延迟对单线程带宽的影响的重要详细信息,请参阅此答案的延迟限制平台部分:bandwidth <= max_concurrency / latency,这实际上是现代多核CPU(如Xeon)上单线程带宽的主要瓶颈。(但是四核Skylake桌面几乎可以用一个线程来最大化DRAM带宽)。这个链接有一些关于NT商店和x86上普通商店的非常好的信息。好的。

因此,Ulrich在6.5.8中提出的利用所有带宽的建议(通过在其他NUMA节点上以及您自己的节点上使用远程内存)在现代硬件上会适得其反,因为在这些硬件上,内存控制器的带宽超过了单核所能使用的带宽。很可能您可以想象这样一种情况:在同一个NUMA节点上运行多个内存占用线程,以实现低延迟的线程间通信,但让它们使用远程内存实现高带宽而不是延迟敏感的功能,会有一些好处。但这很难理解;通常,当您可以使用本地时,不必故意使用远程内存,只需在numa节点之间划分线程,让它们使用本地内存。好的。(通常)不使用软件预取

一个主要的改变是硬件预取比P4好得多,并且可以识别跨步访问模式到相当大的跨步,并且一次可以识别多个流(例如,每4K页一个前进/后退)。英特尔的优化手册介绍了各种级别的高速缓存中的硬件预取器的一些细节,这些预取器用于SandyBridge系列微体系结构。Ivybridge和更高版本具有下一页硬件预取,而不是等待新页中的缓存丢失触发快速启动。(我认为AMD在他们的优化手册中也有类似的东西。)请注意,英特尔的手册也充满了旧的建议,其中一些建议只对P4有用。SandyBridge特定的部分对于SNB当然是精确的,但是,例如,HSW中的微熔Uops未分层,手册没有提到。好的。

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

我认为,使用单独的预取线程(6.3.4)的建议完全过时了,而且只在奔腾4上做得很好。P4具有超线程(两个逻辑内核共享一个物理内核),但没有足够的无序执行资源或跟踪缓存来获得在同一个内核上运行两个完整计算线程的吞吐量。但是现代的CPU(sandybridge家族和ryzen)要大得多,要么运行一个真正的线程,要么不使用超线程(让另一个逻辑核心空闲,这样SOLO线程就有了完整的资源)。好的。

软件预取一直是"脆弱的":正确的魔法调优数字,以获得一个加速取决于硬件的细节,也许系统负载。太早了,在需求加载之前就被驱逐了。太晚了,没用。这篇博客文章展示了一个有趣的实验的代码+图形,在Haswell上使用sw prefetch来预取问题的非顺序部分。另请参见如何正确使用预取指令?.nt prefetch很有趣,但更脆弱(因为从l1提前退出意味着您必须一直到l3或dram,而不仅仅是l2)。如果您需要最后一点性能,并且可以针对特定的机器进行调优,那么sw prefetch值得考虑顺序访问,但是如果您有足够的ALU工作来完成,并且接近内存瓶颈,那么仍然可能是一个减速。好的。

缓存行大小仍为64字节。(L1D读/写带宽非常高,现代CPU可以在每个时钟上执行2个矢量加载+1个矢量存储(如果它都命中了L1D中)。看缓存怎么能这么快?)对于avx512,行大小=向量宽度,因此您可以在一条指令中加载/存储整个缓存行。(因此,对于256b avx1/avx2,每一个未对齐的加载/存储都会跨越缓存线边界,而不是每隔一个边界,这通常不会减慢对不在l1d中的数组的循环。)好的。

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

正如乌尔里希预测的那样,现在每个多插座系统都是NUMA:集成内存控制器是标准的,也就是说,没有外部的NorthBridge。但是,由于多核CPU的普及,SMP不再意味着多插槽。(从Nehalem到Skylake的Intel CPU都使用了一个大的包含三级缓存作为内核之间一致性的后盾。)AMD CPU是不同的,但我对细节不太清楚。好的。

Skylake-X(AVX512)不再具有包含L3的功能,但我认为还有一个标签目录,它可以检查芯片上任何位置缓存的内容(如果有的话,在哪里),而不必向所有核心广播嗅探。不幸的是,SKX使用的是网格而不是环形总线,其延迟通常比以前的许多核心Xeon更糟糕。好的。

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

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

C++ 11 EDCOX1,1 EDCOX1,2,将编译成EDCOX1,0,0(或使用EDCOX1,4,如果使用返回值),但是一个使用CAS来做不能用EDCOX1,5,ED指令做的事情的算法通常不是灾难。使用C++ 11 EDCOX1,1或C11 EDCOX1,7,而不是GCC遗留EDCOX1,8内建的INS或更新的EDCOX1×9内置的,除非您想要混合原子和非原子访问到相同的位置…好的。

8.1 DWCAS(EDCOX1(10)):你可以哄骗GCC发射它,但是如果你只需要一半的有效负载,你就需要丑陋的EDOCX1和11个黑客:如何用C++ 11 CAS实现ABA计数器?(不要将dwcas与两个独立内存位置的dcas混淆。使用dwcas不可能实现dcas的无锁原子仿真,但事务性内存(如x86 TSX)使之成为可能。)好的。

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

7.5 Hugepages:匿名透明Hugepages在Linux上运行良好,无需手动使用Hugetlbfs。使用2MIB对齐(例如EDCOX1,12),或aligned_alloc,在size % alignment != 0中不执行愚蠢的ISO C++ 17要求时,分配分配>=2MIB。好的。

默认情况下,与2Mib对齐的匿名分配将使用Hugepages。一些工作负载(例如,在进行了大量分配之后仍在使用一段时间的工作负载)可能会受益于echo always >/sys/kernel/mm/transparent_hugepage/defrag让内核在需要时对物理内存进行碎片整理,而不是返回到4K页。(参见内核文档)。或者,在进行大量分配(最好仍保持2Mib对齐)后使用madvise(MADV_HUGEPAGE)。好的。

附录B:oprofile:linux perf已经大部分取代了oprofile。对于特定于某些微体系结构的详细事件,请使用ocperf.py包装器。例如好的。

1
2
3
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是否真的是"免费的"?为什么我不能复制这个?.好的。好啊。


从我的快速浏览来看,它看起来相当准确。需要注意的一件事是"集成"和"外部"内存控制器之间的区别部分。自从i7系列Intel CPU发布以来,AMD一直在使用集成内存控制器,AMD64芯片首次发布以来。

自从写了这篇文章之后,并没有改变很多东西,速度提高了,内存控制器变得更加智能(i7将延迟写入RAM,直到它感觉像提交了更改),但并没有改变很多东西。至少不是软件开发人员所关心的。