【问题标题】:What limits scaling in this simple OpenMP program?在这个简单的 OpenMP 程序中,是什么限制了扩展?
【发布时间】:2013-11-15 19:53:53
【问题描述】:

我试图了解 48 核系统(4xAMD Opteron 6348、2.8 Ghz、每个 CPU 12 个内核)上并行化的限制。我编写了这个微小的 OpenMP 代码来测试我认为可能是最好的情况下的加速(任务是令人尴尬的并行):

// Compile with: gcc scaling.c -std=c99 -fopenmp -O3                                                                                               

#include <stdio.h>
#include <stdint.h>

int main(){

  const uint64_t umin=1;
  const uint64_t umax=10000000000LL;
  double sum=0.;
#pragma omp parallel for reduction(+:sum)
  for(uint64_t u=umin; u<umax; u++)
    sum+=1./u/u;
  printf("%e\n", sum);

}

我惊讶地发现缩放是高度非线性的。 48线程代码运行大约需要2.9s,36线程运行3.1s,24线程运行3.7s,12线程运行4.9s,1线程运行代码需要57s。

不幸的是,我不得不说计算机上运行的一个进程使用 100% 的一个内核,所以这可能会影响它。这不是我的过程,所以我无法结束它来测试差异,但不知何故,我怀疑这是否会在 19~20 倍加速和理想的 48 倍加速之间产生差异。

为了确保这不是 OpenMP 问题,我同时运行了程序的两个副本,每个副本有 24 个线程(一个具有 umin=1,umax=5000000000,另一个具有 umin=5000000000,umax= 10000000000)。在这种情况下,程序的两个副本都在 2.9 秒后完成,因此它与使用单个程序实例运行 48 个线程完全相同。

是什么阻止了这个简单程序的线性缩放?

标签: c multithreading performance openmp smp


【解决方案1】:

我不确定这是否符合答案,但感觉不仅仅是评论,所以我们开始吧。

在我的任何项目中,我从未注意到与线程数相比具有特别线性的性能。一方面,在我看来,调度程序绝不是严格公平的。 OpenMP 一开始可能会在其线程组中平均分配任务,然后加入每个线程。在我有幸使用过的每个 Linux 机器上,我都希望有几个线程会提前完成,而几个线程会滞后。其他平台会有所不同。然而,这行得通,当然你在等待最慢的赶上。所以随机地说,线程处理的脉冲以钟形曲线的形式进行,线程越多,我认为越宽,直到后缘越过终点线,你永远不会完成。

top 说什么?它是否告诉您您的进程在 20 个线程时获得 2000% CPU,在 40 个线程时获得 4000%?我敢打赌它会逐渐减少。顺便说一下,htop 通常会显示一个进程总数,并为每个线程显示单独的行。这可能会很有趣。

使用这样的小循环,您可能不会遇到缓存抖动或任何此类烦恼。但另一个问题必然会降低一些性能:就像任何现代多核 CPU 一样,Opteron 在凉爽时以更高的时钟频率运行。你加热的核心越多,你看到的涡轮模式就越少。

【讨论】:

  • 我应该补充一点,内核与线程一样多,人们希望调度产生的影响最小。事情是我不确定最小是多少。根据我的经验,最有趣的多线程程序的线程数多于内核数。不过,我预计会有一些马虎。使用显式处理器亲和性进行实验可能会很有趣。
  • OpenMP 的 GNU 实现在每个并行区域的开头放置了一个对接屏障,因此团队中的所有线程或多或少同步启动。 GOMP 中的默认调度是static,即工作是均匀分布的,每个线程从一个连续的子集中执行迭代。 +1 关于核心频率提升的评论。
  • @DouglasB.Staple,您的代码使用静态调度,因此由于操作系统抖动导致的负载不平衡非常重要。请记住,团队中的所有线程都由每个并行区域末尾的隐式屏障同步,因此最慢的线程决定了该区域的总执行时间。一个核心保持忙碌这一事实意味着核心分时共享并执行持续的上下文切换(也可能是线程迁移)。
  • @HristoIliev 你说得对,动态调度显着加快了这个小测试代码的速度。当我有机会使用未加载的系统时,我将不得不重新测试。在我正在开发的实际代码中,我基本上必须使用动态调度,但问题可能是调度的开销,或者这里的任何其他答案。
  • @redrum,进程调度程序非常愚蠢。默认设置是尝试使所有逻辑 CPU 保持同样繁忙。如果没有绑定,一些线程将与其他进程分时共享,并且进程之间的上下文切换非常昂贵,因为 TLB 已刷新。这个问题不取决于套接字的数量——拥有多个套接字只会使情况变得更糟,因为套接字间迁移的成本更高。
【解决方案2】:

我有两个重要的观点,即为什么你的结果不是线性的。第一个是关于英特尔超线程和 AMD 模块的。下一篇是关于 Intel 和 AMD 的睿频模式

1.) 超线程和 AMD 模块/内核

太多人将模块中的英特尔超线程和 AMD 内核混淆为真正的内核,并期望线性加速。具有超线程的英特尔处理器可以运行两倍于内核的超线程/硬件线程。 AMD 还拥有自己的技术,其中基本单元称为模块,每个模块都有 AMD 不诚实地称之为核心 What's a module, what's a core。这很容易混淆的一个原因是,例如,在具有超线程的 Windows 中使用任务管理器,它会显示硬件线程的数量,但它会显示 CPU。这是一种误导,因为它不是 CPU 内核的数量。

我没有足够的 AMD 知识来详细介绍,但据我了解,每个模块都有一个浮点单元(但有两个整数单元)。因此,对于浮点运算,您不能真正期望超过 Intel 内核或 AMD 模块数量的线性加速。

在您的情况下,Opteron 6348 每个处理器有 2 个芯片,每个芯片有 3 个模块,每个模块作为 2 个“内核”。尽管这提供了 12 个内核,但实际上只有 6 个浮点单元。

我在我的单插槽 Intel Xeon E5-1620 @ 3.6 GHz 上运行了您的代码。它有 4 个内核和超线程(所以有 8 个硬件线程)。我明白了:

1 threads: 156s 
4 threads: 37s  (156/4 = 39s)
8 threads: 30s  (156/8 = 19.5s)

请注意,对于 4 个线程,缩放几乎是线性的,但对于 8 个线程,超线程只提供了一点帮助(至少它有所帮助)。另一个奇怪的观察是我的单线程结果比你的低很多(MSVC2013 64位发布模式)。我希望更快的单线程常春藤桥核心轻松胜过较慢的 AMD 桩驱动核心。这对我来说没有意义。

2.) Intel Turbo Boost 和 AMD Turbo Core。

英特尔有一项称为 Turbo Boost 的技术,它可以根据正在运行的线程数改变时钟频率。当所有线程都在运行时,涡轮增压处于最低值。在 Linux 上,我知道的唯一可以在操作运行时测量这一点的应用程序是 powertop。获得真正的运行频率并不是一件容易测量的事情(因为它需要 root 访问权限)。在 Windows 上,您可以使用 CPUz。无论如何,结果是,与运行最大数量的真实内核相比,仅运行一个线程时您不能期望线性扩展。

再说一次,我对 AMD 处理器几乎没有经验,但据我所知,他们的技术称为 Turbo Core,我希望效果类似。这就是在比较线程代码时,一个好的基准测试会禁用涡轮频率模式(如果可以的话,在 BIOS 中)的原因。

【讨论】:

  • 您的两个 cmets 都是正确且有用的,但 HristoIliev 对另一个答案的 cmets 让我质疑这种特定情况下的限制因素。今晚有空的时候我得再看一遍。
  • 检查 MSVC 的程序集输出(项目设置中有保存程序集文件的选项)。 GCC 将代码编译成一个非常紧凑的循环,使用CVTSI2SD 简单地将RDX(保持u)转换为XMM2,将一个常量1.0 从内存(使用RIP 相对寻址)加载到XMM1 中,使用两次DIVSD XMM1,XMM2执行除法,将结果添加到 XMM0 并再次循环。迭代空间的上限保存在 RCX 中。减少阶段是使用紧密互斥循环实现的。没有花哨的矢量化东西 - 只是简单的标量 SSE 算法。
  • @DouglasB.Staple,是的,Hristo 的 cmets 很有趣。我认为我的评论至少可以解释为什么你不应该期望在 24 个线程之后进行线性缩放。但是动态调度更好的事实是非常奇怪的。动态调度的开销很大,而且您所做的工作负载均匀,因此静态调度通常是正确的选择。
  • 如果块大小足够大,动态调度的开销将大大减少(可以忽略不计?)。我用了schedule(dynamic,1000000),意思是只有10000块。
  • 我认为动态调度在这里更快的原因是,对于静态调度,如果系统上根本没有运行任何其他进程,并且线程数与“核心”相同,则存在线程之间会出现严重的不平衡。 IE。无论哪个线程在任何时候与另一个进程共享一个核心,都会运行得更慢。这将需要在内核之间切换线程以平衡执行时间,从而产生 Hristo 所说的开销。
【解决方案3】:

我终于有机会使用完全卸载的系统对代码进行基准测试:

对于动态计划,我使用了schedule(dynamic,1000000)。对于静态计划,我使用默认值(在核心之间均匀)。对于线程绑定,我使用了export GOMP_CPU_AFFINITY="0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47"

此代码高度非线性扩展的主要原因是 AMD 所谓的“核心”实际上并不是独立的核心。这是redrum答案的第(1)部分。从 24 个线程的突然加速平台可以清楚地看出这一点;动态调度非常明显。从我选择的线程绑定中也很明显:事实证明,我上面写的绑定将是一个糟糕的选择,因为你最终在每个“模块”中都有两个线程。

第二大减速来自具有大量线程的静态调度。不可避免地,最慢和最快的线程之间存在不平衡,当使用默认的静态调度将迭代分成大块时,会在运行时间中引入很大的波动。这部分答案来自 Hristo 的 cmets 和 Salt 的答案。

我不知道为什么“涡轮增压”的效果不明显(Redrum 回答的第 2 部分)。此外,我不能 100% 确定缩放的最后一点在哪里(可能是开销)丢失了(我们得到了 22 倍的性能,而不是 模块 数量的线性缩放预期的 24 倍)。但除此之外,这个问题得到了很好的回答。

【讨论】:

  • 很高兴看到您跟进此事。这些都是有趣的结果。你说一个CPU的一个核心总是100%。因此,我认为最大值是 23 倍(忽略频率提升)。
  • @Zboson 谢谢。我可能还要仔细看看,找出剩余的损失在哪里。当我第一次发布问题时,有一个 100% 的进程,但几天后(当我发布此答案时)系统或多或少完全空闲。
猜你喜欢
  • 2016-11-08
  • 1970-01-01
  • 1970-01-01
  • 2021-08-17
  • 1970-01-01
  • 1970-01-01
  • 2010-09-21
  • 2018-07-17
  • 1970-01-01
相关资源
最近更新 更多