在“遍历”大图时最大程度地减少页面错误(和TLB错误)

时间:2018-09-07 11:13:00

标签: performance graph x86 garbage-collection page-fault

问题(考虑GC的标记阶段)

  • 我有一张需要走动的“物体”图,拜访了所有物体。
  • 如果已经访问过,我可以将其存储在每个对象中。
  • 所有对象都存储在内存中,并使用普通指针链接在一起。
  • 对象的大小不尽相同。
  • 有时系统中的内存不足以同时将所有对象都保存在内存中,我希望避免“页面颠簸”。
  • 我也希望避免TLB错误
  • 其他时候,ram足够多了。
  • 我不介意编写低级代码。
  • 我不介意Windows和Linux使用不同的代码。
  • 代码必须在“用户空间”中运行,不需要任何标准权限。
  • 我不在乎访问节点的顺序。

我将询问有关可能解决方案的更多详细问题,并链接回这些问题。

3 个答案:

答案 0 :(得分:2)

页面错误不一定不好,只要它们不会阻止您的进度。

这意味着,如果您的节点Node* p包含两个候选后继者p->leftp->right,则选择最近的节点(以(char*)p - (char*)p->next表示)会很有用。并预取另一个(例如,使用PrefetchVirtualMemory)。

这将无法预测到如何有效;这很大程度上取决于您的图形拓扑。但是,当您有足够的RAM时,预取实际上是免费的。

更接近CPU的是cache prefetching。相同的想法,不同的存储方式

答案 1 :(得分:2)

使用2M个大页面处理充满“热”数据的地址范围,内核无法有效地换出任何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缓存中逐出并且必须再次从DRAM重新访问时便宜很多。

如果您要访问集合中的地址很多,那么将基数排序一次传递到2M个存储桶中,就可以在一个很大的页面内找到您的所在地。 2M还小于L3缓存大小,因此,即使您不背靠背击中它们,访问同一缓存行中的多个对象时,也可能会遇到一些缓存命中。

根据Set的大小,扔掉那么多指针甚至对它们进行部分排序可能都不值得占用的内存流量。但是,获取一个数据窗口并至少对其进行部分排序可能会有一些好处。在将它们从缓存中驱出之前使用指针是很好的。

SW预取可以触发页面漫游,从而避免TLB遗漏,因此您可以_mm_prefetch(_MM_HINT_T2)从下一个2M存储桶中选择一个地址,然后再开始使用当前存储桶。另请参见{{ 3}}。我还没有测试过,但是可能效果很好。这不会解决页面错误:从未映射的页面进行预取不会导致页面错误,并且在准备好触摸页面之前,您不希望触发实际的PF。

MSalter建议让操作系统预取并连接下一页的建议很有趣(我认为madvise(MADV_WILLNEED)与Linux等效),但是如果该页面已被映射并连接,那么系统调用将很慢,没有任何好处。进入HW页面表。没有x86 asm指令仅询问页面是否被映射而不会错误地进行映射,因此我无法想到一种有效选择不调用它的方法。顺便说一句,我认为Linux将透明的大页面分解为4k常规页面以进行分页输入/输出。但是不要写一个仅在2M块中的所有4k页面上执行_mm_prefetch()madvise的大循环;那可能糟透了。 prefetcht2部分可能只会导致多余的预取请求被丢弃。

使用性能计数器查看缓存的命中/未命中率。在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个位可播放。在取消引用之前对其进行AND操作非常便宜。

具有完整64位指针的

x86-64当前要求高16位是低位48的符号扩展。因此,如果重新规范化,则可以在此处使用位(16位,或者可能只是最高字节)指针。 (重做符号扩展名,或者如果要假定用户空间指针,则仅将高16位设为零; Linux使用上半部分内核VM布局,因此用户空间地址始终位于虚拟地址空间的下半部分。IDK Windows)确实如此。)

在x86-64上,如果4GiB的地址空间足够,您可以考虑使用Prefetching Examples? ,尤其是在达到物理内存限制并进行交换时。较小的指针意味着较小的数据结构,因此缓存占一半。

尽管有些Linux系统是在没有内核支持x32的情况下构建的,但是只有经典的x86-64且通常是32位模式。但是,如果在您的系统上可以使用,请考虑使用gcc -mx32

答案 2 :(得分:0)

这些是我对可能的解决方案的第一个想法,它们显然不是最佳的。如果有人发布更好的答案,我将删除此答案。

基本方法:

  • 假设我们有一个Set<NodePointer> nodesToVisit,其中包含我们尚未访问过的所有节点。

  • 我们一次从nodesToVisit

    中删除一项
    • ,如果在我们将所有“指向其他节点的指针”添加到nodesToVisit之前尚未访问过它。

改进

但是,通过根据地址对nodesToVisit进行排序,我们显然可以做得更好,以便我们更有可能访问最近访问的页面中包含的节点。这很简单,只需拥有第二个Set<NodePointer> nodesToVisitLater,然后将地址距离当前节点很远的任何节点放入其中即可。

或者我们可以跳过内存中不驻留的页面中包含的任何节点,在访问完当前内存中的所有节点之后访问这些节点。

(“集合”可能只是一个堆栈,因为多次访问节点是“无操作”)


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