[英]How to multithread “tail call” recursion using TBB
我試圖使用tbb多線程現有的遞歸算法。 單線程版本使用尾調用遞歸,從結構上看,它看起來像這樣:
void my_func() {
my_recusive_func (0);
}
bool doSomeWork (int i, int& a, int& b, int& c) {
// do some work
}
void my_recusive_func (int i) {
int a, b, c;
bool notDone = doSomeWork (i, a, b, c);
if (notDone) {
my_recusive_func (a);
my_recusive_func (b);
my_recusive_func (c);
}
}
我是tbb新手所以我的第一次嘗試使用了parallel_invoke函數:
void my_recusive_func (int i) {
int a, b, c;
bool notDone = doSomeWork (i, a, b, c);
if (notDone) {
tbb::parallel_invoke (
[a]{my_recusive_func (a);},
[b]{my_recusive_func (b);},
[c]{my_recusive_func (c);});
}
}
這確實有效,並且運行速度比單線程版本快,但它似乎不能很好地擴展核心數量。 我所針對的機器有16個內核(32個超線程),因此可擴展性對於這個項目來說非常重要,但是這個版本在該機器上最多只能獲得8倍的加速,並且許多內核在算法運行時似乎處於空閑狀態。
我的理論是tbb正在等待在parallel_invoke之后完成子任務,所以可能有許多任務閑置等待不必要? 這會解釋空閑核心嗎? 有沒有辦法讓父任務返回而不等待孩子? 我當時想的可能是這樣的,但我對調度程序還不了解,但還不知道這是否正常:
void my_func()
{
tbb::task_group g;
my_recusive_func (0, g);
g.wait();
}
void my_recusive_func (int i, tbb::task_group& g) {
int a, b, c;
bool notDone = doSomeWork (i, a, b, c);
if (notDone) {
g.run([a,&g]{my_recusive_func(a, g);});
g.run([b,&g]{my_recusive_func(b, g);});
my_recusive_func (c, g);
}
}
我的第一個問題是tbb::task_group::run()
線程安全嗎? 我無法從文檔中找到答案。 此外,還有更好的方法來解決這個問題嗎? 也許我應該使用低級調度程序調用?
(我輸入的代碼沒有編譯,所以請原諒錯別字。)
我很相信tbb::task_group::run()
是線程安全的。 我在文檔中找不到提及,這是相當令人驚訝的。
然而,
task_group
的原始實現,其run()
方法被明確指出是線程安全的。 目前的實施非常相似。 tbb::task_group
的測試代碼(在src/test/test_task_group.cpp
)帶有一個測試,用於測試task_group
的線程安全性(它產生一堆線程,每個線程調用run()
一千次或者更多關於同一task_group
對象)。 sudoku
示例代碼(在examples/task_group/sudoku/sudoku.cpp
)也從遞歸函數中的多個線程調用task_group::run
,基本上與您提出的代碼相同。 task_group
是TBB和Microsoft的PPL之間共享的功能之一,其task_group
是線程安全的 。 雖然TBB文檔提醒說TBB和PPL版本之間的行為仍然存在差異,但如果線程安全(因此需要外部同步)不同的話,那將是非常令人驚訝的。 tbb::structured_task_group
(描述為“類似於task_group
,但只有一部分功能”)具有明確的限制,即“方法run
, run_and_wait
, cancel
和wait
應僅由創建structured_task_group
的線程調用”。 這里有兩個問題:
通常最好從task_group中生成少量任務。 如果使用遞歸並行,請為每個級別提供自己的task_group。 雖然性能可能不會比使用parallel_invoke更好。
低級tbb :: task接口是最好的選擇。 您甚至可以使用tasK :: execute返回指向尾調用任務的指針的技巧來編寫尾遞歸。
但我有點擔心空轉線程。 我想知道是否有足夠的工作來保持線程繁忙。 首先考慮進行工作范圍分析 。 如果您使用的是英特爾編譯器(或gcc 4.9),您可以先嘗試使用Cilk版本。 如果這不會加速,那么即使是低級別的tbb :: task接口也不太可能有所幫助,需要檢查更高級別的問題(工作和跨度)。
您也可以按如下方式實現:
constexpr int END = 10;
constexpr int PARALLEL_LIMIT = END - 4;
static void do_work(int i, int j) {
printf("%d, %d\n", i, j);
}
static void serial_recusive_func(int i, int j) {
// DO WORK HERE
// ...
do_work(i,j);
if (i < END) {
serial_recusive_func(i+1, 0);
serial_recusive_func(i+1, 1);
serial_recusive_func(i+1, 2);
}
}
class RecursiveTask : public tbb::task {
int i;
int j;
public:
RecursiveTask(int i, int j) :
tbb::task(),
i(i), j(j)
{}
task *execute() override {
//DO WORK HERE
//...
do_work(i,j);
if (i >= END) return nullptr;
if (i < PARALLEL_LIMIT) {
auto &c = *new (allocate_continuation()) tbb::empty_task();
c.set_ref_count(3);
spawn(*new(c.allocate_child()) RecursiveTask(i+1, 0));
spawn(*new(c.allocate_child()) RecursiveTask(i+1, 1));
recycle_as_child_of(c);
i = i+1; j = 2;
return this;
} else {
serial_recusive_func(i+1, 0);
serial_recusive_func(i+1, 1);
serial_recusive_func(i+1, 2);
}
return nullptr;
}
};
static void my_func()
{
tbb::task::spawn_root_and_wait(
*new(tbb::task::allocate_root()) RecursiveTask(0, 0));
}
int main() {
my_func();
}
你的問題沒有包含很多關於“在這里工作”的信息,所以我的實現並沒有給do_work
很多機會來返回值或影響遞歸。 如果您需要,您應該編輯您的問題,以便提及“在這里工作”會對整體計算產生什么樣的影響。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.