执行策略之间的区别以及何时使用它们

Posted

技术标签:

【中文标题】执行策略之间的区别以及何时使用它们【英文标题】:Difference between execution policies and when to use them 【发布时间】:2016-10-10 08:59:01 【问题描述】:

我注意到<algorithm> 中的大多数(如果不是全部)函数正在获得一个或多个额外的重载。所有这些额外的重载都添加了一个特定的新参数,例如,std::for_each 来自:

template< class InputIt, class UnaryFunction >
UnaryFunction for_each( InputIt first, InputIt last, UnaryFunction f );

到:

template< class ExecutionPolicy, class InputIt, class UnaryFunction2 >
void for_each( ExecutionPolicy&& policy, InputIt first, InputIt last, UnaryFunction2 f );

这个额外的ExecutionPolicy对这些功能有什么影响?

两者有什么区别:

std::execution::seq std::execution::par std::execution::par_unseq

什么时候使用其中一个?

【问题讨论】:

还有en.cppreference.com/w/cpp/algorithm/execution_policy_tag_t有每个的描述 @SingerOfTheFall 我怎么会错过这个......不过,它似乎没有给出好的代码示例。我想那会及时到来的。 @Gill Bates - 不过,使用 ::par 并没有让生活变得更轻松:“使用并行执行策略时,程序员有责任避免死锁” 【参考方案1】:

seqpar/par_unseq有什么区别?

std::for_each(std::execution::seq, std::begin(v), std::end(v), function_call);

std::execution::seq 代表顺序执行。如果您根本不指定执行策略,则它是默认值。它将强制实现按顺序执行所有函数调用。也保证一切都由调用线程执行。

相比之下,std::execution::parstd::execution::par_unseq 意味着并行执行。这意味着您保证可以安全地并行执行给定函数的所有调用,而不会违反任何数据依赖关系。允许实现使用并行实现,但并非强制这样做。

parpar_unseq有什么区别?

par_unseq 需要比par 更强的保证,但允许额外的优化。具体来说,par_unseq 需要在同一线程中交错执行多个函数调用的选项。

让我们用一个例子来说明区别。假设你想并行化这个循环:

std::vector<int> v =  1, 2, 3 ;
int sum = 0;
std::for_each(std::execution::seq, std::begin(v), std::end(v), [&](int i) 
  sum += i*i;
);

您不能直接并行化上面的代码,因为它会引入sum 变量的数据依赖关系。为避免这种情况,您可以引入锁:

int sum = 0;
std::mutex m;
std::for_each(std::execution::par, std::begin(v), std::end(v), [&](int i) 
  std::lock_guard<std::mutex> lockm;
  sum += i*i;
);

现在所有函数调用都可以安全地并行执行,切换到par时代码不会中断。但是如果你改用par_unseq 会发生什么,一个线程可能会执行多个函数调用,而不是按顺序执行,而是并发执行?

它可能导致死锁,例如,如果代码像这样重新排序:

 m.lock();    // iteration 1 (constructor of std::lock_guard)
 m.lock();    // iteration 2
 sum += ...;  // iteration 1
 sum += ...;  // iteration 2
 m.unlock();  // iteration 1 (destructor of std::lock_guard)
 m.unlock();  // iteration 2

在标准中,术语是vectorization-unsafe。引用P0024R2:

如果一个标准库函数被指定为与另一个函数调用同步,或者另一个函数调用被指定为与之同步,并且它不是内存分配或释放函数,则它是向量化不安全的。从parallel_vector_execution_policy算法调用的用户代码不能调用向量化不安全的标准库函数。

使上述矢量化代码安全的一种方法是用原子替换互斥锁:

std::atomic<int> sum0;
std::for_each(std::execution::par_unseq, std::begin(v), std::end(v), [&](int i) 
  sum.fetch_add(i*i, std::memory_order_relaxed);
);

使用par_unseqpar 有什么优势?

实现可以在par_unseq 模式下使用的其他优化包括向量化执行和跨线程的工作迁移(如果任务并行性与父窃取调度程序一起使用,则后者是相关的)。

如果允许矢量化,实现可以在内部使用 SIMD 并行(单指令,多数据)。例如,OpenMP 通过#pragma omp simd annotations 支持它,这可以帮助编译器生成更好的代码。

什么时候我应该更喜欢std::execution::seq

    正确性(避免数据竞争) 避免并行开销(启动成本和同步) 简单(调试)

数据依赖会强制执行顺序执行的情况并不少见。换句话说,如果并行执行会增加数据竞争,请使用顺序执行。

为并行执行重写和调整代码并不总是微不足道的。除非它是您应用程序的关键部分,否则您可以从顺序版本开始,然后再进行优化。如果您在需要保守资源使用的共享环境中执行代码,您可能还希望避免并行执行。

并行性也不是免费的。如果循环的预期总执行时间非常短,即使从纯粹的性能角度来看,顺序执行也很可能是最好的。数据越大,每个计算步骤的成本越高,同步开销就越不重要。

例如,在上面的例子中使用并行是没有意义的,因为向量只包含三个元素并且操作非常便宜。另请注意,原始版本 - 在引入互斥锁或原子之前 - 不包含同步开销。衡量并行算法加速的一个常见错误是使用在一个 CPU 上运行的并行版本作为基线。相反,您应该始终与没有同步开销的优化顺序实现进行比较。

什么时候我应该更喜欢std::execution::par_unseq

首先,确保它不会牺牲正确性:

如果在不同线程并行执行步骤时出现数据争用,par_unseq 不是一个选项。 如果代码是vectorization-unsafe,例如,因为它获得了一个锁,par_unseq 不是一个选项(但par 可能是)。

否则,如果它是性能关键部分,则使用par_unseq,并且par_unseqseq 提高性能。

什么时候我应该更喜欢std::execution::par

如果这些步骤可以安全地并行执行,但你不能使用par_unseq,因为它是vectorization-unsafe,它是par的候选者。

par_unseq 一样,验证它是性能关键部分,并且par 是对seq 的性能改进。

来源:

cppreference.com (execution policy) P0024R2: The Parallelism TS Should be Standardized

【讨论】:

非常感谢这个答案的清晰、简洁和完整!泰菲利普【参考方案2】:

seq 表示“顺序执行”,与没有执行策略的版本完全相同。

par 表示“并行执行”,它允许实现在多个线程上并行执行。您有责任确保f 内不会发生数据竞争。

par_unseq 意味着除了允许在多个线程中执行之外,还允许实现在单个线程中交错各个循环迭代,即加载多个元素并仅在之后对所有元素执行f。这是允许矢量化实现所必需的。

【讨论】:

在一次 Cppcon16 谈话中提到,seq 策略的行为类似于 par,但只是在调用线程中,以使调试更容易,并且它与算法不同没有执行策略。我试图找到参考。 @Serthy 我相信你可能会想到 Bryce Lelbach 在 2016 年的 Boostcon 上谈论 C++17 中的语言和库变化。演示幻灯片的视频 (YouTube) 和 PDF (Github) 都在线(没有关于用于调试的 seq 的解释)。 你确定seq和省略策略完全一样吗?我认为标准不能保证这一点。相反,en.cppreference.com/w/cpp/algorithm/execution_policy_tag_t 指出此策略会导致元素访问函数的顺序不确定。

以上是关于执行策略之间的区别以及何时使用它们的主要内容,如果未能解决你的问题,请参考以下文章

appbar、toolbar、actionbar 之间的确切区别是啥?以及何时专门使用它们?

策略模式

javascript中self和this之间的区别以及何时使用它们中的任何一个[重复]

简单工厂模式与策略模式的优缺点以及它们的区别

设计模式策略模式责任链以及装饰器之间的区别

Grunt、Gulp.js 和 Bower 有啥区别?为啥以及何时使用它们?