Jes*_*tin 5 c windows assembly x86-64 delimited-continuations
我看过一篇名为A Primer on Scheduling Fork-Join Parallelism with Work Stealing 的论文。我想实现连续窃取,其中调用后的其余代码spawn有资格被窃取。这是论文中的代码。
1 e();\n2 spawn f(); \n3 g();\n4 sync;\n5 h();\nRun Code Online (Sandbox Code Playgroud)\n\n\n\n\n导入设计选择是向窃贼线程提供哪个分支。\n 使用图 1,选择是:
\n\n偷窃儿童:
\n\n\n
\n\n- f() 可供窃贼线程使用。
\n- 执行e()的线程执行g()。
\n继续盗窃:
\n\n\n
\n- 也称为\xe2\x80\x9c父偷\xe2\x80\x9d。
\n- 执行e()的线程执行f()。
\n- 延续(接下来将调用 g())可供窃贼线程使用。
\n
我听说保存延续需要保存两组寄存器(易失性/非易失性/FPU)。在我所做的光纤实现中,我最终实现了儿童盗窃。我读到了有关子窃取的(理论上)负面影响(可运行任务的数量不受限制,请参阅论文以获取更多信息),所以我想改用延续。
\n\n我正在考虑两个函数,shift和reset,其中reset界定当前延续,并shift具体化当前延续。我所问的问题在 C 环境中是否合理?
编辑:我正在考虑reset为当前函数调用(=第3行)保存返回地址/NV GPR,并shift在将值返回给 的调用者后将控制转移到下一个延续reset。
我在 x86 上为名为 PARLANSE 的 HLL 而不是 C 实现了工作窃取。PARLANSE 每天用于构建百万行规模的生产符号并行程序。
一般来说,您保留了延续或“子”的寄存器。考虑到您的编译器可能会在 f() 中看到计算,并在 g() 中看到相同的计算,并且可能将该计算提升到生成之前的点,并将该计算结果放置在 f() 和 g 的寄存器中() 用作隐含参数。是的,这假设有一个复杂的编译器,但如果您使用的是一个不优化的愚蠢编译器,为什么要尝试并行以提高速度?
然而,具体来说,如果编译器理解spawn 的含义,则可以在调用spawn 之前将寄存器安排为空。那么延续或子级都不必保留寄存器。(PARLANSE 编译器实际上就是这样做的)。
因此,需要节省多少取决于编译器愿意提供多少帮助,而这取决于它是否知道spawn真正做了什么。
您本地友好的 C 编译器可能不知道您的spawn 实现。因此,要么你做一些事情来强制寄存器刷新(不要问我,这是你的编译器),要么你忍受你个人不知道寄存器中有什么的事实,并且你的实现会保留它们全部安全。
如果产生的工作量很大,那么保存所有寄存器可能并不重要。然而,x86(和其他现代架构)似乎有大量的状态,大部分在向量寄存器中,可能正在使用;上次我查看时,它远远超过了 500 个字节 ~~ 100 次写入内存来保存这些内容,恕我直言,这是一个过高的价格。如果您不相信这些寄存器将从父线程传递到生成的线程,那么您可以在没有寄存器的情况下强制生成。
如果您使用您发明的标准延续机制唤醒生成例程,那么您还需要担心您的延续是否会传递大型寄存器状态。与spawn相同的问题,相同的解决方案;编译器必须提供帮助,否则您必须亲自干预。
你会发现这很有趣。
[如果你想让它变得真正有趣,请尝试对线程进行时间切片,以防它们进入深度计算,而不会偶尔导致线程饥饿。现在你肯定已经拯救了整个国家。我设法让 PARLANSE 在没有保存寄存器的情况下实现生成,但有时间切片保存/恢复完整寄存器状态,方法是在时间片上保存完整状态,并在将控制权传递给之前在一个特殊位置继续重新填充所有寄存器时间切片的 PC 位置]。