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

Fra*_*ter 145 optimization x86 memory-management cpu-architecture micro-optimization

我想知道Ulrich Drepper 从2007年开始对每个程序员应该知道的内容有多少仍然有效.另外,我找不到比1.0更新的版本或勘误表.

Pet*_*des 112

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,所有与arch无关的东西一般都适用于例如AArch64/ARM32.

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

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


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

所更改一个重要的事情是,硬件预取比P4好,可以一次识别跨入访问模式达到一个相当大的跨度和多流(例如,一个前进/每4K向后翻页). 英特尔的优化手册描述了Sandybridge系列微体系结构中各种级别缓存中HW预取程序的一些细节.Ivybridge以及后来的页面硬件预取,而不是等待新页面中的缓存未命中以触发快速启动.(我认为AMD在他们的优化手册中有一些类似的东西.)请注意,英特尔的手册也充满了旧的建议,其中一些只对P4有用.Sandybridge特定的部分当然对于SnB是准确的,但是例如在HSW中改变了微熔融 uops 的分层并且手册没有提到它.

这些天通常的建议是从旧代码中删除所有SW预取,并且如果分析显示缓存未命中(并且您没有使内存带宽饱和),则只考虑将其重新放入.预取二进制搜索的下一步的两侧仍然有用.例如,一旦确定接下来要查看哪个元素,请预取1/4和3/4元素,以便它们可以与加载/检查中间并行加载.

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

软件预取一直是"脆弱的":获得加速的正确魔法调整数取决于硬件的细节,也可能是系统负载.太早了,它在需求负荷之前被逐出.太晚了,没有用. 这篇博客文章展示了在Haswell上使用SW预取来预取问题的非连续部分的有趣实验的代码+图表.另请参阅如何正确使用预取指令?.NT预取很有意思,但更加脆弱(因为早期从L1中驱逐意味着你必须一直到L3或DRAM,而不仅仅是L2).如果您需要最后一滴性能,并且可以针对特定机器进行调整,则SW预取值得关注顺序访问,但如果您在接近瓶颈内存时有足够的ALU工作可能仍然会减速.


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

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


正如Ulrich预测的那样,现在每个多插槽系统都是NUMA:集成内存控制器是标准配置,即没有外部北桥.但是SMP不再意味着多插槽,因为多核CPU很普遍.(从Nehalem到Skylake的Intel CPU已经使用大型包容性 L3缓存作为核心之间一致性的后盾.)AMD CPU有所不同,但我对细节不太清楚.

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

基本上所有关于优化内存放置的建议仍然适用,只是当您无法避免缓存未命中或争用变化时所发生的具体细节.


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

C++ 11 std::atomic fetch_add将编译为a lock add(或者lock xadd如果使用返回值),但是使用CAS执行无法使用locked指令执行的操作的算法通常不是灾难.使用C++ 11std::atomic或C11 stdatomic代替gcc遗留__sync内置函数或更新的__atomic内置函数,除非您想将原子和非原子访问混合到同一位置...

8.1 DCAS(cmpxchg16b):你可以哄骗gcc发射它,但是如果你想要只有一半对象的有效负载,你需要丑陋的union黑客:我如何用c ++ 11 CAS实现ABA计数器?

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.py包装器.例如

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
Run Code Online (Sandbox Code Playgroud)

有关使用它的一些示例,请参阅x86的MOV真的可以"免费"吗?为什么我不能重现这个呢?.

  • 非常有启发性的答案和指针!这显然值得更多的选票! (3认同)
  • @ user3927312:https://agner.org/optimize/是专门针对x86的低级内容的最佳,最一致的指南之一,但其中一些一般性思想适用于其他ISA。除asm指南外,Agner还具有优化的C ++ PDF。有关其他性能/ CPU体系结构链接,请参见https://stackoverflow.com/tags/x86/info。我还写了一些有关优化C ++的方法,通过值得一看的编译器的asm输出来帮助编译器为关键循环提供更好的asm:[用于测试Collat​​z猜想的C ++代码比手写的asm更快?](// stackoverflow.com/q/40354978) (3认同)
  • @PeterCordes:“大页面”是 Intel 和 AMD 一直所说的 2 MiB(和 4 MiB)页面。Windows 也称它们为大页面(例如“VirtualAlloc()”的“MEM_LARGE_PAGES”标志)。Linux 似乎支持其中之一,但不同时支持两者,并且对这两种情况使用相同的词。请注意,操作系统的缺陷是相对令人震惊的(Windows 根本不支持 1 GiB 页面,仅需要特殊权限才能使用 2 MiB 页面,不允许 2 MiB 页面“可分页”;而 Linux 则有一个黑客的污水池,具有 2独立的系统,用户空间无法选择) (2认同)
  • @PeterCordes:啊,你是对的(“kcompactd”是我错过的线索)。它实际上比我想象的更糟糕/更可怕 - 就像名字所暗示的那样,它实际上是在进行“压缩”(将所有内容从物理内存区域的一端移动到另一端),而不是仅在属于较大页面的大多数小页面已经存在时进行打扰自由的。 (2认同)

Dan*_*nin 102

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

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

  • 如今性能==内存性能,因此理解内存在任何高性能应用程序中都是最重要的.这使得论文对参与的任何人都至关重要:游戏开发,科学计算,财务,数据库,编译器,大型数据集处理,可视化,任何必须处理大量请求的东西......所以是的,如果你在一个应用程序中工作大部分时间都是空闲的,就像文本编辑器一样,这篇论文完全没有意义,直到你需要做一些快速的事情,比如找一个单词,计算单词,拼写检查......哦等等......没关系. (43认同)
  • 是的,我真的不明白为什么程序员应该知道SRAM和DRAM如何在模拟级别上工作 - 这在编写程序时无济于事.而那些真正需要这些知识的人,最好花时间阅读手册,了解有关实际时间的详细信息等等.但是对于那些对HW低级别的东西感兴趣的人呢?也许没用,但至少是有趣的. (3认同)

Tim*_*dge 72

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

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

  • 我本来希望接受你们两个.但我已经赞成你的帖子了. (5认同)
  • 可能与SW开发人员相关的最重大变化是预取线程是一个坏主意.CPU足够强大,可以通过超线程运行2个完整的线程,并且具有更好的HW预取功能.一般来说,SW预取是一个不太重要的地方,特别是对于顺序访问.看我的回答. (5认同)

归档时间:

查看次数:

20503 次

最近记录:

6 年,2 月 前