执行策略之间的区别以及何时使用它们
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】:seq
和par
/par_unseq
有什么区别?
std::for_each(std::execution::seq, std::begin(v), std::end(v), function_call);
std::execution::seq
代表顺序执行。如果您根本不指定执行策略,则它是默认值。它将强制实现按顺序执行所有函数调用。也保证一切都由调用线程执行。
相比之下,std::execution::par
和 std::execution::par_unseq
意味着并行执行。这意味着您保证可以安全地并行执行给定函数的所有调用,而不会违反任何数据依赖关系。允许实现使用并行实现,但并非强制这样做。
par
和par_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_unseq
比par
有什么优势?
实现可以在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_unseq
比seq
提高性能。
什么时候我应该更喜欢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 之间的确切区别是啥?以及何时专门使用它们?