在“行走”大图时最小化页面错误(和TLB错误)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在“行走”大图时最小化页面错误(和TLB错误)相关的知识,希望对你有一定的参考价值。
问题(想想GC的标记阶段)
- 我有一个“对象”图表,我需要走路,访问所有对象。
- 如果已访问过,我可以存储在每个对象中。
- 所有对象都存储在内存中,并使用普通指针链接在一起。
- 对象的大小不尽相同。
- 有时系统中没有足够的RAM来同时将所有对象保存在内存中,我希望避免“页面颠簸”。
- 我也希望避免TLB故障
- 其他时候,有足够的公羊。
- 我不介意编写低级代码。
- 我不介意Windows和Linux的不同代码。
- 代码必须在“用户空间”中运行,而不需要任何标准权限。
- 我不关心我访问节点的顺序。
我将询问有关可能解决方案的更多详细问题,并回到这些问题。
页面错误不一定是坏的,只要它们不会拖延你的进度。
这意味着如果你有一个节点Node* p
有两个候选后继p->left
和p->right
,那么选择最近的(用(char*)p - (char*)p->next
)和预取另一个(例如用PrefetchVirtualMemory)会很有用。
效率如何无法预测;它很大程度上取决于您的图形拓扑。但是当你有足够的RAM时,预取几乎是免费的。
更接近CPU,有cache prefetching。相同的想法,不同的存储
对于充满“热”数据的地址范围使用2M largepages,内核无法有效地交换任何/多个4k块。这将减少TLB未命中,但如果有大量页面的任何4k块不热,则会花费额外的物理内存。
Linux对匿名页面(https://www.kernel.org/doc/Documentation/vm/transhuge.txt)透明地执行此操作,但您可以在您知道值得的页面上使用madvise(MADV_HUGEPAGE)
,以鼓励内核对物理内存进行碎片整理,即使这不是/sys/kernel/mm/transparent_hugepage/defrag
中的默认值。 (你可以查看/proc/PID/smaps
,看看有多少透明大页面用于任何给定的映射。)
根据你在答案中发布的内容:一组有序的nodesToVisit
会给你最多的地方,但维护起来可能太贵了。同一个64字节高速缓存行中的多次访问比从L3高速缓存中逐出后再返回它后便宜得多。
如果你在你的Set中有很多地址可以访问,那么在一个巨大的页面中进行一次基数排序的传递到2M桶中会给你一个位置。 2M也小于L3缓存大小,因此当访问同一缓存行中的多个对象时,您可能会获得一些缓存命中,即使您没有连续点击它们。
根据你的Set的大小,抛出那么多指针甚至对它们进行部分排序可能不值得花费在内存流量上。但是,可能会有一些利用数据窗口并至少部分对其进行排序的好处。在将它们从缓存中逐出之前使用指针是很好的。
SW预取可以触发页面遍历以避免TLB未命中,因此您可以在启动当前存储桶之前从下一个2M存储桶中获取_mm_prefetch(_MM_HINT_T2)
一个地址。另见Prefetching Examples?。我没有测试过这个,但它可能运行良好。它对页面错误没有帮助:从未映射的页面预取不会导致页面错误,并且在您准备好触摸页面之前,您不希望触发实际的PF。
MSalter建议要求操作系统预取并连接下一页很有意思(我认为madvise(MADV_WILLNEED)
是Linux的等价物),但是如果页面已经映射+连接到HW页表,系统调用将会很慢,没有任何好处。没有x86 asm指令只是询问页面是否被映射而没有错误,如果不是,所以我想不出有效选择不调用它的方法。而顺便说一句,我认为Linux将透明的大页面分解为4k常规页面,用于分页输入/输出。但是,不要在2M块的所有4k页面上写一个只有_mm_prefetch()
或madvise
的大循环;这可能很糟糕。 prefetcht2
部分可能只会导致多余的预取请求被删除。
使用perf计数器查看缓存命中/未命中率。在Intel CPU上,mem_load_retired.l1_miss
和/或.l2_miss
事件应该显示您是否在访问Set本身时以及访问取消引用这些指针时获得缓存命中。这些计数器是精确事件,因此它们应该准确映射到asm加载指令。 (例如Linux上的perf record -e mem_load_retired.l2_miss ./my_program
/ perf report
)。
我们从
nodesToVisit
一次删除一个项目
我对GC设计了解不多,但是你不能使用序列号或标记指针或其他东西来避免每次GC传递都修改Set数据结构本身吗?如果您的最小对象对齐是4个字节,则在每个指针的底部有2位可以使用。在解除引用之前将它们关闭非常便宜。
具有完整64位指针的x86-64当前要求前16位是低48的符号扩展。因此,如果重新规范化指针,则可以使用位(16位,或者可能只是顶部字节)。 (重做符号扩展,如果你想假设用户空间指针,只需将高16位归零; Linux使用高半内核VM布局,因此用户空间地址总是处于虚拟地址空间的一半.IDK是什么Windows确实。)
在x86-64上,如果4GiB的地址空间足够,你可以考虑使用x32 ABI (32-bit pointers in long mode),特别是如果你达到物理内存限制和交换。较小的指针意味着较小的数据结构,因此缓存占用空间的一半。
有些Linux系统是在没有x32内核支持的情况下构建的,只有经典的x86-64和32位模式。但如果它适用于您的系统,请考虑gcc -mx32
。
这些是我对可能的解决方案的第一个想法,它们显然不是最佳的。如果有人发布更好的答案,我会删除此答案。
基本方法:
- 假设我们有一个包含我们尚未访问过的所有节点的
Set<NodePointer> nodesToVisit
。 - 我们从
nodesToVisit
一次删除一个项目, 如果在我们将所有“指向其他节点的指针”添加到nodesToVisit之前尚未访问过它。
改进:
但是我们可以通过基于地址对nodesToVisit进行排序来做得更好,这样我们就更有可能访问我们最近访问过的页面中包含的节点。这可能就像拥有第二个Set<NodePointer> nodesToVisitLater
一样简单,并且将任何具有地址的节点从当前节点放入其中。
或者我们可以跳过未驻留在内存中的页面中包含的任何节点,在访问了当前在内存中的所有节点之后访问这些节点。
(“set”可能只是一个堆栈,因为不止一次访问节点是“no-opp”)
https://patents.google.com/patent/US7653797B1/en似乎有关系,但我还没看过。 https://hosking.github.io/links/Cher+2004ASPLOS.pdf https://people.cs.umass.edu/~emery/pubs/cramm.pdf https://people.cs.umass.edu/~emery/pubs/f034-hertz.pdf https://people.cs.umass.edu/~emery/pubs/04-16.pdf
以上是关于在“行走”大图时最小化页面错误(和TLB错误)的主要内容,如果未能解决你的问题,请参考以下文章