我正在尝试使用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()
线程安全吗?我无法从文档中找到答案。此外,还有更好的方法来解决这个问题吗?也许我应该使用低级调度程序调用?
(我输入的代码没有编译,所以请原谅错别字。)
答案 0 :(得分:3)
我非常确定tbb::task_group::run()
是线程安全的。我无法在文档中找到提及,这是非常令人惊讶的。
然而,
task_group
的基本实现,其run()
方法明确指出是线程安全的。目前的实施非常相似。tbb::task_group
的测试代码(在src/test/test_task_group.cpp
中)附带了一个测试,用于测试task_group
的线程安全性(它产生了一堆线程,每个线程调用{同一run()
个对象上的{1}}千倍或更多。task_group
示例代码(在sudoku
中)也从递归函数中的多个线程调用examples/task_group/sudoku/sudoku.cpp
,基本上与您提出的代码相同。task_group::run
是TBB与Microsoft的PPL共享的功能之一,其task_group
为thread-safe。虽然TBB文档提醒说TBB和PPL版本之间的行为仍然存在差异,但如果线程安全(因此需要外部同步)这些基本内容不同,那将是非常令人惊讶的。task_group
(描述为"与tbb::structured_task_group
一样,但只有一部分功能")明确限制"方法task_group
,run
,run_and_wait
和cancel
只能由创建wait
"。答案 1 :(得分:3)
这里确实有两个问题:
通常最好从task_group中生成少量任务。如果使用递归并行,请为每个级别提供自己的task_group。虽然性能可能不会比使用parallel_invoke更好。
低级tbb :: task接口是最好的选择。您甚至可以使用tasK :: execute返回指向尾调用任务的指针来编写尾递归代码。
但我有点担心空转线程。我想知道是否有足够的工作来保持线程繁忙。考虑先做work-span analysis。如果您使用的是英特尔编译器(或gcc 4.9),您可以先尝试使用Cilk版本。如果这不会加速,那么即使是低级别的tbb :: task接口也不太可能有所帮助,需要检查更高级别的问题(工作和跨度)。
答案 2 :(得分:0)
您也可以按如下方式实现:
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
很多机会返回值或影响递归。如果您需要,您应该编辑您的问题,以便提及“在这里工作”会对整体计算产生什么样的影响。