执行策略之间的区别以及何时使用它们
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
对这些功能有什么影响?
有什么区别:
std::execution::seq
std::execution::par
std::execution::par_unseq
什么时候使用其中之一?
seq
表示 "execute sequentially" 并且与没有执行策略的版本完全相同。
par
表示 "execute in parallel",它允许实现在多个线程上并行执行。您有责任确保 f
.
内不会发生数据竞争
par_unseq
表示除了允许在多线程中执行外,该实现还允许在单个线程内交错执行各个循环迭代,即加载多个元素并在f
上执行所有这些都只是在之后。这是允许矢量化实施所必需的。
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> 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_unseq
比par
有什么优势?
实施可以在 par_unseq
模式下使用的额外优化包括矢量化执行和跨线程工作迁移(如果任务并行性与父级窃取调度程序一起使用,则后者是相关的)。
如果允许矢量化,实现可以在内部使用 SIMD 并行性(单指令、多数据)。例如,OpenMP 通过 #pragma omp simd
annotations 支持它,这可以帮助编译器生成更好的代码。
我应该什么时候选择std::execution::seq
?
- 正确性(避免数据竞争)
- 避免并行开销(启动成本和同步)
- 简单(调试)
数据依赖性强制顺序执行的情况并不少见。换句话说,如果并行执行会增加数据竞争,则使用顺序执行。
为并行执行重写和调整代码并不总是微不足道的。除非它是您应用程序的关键部分,否则您可以从顺序版本开始,然后再进行优化。如果您在需要保守资源使用的共享环境中执行代码,您可能还希望避免并行执行。
并行也不是免费的。如果循环的预期总执行时间非常短,即使从纯粹的性能角度来看,顺序执行也很可能是最好的。数据越大,每个计算步骤的成本越高,同步开销就越不重要。
例如,在上面的例子中使用并行是没有意义的,因为向量只包含三个元素并且操作非常便宜。另请注意,原始版本 - 在引入互斥锁或原子之前 - 不包含同步开销。测量并行算法加速比的一个常见错误是使用一个 CPU 上的并行版本 运行 作为基线。相反,您应该始终与没有同步开销的优化顺序实现进行比较。
我应该什么时候选择std::execution::par_unseq
?
首先,确保它不会牺牲正确性:
- 如果不同线程并行执行步骤时存在数据竞争,
par_unseq
不是一个选项。
- 如果代码是 向量化不安全,例如,因为它获取了一个锁,
par_unseq
不是一个选项(但 par
可能是).
否则,如果它是性能关键部分,请使用 par_unseq
,并且 par_unseq
比 seq
提高了性能。
我应该什么时候选择std::execution::par
?
如果这些步骤可以安全地并行执行,但你不能使用 par_unseq
因为它 向量化不安全 ,它是 [=16= 的候选者].
喜欢par_unseq
,验证它是性能关键部分并且par
是对seq
的性能改进。
来源:
我注意到 <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
什么时候使用其中之一?
seq
表示 "execute sequentially" 并且与没有执行策略的版本完全相同。
par
表示 "execute in parallel",它允许实现在多个线程上并行执行。您有责任确保 f
.
par_unseq
表示除了允许在多线程中执行外,该实现还允许在单个线程内交错执行各个循环迭代,即加载多个元素并在f
上执行所有这些都只是在之后。这是允许矢量化实施所必需的。
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> 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_unseq
比par
有什么优势?
实施可以在 par_unseq
模式下使用的额外优化包括矢量化执行和跨线程工作迁移(如果任务并行性与父级窃取调度程序一起使用,则后者是相关的)。
如果允许矢量化,实现可以在内部使用 SIMD 并行性(单指令、多数据)。例如,OpenMP 通过 #pragma omp simd
annotations 支持它,这可以帮助编译器生成更好的代码。
我应该什么时候选择std::execution::seq
?
- 正确性(避免数据竞争)
- 避免并行开销(启动成本和同步)
- 简单(调试)
数据依赖性强制顺序执行的情况并不少见。换句话说,如果并行执行会增加数据竞争,则使用顺序执行。
为并行执行重写和调整代码并不总是微不足道的。除非它是您应用程序的关键部分,否则您可以从顺序版本开始,然后再进行优化。如果您在需要保守资源使用的共享环境中执行代码,您可能还希望避免并行执行。
并行也不是免费的。如果循环的预期总执行时间非常短,即使从纯粹的性能角度来看,顺序执行也很可能是最好的。数据越大,每个计算步骤的成本越高,同步开销就越不重要。
例如,在上面的例子中使用并行是没有意义的,因为向量只包含三个元素并且操作非常便宜。另请注意,原始版本 - 在引入互斥锁或原子之前 - 不包含同步开销。测量并行算法加速比的一个常见错误是使用一个 CPU 上的并行版本 运行 作为基线。相反,您应该始终与没有同步开销的优化顺序实现进行比较。
我应该什么时候选择std::execution::par_unseq
?
首先,确保它不会牺牲正确性:
- 如果不同线程并行执行步骤时存在数据竞争,
par_unseq
不是一个选项。 - 如果代码是 向量化不安全,例如,因为它获取了一个锁,
par_unseq
不是一个选项(但par
可能是).
否则,如果它是性能关键部分,请使用 par_unseq
,并且 par_unseq
比 seq
提高了性能。
我应该什么时候选择std::execution::par
?
如果这些步骤可以安全地并行执行,但你不能使用 par_unseq
因为它 向量化不安全 ,它是 [=16= 的候选者].
喜欢par_unseq
,验证它是性能关键部分并且par
是对seq
的性能改进。
来源: