OpenMP 第一个内核比第二个内核慢得多
OpenMP first kernel much slower than the second kernel
我有一个巨大的 98306 x 98306 二维数组已初始化。我创建了一个内核函数来计算低于某个阈值的元素总数。
#pragma omp parallel for reduction(+:num_below_threshold)
for(row)
for(col)
index = get_corresponding_index(row, col);
if (array[index] < threshold)
num_below_threshold++;
出于基准测试目的,我测量了当线程数设置为 1 时内核执行的执行时间。我注意到内核第一次执行大约需要 11 秒。下一次使用一个线程调用在同一个数组上执行的内核只用了大约 3 秒。我认为这可能是与缓存相关的问题,但它似乎并不相关。造成这种情况的可能原因是什么?
这个数组初始化为:
float *array = malloc(sizeof(float) * 98306 * 98306);
for (int i = 0; i < 98306 * 98306; i++) {
array[i] = rand() % 10;
}
同一个内核被应用到这个数组两次,第二次执行时间比第一个内核快得多。我虽然在 Linux 上进行了延迟分配,但由于初始化函数,这应该不是问题。任何解释都会有所帮助。谢谢!
由于您没有提供任何信息 Minimal, Complete and Verifiable Example,我将不得不在这里进行一些大胆的猜测,但我非常有信心我已经掌握了问题的要点。
首先,你必须注意到 98,306 x 98,306 是 9,664,069,636,这比带符号的 32 位整数可以存储的最大值(2,147,483,647)大得多。因此,你的 for
初始化循环的上限在溢出后可能会变成 1,074,135,044(在我的机器上,虽然严格来说这是未定义的行为,但任何事情都可能发生),这大约是你的 9 倍预期。
所以现在,在初始化循环之后,您认为分配的内存中只有 11% 实际上已被操作系统分配和触及。但是,您的第一个缩减循环在遍历数组的各个元素方面做得很好,因为对于其中的大约 89%,这是第一次, OS 在那里进行实际的内存分配,然后,这需要花费大量时间。
现在,对于您的第二个缩减循环,所有内存都已正确分配和使用,这使其速度更快。
这就是我认为发生的事情。也就是说,许多其他参数可以在这里发挥作用,例如:
- Swapping:您尝试分配的数组代表大约 36GB 的内存。如果你的机器没有那么多可用内存,那么你的代码可能会交换,这可能会使你能想出的任何性能测量变得一团糟
- NUMA 效果:如果您的机器有多个 NUMA 节点,那么线程固定和内存亲和性如果管理不当,会对循环发生之间的性能产生很大影响
- 编译器优化:您没有提到您使用的是哪个编译器以及您要求的优化级别。取决于此,您会惊讶于您的代码会变得多么短。例如,编译器可以完全删除第二个循环,因为它与第一个循环做同样的事情并且变得无用,因为结果将是相同的......还有许多其他有趣和意想不到的事情使你的基准测试变得毫无意义
我有一个巨大的 98306 x 98306 二维数组已初始化。我创建了一个内核函数来计算低于某个阈值的元素总数。
#pragma omp parallel for reduction(+:num_below_threshold)
for(row)
for(col)
index = get_corresponding_index(row, col);
if (array[index] < threshold)
num_below_threshold++;
出于基准测试目的,我测量了当线程数设置为 1 时内核执行的执行时间。我注意到内核第一次执行大约需要 11 秒。下一次使用一个线程调用在同一个数组上执行的内核只用了大约 3 秒。我认为这可能是与缓存相关的问题,但它似乎并不相关。造成这种情况的可能原因是什么?
这个数组初始化为:
float *array = malloc(sizeof(float) * 98306 * 98306);
for (int i = 0; i < 98306 * 98306; i++) {
array[i] = rand() % 10;
}
同一个内核被应用到这个数组两次,第二次执行时间比第一个内核快得多。我虽然在 Linux 上进行了延迟分配,但由于初始化函数,这应该不是问题。任何解释都会有所帮助。谢谢!
由于您没有提供任何信息 Minimal, Complete and Verifiable Example,我将不得不在这里进行一些大胆的猜测,但我非常有信心我已经掌握了问题的要点。
首先,你必须注意到 98,306 x 98,306 是 9,664,069,636,这比带符号的 32 位整数可以存储的最大值(2,147,483,647)大得多。因此,你的 for
初始化循环的上限在溢出后可能会变成 1,074,135,044(在我的机器上,虽然严格来说这是未定义的行为,但任何事情都可能发生),这大约是你的 9 倍预期。
所以现在,在初始化循环之后,您认为分配的内存中只有 11% 实际上已被操作系统分配和触及。但是,您的第一个缩减循环在遍历数组的各个元素方面做得很好,因为对于其中的大约 89%,这是第一次, OS 在那里进行实际的内存分配,然后,这需要花费大量时间。
现在,对于您的第二个缩减循环,所有内存都已正确分配和使用,这使其速度更快。
这就是我认为发生的事情。也就是说,许多其他参数可以在这里发挥作用,例如:
- Swapping:您尝试分配的数组代表大约 36GB 的内存。如果你的机器没有那么多可用内存,那么你的代码可能会交换,这可能会使你能想出的任何性能测量变得一团糟
- NUMA 效果:如果您的机器有多个 NUMA 节点,那么线程固定和内存亲和性如果管理不当,会对循环发生之间的性能产生很大影响
- 编译器优化:您没有提到您使用的是哪个编译器以及您要求的优化级别。取决于此,您会惊讶于您的代码会变得多么短。例如,编译器可以完全删除第二个循环,因为它与第一个循环做同样的事情并且变得无用,因为结果将是相同的......还有许多其他有趣和意想不到的事情使你的基准测试变得毫无意义