我正在看一篇名为A Primer on Scheduling 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)。在执行光纤的过程中,我最终实施了偷孩子的行为。我读到了有关儿童盗窃的(理论上的)负面信息(可运行任务的数量无限制,请参阅本文以获取更多信息),所以我想改用延续。
我正在考虑两个函数shift
和reset
,其中reset
界定了当前的延续,而shift
赋予了当前的延续。在C环境中,我要问的甚至合理吗?
编辑:我正在考虑使reset
保存当前函数调用的返回地址/ NV GPR(=第3行),并使shift
在返回值后将控制权转移到下一个继续reset
的呼叫者。
答案 0 :(得分:3)
我在x86上实现了work stealing for a HLL called PARLANSE而不是C。每天使用PARLANSE来构建百万行规模的生产符号并行程序。
通常,您保留了延续或“子”的寄存器。 考虑到您的编译器可能在f()中看到了一个计算,而在g()中看到了相同的计算,并且可能将该计算提升到生成之前的那一点,并将该计算结果放入f()和g ()用作隐含参数。 是的,这是假定您使用的是复杂的编译器,但是如果您使用的愚蠢的编译器没有进行优化,那么为什么要尝试并行提高速度?
但是,具体来说,如果编译器了解生成的含义,则可以在生成调用之前将寄存器安排为空。这样,延续或子进程都不必保留寄存器。 (实际上,PARLANSE编译器执行此操作)。
因此要节省多少,取决于编译器愿意提供多少帮助,这取决于它是否知道spawn真正起作用。
您的本地友好C编译器可能不了解您的的spawn实现。因此,您要么采取某种措施来强制清除寄存器(不要问我,它的编译器),要么您自己不知道寄存器中包含什么,并且您的实现将它们全部保存为安全的事实。
如果产生的工作量很大,那么可以说保存所有寄存器都没关系。但是,x86(和其他现代体系结构)似乎具有大量可能正在使用的状态,主要是在向量寄存器中。上次我看它完全超过了500个字节~~ 100次写入内存以保存这些内容,恕我直言,这是一个过高的代价。如果您不相信这些寄存器会从父线程传递到生成的线程,那么您可以在不使用寄存器的情况下强制执行生成。
如果您使用自己发明的标准连续机制唤醒例程,那么您将担心自己的连续性是否也通过大型寄存器状态。与生成相同的问题,相同的解决方案;编译器必须提供帮助,或者您必须亲自进行干预。
您会发现很多乐趣。
[[如果您想使其真正有趣,请尝试对线程进行时间切片,以防它们进入深度计算而不会偶尔引起线程饥饿。现在,您肯定已经保存了整个状态。我设法使PARLANSE在没有保存任何寄存器的情况下实现生成,但是通过在一个时间片上保存完整状态,并在一个特殊的地方继续进行时间切片,以保存/恢复完整的寄存器状态,并在将控制权交给时间分段的PC位置]。