由于MPI-3具有共享内存并行的功能,而且它似乎与我的应用程序完全匹配,所以我正在认真考虑将我的混合OpemMP代码重写为一个纯MPI实现。
为了把最后一颗钉子钉进棺材,我决定运行一个小程序来测试OpenMP叉/连接机制的延迟。下面是代码(为Intel编译器编写的):
void action1(std::vector<double>& t1, std::vector<double>& t2)
{
#pragma omp parallel for schedule(static) num_threads(std::thread::hardware_concurrency())
for (auto index = std::size_t{}; index < t1.size(); ++index)
{
t1.data()[index] = std::sin(t2.data()[index]) * std::cos(t2.data()[index]);
}
}
void action2(std::vector<double>& t1, std::vector<double>& t2)
{
#pragma omp parallel for schedule(static) num_threads(std::thread::hardware_concurrency())
for (auto index = std::size_t{}; index < t1.size(); ++index)
{
t1.data()[index] = t2.data()[index] * std::sin(t2.data()[index]);
}
}
void action3(std::vector<double>& t1, std::vector<double>& t2)
{
#pragma omp parallel for schedule(static) num_threads(std::thread::hardware_concurrency())
for (auto index = std::size_t{}; index < t1.size(); ++index)
{
t1.data()[index] = t2.data()[index] * t2.data()[index];
}
}
void action4(std::vector<double>& t1, std::vector<double>& t2)
{
#pragma omp parallel for schedule(static) num_threads(std::thread::hardware_concurrency())
for (auto index = std::size_t{}; index < t1.size(); ++index)
{
t1.data()[index] = std::sqrt(t2.data()[index]);
}
}
void action5(std::vector<double>& t1, std::vector<double>& t2)
{
#pragma omp parallel for schedule(static) num_threads(std::thread::hardware_concurrency())
for (auto index = std::size_t{}; index < t1.size(); ++index)
{
t1.data()[index] = t2.data()[index] * 2.0;
}
}
void all_actions(std::vector<double>& t1, std::vector<double>& t2)
{
#pragma omp parallel for schedule(static) num_threads(std::thread::hardware_concurrency())
for (auto index = std::size_t{}; index < t1.size(); ++index)
{
t1.data()[index] = std::sin(t2.data()[index]) * std::cos(t2.data()[index]);
t1.data()[index] = t2.data()[index] * std::sin(t2.data()[index]);
t1.data()[index] = t2.data()[index] * t2.data()[index];
t1.data()[index] = std::sqrt(t2.data()[index]);
t1.data()[index] = t2.data()[index] * 2.0;
}
}
int main()
{
// decide the process parameters
const auto n = std::size_t{8000000};
const auto test_count = std::size_t{500};
// garbage data...
auto t1 = std::vector<double>(n);
auto t2 = std::vector<double>(n);
//+/////////////////
// perform actions one after the other
//+/////////////////
const auto sp = timer::spot_timer();
const auto dur1 = sp.duration_in_us();
for (auto index = std::size_t{}; index < test_count; ++index)
{
#pragma noinline
action1(t1, t2);
#pragma noinline
action2(t1, t2);
#pragma noinline
action3(t1, t2);
#pragma noinline
action4(t1, t2);
#pragma noinline
action5(t1, t2);
}
const auto dur2 = sp.duration_in_us();
//+/////////////////
// perform all actions at once
//+/////////////////
const auto dur3 = sp.duration_in_us();
for (auto index = std::size_t{}; index < test_count; ++index)
{
#pragma noinline
all_actions(t1, t2);
}
const auto dur4 = sp.duration_in_us();
const auto a = dur2 - dur1;
const auto b = dur4 - dur3;
if (a < b)
{
throw std::logic_error("negative_latency_error");
}
const auto fork_join_latency = (a - b) / (test_count * 4);
// report
std::cout << "Ran the program with " << omp_get_max_threads() << ", the calculated fork/join latency is: " << fork_join_latency << " us" << std::endl;
return 0;
}正如您所看到的,这个想法是分别执行一组操作(每个操作在一个OpenMP循环中),并计算该操作的平均持续时间,然后一起执行所有这些操作(在同一个OpenMP循环中),并计算该操作的平均持续时间。然后,我们在两个变量中建立了一个线性方程组,其中一个变量是叉/连接机制的延迟时间,可以通过求解得到这个值。
问题
我忽略了something?
编辑3
(
做了一个热身计算。
(
我改用“understandable.”(
我现在使用标志-O3运行以下代码:
void action1(std::vector<double>& t1)
{
#pragma omp parallel for schedule(static) num_threads(std::thread::hardware_concurrency())
for (auto index = std::size_t{}; index < t1.size(); ++index)
{
t1.data()[index] = std::sin(t1.data()[index]);
}
}
void action2(std::vector<double>& t1)
{
#pragma omp parallel for schedule(static) num_threads(std::thread::hardware_concurrency())
for (auto index = std::size_t{}; index < t1.size(); ++index)
{
t1.data()[index] = std::cos(t1.data()[index]);
}
}
void action3(std::vector<double>& t1)
{
#pragma omp parallel for schedule(static) num_threads(std::thread::hardware_concurrency())
for (auto index = std::size_t{}; index < t1.size(); ++index)
{
t1.data()[index] = std::atan(t1.data()[index]);
}
}
void all_actions(std::vector<double>& t1, std::vector<double>& t2, std::vector<double>& t3)
{
#pragma omp parallel for schedule(static) num_threads(std::thread::hardware_concurrency())
for (auto index = std::size_t{}; index < t1.size(); ++index)
{
#pragma optimize("", off)
t1.data()[index] = std::sin(t1.data()[index]);
t2.data()[index] = std::cos(t2.data()[index]);
t3.data()[index] = std::atan(t3.data()[index]);
#pragma optimize("", on)
}
}
int main()
{
// decide the process parameters
const auto n = std::size_t{1500000}; // 12 MB (way too big for any cache)
const auto experiment_count = std::size_t{1000};
// garbage data...
auto t1 = std::vector<double>(n);
auto t2 = std::vector<double>(n);
auto t3 = std::vector<double>(n);
auto t4 = std::vector<double>(n);
auto t5 = std::vector<double>(n);
auto t6 = std::vector<double>(n);
auto t7 = std::vector<double>(n);
auto t8 = std::vector<double>(n);
auto t9 = std::vector<double>(n);
//+/////////////////
// warum-up, initialization of threads etc.
//+/////////////////
for (auto index = std::size_t{}; index < experiment_count / 10; ++index)
{
all_actions(t1, t2, t3);
}
//+/////////////////
// perform actions (part A)
//+/////////////////
const auto dur1 = omp_get_wtime();
for (auto index = std::size_t{}; index < experiment_count; ++index)
{
action1(t4);
action2(t5);
action3(t6);
}
const auto dur2 = omp_get_wtime();
//+/////////////////
// perform all actions at once (part B)
//+/////////////////
const auto dur3 = omp_get_wtime();
#pragma nofusion
for (auto index = std::size_t{}; index < experiment_count; ++index)
{
all_actions(t7, t8, t9);
}
const auto dur4 = omp_get_wtime();
const auto a = dur2 - dur1;
const auto b = dur4 - dur3;
const auto fork_join_latency = (a - b) / (experiment_count * 2);
// report
std::cout << "Ran the program with " << omp_get_max_threads() << ", the calculated fork/join latency is: "
<< fork_join_latency * 1E+6 << " us" << std::endl;
return 0;
}这样,测量的延迟现在是115 us。现在令我费解的是,当动作发生变化时,这个值会发生变化。根据我的逻辑,因为我在A和B两个部分都做了相同的动作,所以实际上不应该有变化。为什么会发生这种情况?
发布于 2022-02-11 16:00:42
下面是我测量分叉连接开销的尝试:
#include <iostream>
#include <string>
#include <omp.h>
constexpr int n_warmup = 10'000;
constexpr int n_measurement = 100'000;
constexpr int n_spins = 1'000;
void spin() {
volatile bool flag = false;
for (int i = 0; i < n_spins; ++i) {
if (flag) {
break;
}
}
}
void bench_fork_join(int num_threads) {
omp_set_num_threads(num_threads);
// create threads, warmup
for (int i = 0; i < n_warmup; ++i) {
#pragma omp parallel
spin();
}
double const start = omp_get_wtime();
for (int i = 0; i < n_measurement; ++i) {
#pragma omp parallel
spin();
}
double const stop = omp_get_wtime();
double const ptime = (stop - start) * 1e6 / n_measurement;
// warmup
for (int i = 0; i < n_warmup; ++i) {
spin();
}
double const sstart = omp_get_wtime();
for (int i = 0; i < n_measurement; ++i) {
spin();
}
double const sstop = omp_get_wtime();
double const stime = (sstop - sstart) * 1e6 / n_measurement;
std::cout << ptime << " us\t- " << stime << " us\t= " << ptime - stime << " us\n";
}
int main(int argc, char **argv) {
auto const params = argc - 1;
std::cout << "parallel\t- sequential\t= overhead\n";
for (int j = 0; j < params; ++j) {
auto num_threads = std::stoi(argv[1 + j]);
std::cout << "---------------- num_threads = " << num_threads << " ----------------\n";
bench_fork_join(num_threads);
}
return 0;
}您可以使用多个不同的线程数来调用它,这不应该比机器上的内核数量更高,这样可以给出合理的结果。在我的机器上用6核,用gcc 11.2编译,我得到
$ g++ -fopenmp -O3 -DNDEBUG -o bench-omp-fork-join bench-omp-fork-join.cpp
$ ./bench-omp-fork-join 6 4 2 1
parallel - sequential = overhead
---------------- num_threads = 6 ----------------
1.51439 us - 0.273195 us = 1.24119 us
---------------- num_threads = 4 ----------------
1.24683 us - 0.276122 us = 0.970708 us
---------------- num_threads = 2 ----------------
1.10637 us - 0.270865 us = 0.835501 us
---------------- num_threads = 1 ----------------
0.708679 us - 0.269508 us = 0.439171 us在每一行中,第一个数字是线程的平均值(超过100'000次迭代),第二个数字是没有线程的平均值。最后一个数字是前两个之间的差额,应该是叉连接开销的上限。
确保中间列(没有线程)中的数字在每一行中大致相同,因为它们应该独立于线程数。如果没有,请确保计算机上没有其他运行,并/或增加测量和/或热身运行的次数。
关于将OpenMP交换为MPI,请记住,MPI仍然是多进程,而不是多线程。您可能会花费大量的内存开销,因为进程往往比线程大得多。
编辑:
修改后的基准使用不稳定的旗帜旋转而不是睡觉(谢谢@Jér me Richard)。正如Jér measured在他的回答中所提到的,n_spins的开销在增加。将n_spins设置在1000以下并不会显着地改变我的测量值,所以这就是我测量的地方。正如上面所看到的,所测量的开销比基准测试的早期版本要低得多。
睡眠的不精确是一个问题,特别是因为人们总是要测量睡眠时间最长的线程,因此会有偏向于更长的时间,即使睡眠时间本身会对称地分布在输入时间周围。
发布于 2022-02-11 16:49:13
TL;DR:由于动态频率缩放,核不能以完全相同的速度工作,而且有许多噪声会影响执行,导致昂贵的同步。您的基准测试主要度量这种同步开销。使用独特的并行部分应该可以解决这个问题。
基准存在相当大的缺陷。这段代码实际上并不测量OpenMP叉/连接部分的“延迟”。它衡量多种间接费用的组合,包括:
负载平衡和同步():拆分循环比大合并循环执行更频繁的同步(比大合并循环多5倍)。同步是昂贵的,不是因为通信开销,而是因为内核之间的实际同步本质上是不同步的。事实上,线程之间的轻微工作不平衡会导致其他线程等待最慢线程的完成。您可能认为不应该因为静态调度而发生这种情况,但是上下文切换和动态频率缩放会导致一些线程比其他线程慢。如果线程没有绑定到核心,或者某些程序是由操作系统在计算期间调度的,那么上下文切换就显得尤为重要。动态频率标度(例如。英特尔涡轮增压( Intel turbo )使得一些(一组线程)在工作负载、每个核心和整体封装的温度、活动核数、估计功耗等方面速度更快。的核心数目越多,同步开销越高,就越高。注意,这个开销取决于循环所花费的时间。欲了解更多信息,请阅读下面的分析。
循环分割的性能:将5个循环合并为唯一的循环会影响生成的汇编代码(因为所需指令较少),还会影响缓存中的加载/存储(因为内存访问模式有点不同)。更不用说理论上它可能会影响矢量化,尽管ICC没有将这一特定代码向量化。尽管如此,这似乎并不是我的机器上的主要实际问题,因为我无法通过按顺序运行程序来重现Clang的问题,而我可以使用许多线程。
要解决这个问题,可以使用唯一的并行部分。omp for循环必须使用nowait子句以避免引入同步。或者,与taskloop一起使用nogroup的基于任务的构造可以帮助实现相同的目标。在这两种情况下,您都应该小心依赖项,因为多个for-循环/任务库可以并行运行。这在你目前的代码中很好。
分析
分析了执行噪声(上下文切换、频率缩放、缓存效应、OS中断等)对短同步的影响。这是相当困难的,因为在您的情况下,同步过程中最慢的线程可能永远不是同一个线程(线程之间的工作相当平衡,但它们的速度并不完全相等)。
尽管如此,如果这个假设是真的,fork_join_latency应该依赖于n。因此,增加n也会增加fork_join_latency。这里我可以在我的6核i5-9600KF处理器上使用Clang 13 + IOMP (使用-fopenmp -O3):
n= 80'000 fork_join_latency<0.000001
n= 800'000 fork_join_latency=0.000036
n= 8'000'000 fork_join_latency=0.000288
n=80'000'000 fork_join_latency=0.003236请注意,fork_join_latency时间在实践中并不十分稳定,但其行为非常明显:所测量的开销是依赖于n的。
一个更好的解决方案是通过测量每个线程的循环时间来测量同步时间,并累积最小和最大时间之间的差异。下面是一个代码示例:
double totalSyncTime = 0.0;
void action1(std::vector<double>& t1)
{
constexpr int threadCount = 6;
double timePerThread[threadCount] = {0};
#pragma omp parallel
{
const double start = omp_get_wtime();
#pragma omp for nowait schedule(static) //num_threads(std::thread::hardware_concurrency())
#pragma nounroll
for (auto index = std::size_t{}; index < t1.size(); ++index)
{
t1.data()[index] = std::sin(t1.data()[index]);
}
const double stop = omp_get_wtime();
const double threadLoopTime = (stop - start);
timePerThread[omp_get_thread_num()] = threadLoopTime;
}
const double mini = *std::min_element(timePerThread, timePerThread+threadCount);
const double maxi = *std::max_element(timePerThread, timePerThread+threadCount);
const double syncTime = maxi - mini;
totalSyncTime += syncTime;
}然后,您可以像对totalSyncTime一样对fork_join_latency进行除法,并打印结果。我在0.000284中使用了fork_join_latency=0.000398 (使用n=8'000'000),它几乎证明了开销的很大一部分是由于同步,尤其是由于线程执行速度略有不同。请注意,此开销不包括OpenMP并行部分末尾的隐式屏障。
发布于 2022-04-15 15:34:08
见我对一个相关问题的回答:https://stackoverflow.com/a/71812329/2044454
TLDR:我将10k并行循环分解为平行区域外的x,以及内部10k/x。结论是,启动一个平行区域的成本基本上是拉链的。
https://stackoverflow.com/questions/71077917
复制相似问题