【问题标题】:OpenMP first kernel much slower than the second kernelOpenMP 第一个内核比第二个内核慢得多
【发布时间】:2019-03-20 23:11:09
【问题描述】:

我初始化了一个巨大的 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 reproducible example。特别是,如何您是如何初始化数组的?
  • @EOF 我添加了示例。但是我没有看到数组的初始化方式究竟是如何影响执行结果的

标签: c++ c multithreading parallel-processing openmp


【解决方案1】:

由于您没有提供任何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% 的元素,这是第一次,操作系统在那里进行实际的内存分配,这需要一些重要的时间。

现在,对于您的第二个缩减循环,所有内存都已正确分配和访问,这使其速度更快。

所以这就是我相信发生的事情。也就是说,许多其他参数可以在这里发挥作用,例如:

  • 交换:您尝试分配的数组代表大约 36GB 的内存。如果您的机器没有那么多可用内存,那么您的代码可能会交换,这可能会使您提出的任何性能测量结果变得一团糟
  • NUMA 效应:如果您的机器有多个 NUMA 节点,那么如果管理不当,线程锁定和内存亲和性会对循环发生之间的性能产生很大影响
  • 编译器优化:您没有提及您使用的编译器以及您要求的优化级别。根据这一点,您会惊讶于您的代码会变得多么缩短。例如,编译器可以完全删除第二个循环,因为它与第一个循环执行相同的操作,并且由于结果相同而变得无用......以及许多其他有趣和意想不到的事情,这些事情会使您的基准测试毫无意义

【讨论】:

  • 有符号整数溢出是未定义的行为,因此编译器可以完全删除代码。
  • @EOF 好点。我添加了一个提及它的注释(尽管我仍然非常确信事情就像我描述的那样)
  • 看来编译器优化是问题发生的原因。由于某种原因,编译器决定将归约操作更改为原子添加。这一切都不同。
猜你喜欢
  • 2016-09-10
  • 2018-07-29
  • 1970-01-01
  • 1970-01-01
  • 2010-10-25
  • 1970-01-01
  • 2020-12-03
  • 2015-05-11
  • 2022-11-02
相关资源
最近更新 更多