C / x64 ASM中的实用分隔延续



我看过一篇名为《A Primer on Schedule Fork-Join Parallelism with Work Stealing》的论文。我想实现继续窃取,其中调用spawn后的其余代码有资格被盗。这是论文中的代码。

1 e();
2 spawn f(); 
3 g();
4 sync;
5 h();

导入设计选择是向窃贼线程提供哪个分支。 使用图 1,选项包括:

偷窃儿童:

  • f(( 可供盗贼线程使用。
  • 执行 e(( 的线程执行 g((。

继续窃取:

  • 也称为"父母偷窃"。
  • 执行 e(( 的线程执行 f((。
  • 延续(接下来将调用 g(((可供盗贼线程使用。

我听说保存延续需要保存两组寄存器(易失性/非易失性/FPU(。在我所做的光纤实施中,我最终实现了儿童偷窃。我读到了儿童偷窃的(理论上的(负面影响(无限数量的可运行任务,有关更多信息,请参阅论文(,所以我想改用延续。

我正在考虑两个函数,shiftreset,其中reset界定当前的延续,shift指定当前的延续。我问的问题在 C 环境中是否合理?

编辑:我正在考虑为当前函数调用(= 第 3 行(保存返回地址/NV GPRreset并在将值返回给reset的调用者后shift将控制权转移到下一个延续。

我已经在x86上为名为PARLANSE而不是C的HLL实现了工作窃取。 PARLANSE 每天都用于构建百万生产线规模的生产符号并行程序。

通常,您保留了延续或"子"的寄存器。 考虑您的编译器可能会在 f(( 中看到一个计算,并在 g(( 中看到相同的计算,并且可能会将该计算提升到生成之前的点,并将该计算结果放在一个寄存器中,f(( 和 g(( 都像隐含参数一样使用。 是的,这假设了一个复杂的编译器,但是如果您使用的是不优化的愚蠢编译器,为什么还要尝试并行以提高速度呢?

但是,具体来说,如果编译器理解 spawn 的含义,则可以在调用 spawn 之前安排寄存器为空。 那么延续或子级都不必保留寄存器。(PARLANSE 编译器实际上就是这样做的(。

因此,必须保存多少取决于编译器愿意提供多少帮助,这取决于它是否知道 spawn 真正做了什么。

本地友好的 C 编译器可能不知道您的spawn 实现。 因此,要么你做一些事情来强制寄存器刷新(不要问我,这是你的编译器(,要么你忍受这样一个事实,即你个人不知道寄存器中有什么,并且你的实现将它们全部保留为安全。

如果生成的工作量很大,那么可以说,是否保存了所有寄存器并不重要。 然而,x86(和其他现代架构(似乎有大量的状态,主要是在矢量寄存器中,可能正在使用;上次我看它远远超过 500 字节 ~~ 100 次写入内存以保存这些,恕我直言,这是一个过高的价格。 如果您不相信这些寄存器会从父线程传递到生成的线程,那么您可以在没有寄存器的情况下强制执行 spawn。

如果你使用你发明的标准延续机制来生成例程唤醒,那么你也会担心你的延续是否通过大寄存器状态。 同样的问题,与生成的相同解决方案;编译器必须提供帮助,否则您个人必须进行干预。

你会发现这很有趣。

[如果你想让它变得非常有趣,试着对线程进行时间切片,以防它们进入深度计算,而不会偶尔导致线程饥饿。 现在你肯定已经保存了整个州。 我设法让 PARLANSE 在没有保存寄存器的情况下实现生成,但通过在一个时间片上保存完整状态,并在一个特殊的地方继续在将控制权传递到时间切片 PC 位置之前重新填充所有寄存器,从而对时间切片保存/恢复完整寄存器状态进行了时间切片保存/恢复

最新更新