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

Difference between execution policies and when to use them

我注意到 <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 对这些功能有什么影响?

有什么区别:

什么时候使用其中之一?

seq 表示 "execute sequentially" 并且与没有执行策略的版本完全相同。

par 表示 "execute in parallel",它允许实现在多个线程上并行执行。您有责任确保 f.

内不会发生数据竞争

par_unseq表示除了允许在多线程中执行外,该实现还允许在单个线程内交错执行各个循环迭代,即加载多个元素并在f上执行所有这些都只是在之后。这是允许矢量化实施所必需的。

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> lock{m};
  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

在标准中,术语是向量化不安全。引用自P0024R2

A standard library function is vectorization-unsafe if it is specified to synchronize with another function invocation, or another function invocation is specified to synchronize with it, and if it is not a memory allocation or deallocation function. Vectorization-unsafe standard library functions may not be invoked by user code called from parallel_vector_execution_policy algorithms.

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

std::atomic<int> sum{0};
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

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

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

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

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

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

我应该什么时候选择std::execution::par_unseq

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

  • 如果不同线程并行执行步骤时存在数据竞争,par_unseq 不是一个选项。
  • 如果代码是 向量化不安全,例如,因为它获取了一个锁,par_unseq 不是一个选项(但 par 可能是).

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

我应该什么时候选择std::execution::par

如果这些步骤可以安全地并行执行,但你不能使用 par_unseq 因为它 向量化不安全 ,它是 [=16= 的候选者].

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

来源: