parallel_for (Inter TBB) 上是不是存在类似于我们在 std::function 上看到的开销的开销?

Posted

技术标签:

【中文标题】parallel_for (Inter TBB) 上是不是存在类似于我们在 std::function 上看到的开销的开销?【英文标题】:Is there an overhead on parallel_for (Inter TBB) similar to the overhead we see on std::function?parallel_for (Inter TBB) 上是否存在类似于我们在 std::function 上看到的开销的开销? 【发布时间】:2013-09-01 01:02:53 【问题描述】:

在这个链接std::function vs template 中有一个很好的讨论关于std::function 的开销。基本上,为了避免传递给 std::function 构造函数的函子堆分配导致的 10 倍开销,必须使用 std::ref 或 std::cref。

取自@CassioNeri 答案的示例,该示例显示了如何通过引用将 lambdas 传递给 std::function。

float foo(std::function<float(float)> f)  return -1.0f * f(3.3f) + 666.0f; 
foo(std::cref([a,b,c](float arg) return arg * 0.5f; ));

现在,英特尔线程构建模块库让您能够并行评估循环 使用 lambda/functors,如下例所示。

示例代码:

#include "tbb/task_scheduler_init.h"
#include "tbb/blocked_range.h"
#include "tbb/parallel_for.h"
#include "tbb/tbb_thread.h"
#include <vector>

int main() 
 tbb::task_scheduler_init init(tbb::tbb_thread::hardware_concurrency());
 std::vector<double> a(1000);
 std::vector<double> c(1000);
 std::vector<double> b(1000);

 std::fill(b.begin(), b.end(), 1);
 std::fill(c.begin(), c.end(), 1);

 auto f = [&](const tbb::blocked_range<size_t>& r) 
  for(size_t j=r.begin(); j!=r.end(); ++j) a[j] = b[j] + c[j];    
 ;
 tbb::parallel_for(tbb::blocked_range<size_t>(0, 1000), f);
 return 0;

所以我的问题是:英特尔 TBB parallel_for 是否具有与我们在 std::function 上看到的相同类型的开销(函子的堆分配)?我是否应该使用 std::cref 通过引用 parallel_for 来传递我的仿函数/lambdas 来加速代码?

【问题讨论】:

@ArchD.Robison 不确定当前答案是否结束了这个问题。你似乎是回答这个问题的完美程序员。你能帮我吗 【参考方案1】:

我是否应该使用 std::cref 通过引用 parallel_for 来传递我的仿函数/lambdas 以加快代码速度?

我不知道你的主要问题的答案。但这没关系,因为您应该永远不要这样做 tbb::parallel_for

正如Cassio Neri 在他的回答中指出的那样:

最后,请注意 lambda 的生命周期包含了 std::function 的生命周期。

就他所问问题的情况而言,情况确实如此。但对于tbb::parallel_for,这不正确parallel_for重点是它会在未来的任意时间从其他线程调用给定的函数。

如果你通过引用给它一些仿函数,那么你必须确保这个仿函数的生命周期一直持续到parallel_for 完成。否则,parallel_for 可能会尝试调用对已销毁对象的引用。

这很糟糕。

因此,无论可能发生什么开销,您都无法通过引用来解决它。

【讨论】:

【参考方案2】:

使用 std::cref 传递函子可能会适得其反,但我不做任何承诺。只有在感兴趣的精确背景下进行经验测试才能确定。一般来说,对于 tbb::parallel_for,我的建议是:

按值传递 lambda。 除非存在规定捕获模式的语义考虑,否则通过引用获取 lambda 对象,除非它们是复制成本低的小对象。请记住,通常捕获的变量将比复制 lambda 的次数多得多。

TBB 是否为函子支付堆分配成本?对于 parallel_for(first,*last*,functor) 形式的签名,答案肯定是否定的,因为该形式通过引用传递函子。

对于parallel_for(range,*functor*) 形式的签名,如题,答案是“无额外费用”。它不直接堆分配函子。但是 TBB 创建的每个任务都有一个仿函数的副本,并且这些任务是堆分配的(通常通过本地空闲列表快速分配)。使用 std::cref 不会改变任务是堆分配的事实。使用 std::cref 只会增加一个额外的间接级别。

实际上,我有点惊讶,一种形式的 tbb::parallel_for 通过引用传递函子,另一种通过值传递。我忘记了原因,我相信 TBB 小组一定已经讨论过了。选择可能是由于在引入每个基准和机器时可用的任何基准和机器,或者它可能是“第一个,最后一个”形式的 PPL 兼容性问题,这似乎不需要仿函数是可复制构造的。如前所述,按引用传递与按值传递的性能权衡并不简单。通过引用传递使函子的传递变得便宜,但每次访问它都会增加间接成本(除非编译器可以优化它)。

关于函子参数的生命周期,它只需要在调用parallel_for期间存在。

【讨论】:

非常感谢您的回答:)

以上是关于parallel_for (Inter TBB) 上是不是存在类似于我们在 std::function 上看到的开销的开销?的主要内容,如果未能解决你的问题,请参考以下文章

tbb::concurrent_hash_map 抛出 SIGSEGV

将 TBB 与 SSE2 内在函数混合

tbb简介与使用

结合英特尔 IPP 和 TBB

在 parallel_for 中使用函数对象

并发中的 Pybind11 并行处理问题::parallel_for