【问题标题】:Do single threaded programs execute in parallel in a CPU?单线程程序是否在 CPU 中并行执行?
【发布时间】:2019-11-16 18:09:41
【问题描述】:

在测量 Intel 第 4 代 i5 的 CPI(每条指令的周期数)时,我们得到 CPI

有同学说是并行执行的代码,但是是C语言的单线程代码,老师说现在的处理器是超标量的。

编译是用 gcc -m32 完成的。

假设编译器没有通过并行化代码来发挥作用。

但我的疑虑仍然存在。由于现在的处理器对代码做了一些小魔术,比如乱序执行和推测执行,我想知道:

  • 处理器是否在多核单线程程序中运行?

假设我们有这两条指令:

(1) 添加 %eax, (%eax) (1) addl %ebx, (%ebx)

Core-0 运行 (1) 和 Core-1 运行 (2)

【问题讨论】:

标签: performance parallel-processing cpu-architecture


【解决方案1】:

是的,CPU 在单个线程中发现指令级并行性,每个周期运行超过 1 条指令。具体示例见Why does re-initializing a register inside an unrolled ADD loop make it run faster even with more instructions inside the loop?

指令级并行性与线程级并行性无关(您在问题的第二部分提出)。运行单线程工作负载时,只有一个内核处于活动状态。

现代多核系统同时利用这两者,但你可以只拥有一个而没有另一个。

例如,Sun 的Niagara (UltraSPARC T1) 从一开始就设计为利用线程级并行性(和内存级并行性),但不尝试快速运行任何单个线程,例如某种服务器工作负载。它有 8 个物理 single-issue in-order cores 和 4 路 SMT(每个物理内核 4 个逻辑内核)而不是 OoO exec 来隐藏延迟(例如缓存未命中)。单线程性能很糟糕,但运行 32 个线程的最大吞吐量在 2005 年是不错的,因为它的功率预算和晶体管数量。

早期的 x86(如 Pentium III)是超标量单核。只有多socket 系统是 SMP。但是这样的 CPU 可以并且确实实现了 CPI

您的 i5 第 4 代 CPU 是 Haswell。 请参阅David Kanter's deep dive on Haswell's microarchitecture,包括每个内核内部各个阶段的宽度框图。

处理器是否在多核单线程程序中运行?

NO,单核本身就是超标量,例如在 Haswell 或 Zen 中有 4 个整数 ALU 执行单元。 (在 Intel CPU 上,3 个 SIMD ALU 执行单元与标量/通用整数 ALU 在相同的执行端口上。)和一个足够宽以匹配的前端。

一般来说,超标量 CPU 能够在每个时钟每个内核运行至少 2 条指令。

您问题中的这个错误猜测是 programmers.SE 上 How does a single thread run on multiple cores? 的重复。 回答:他们没有;每个核心都有一个宽广的前端和后端的多个执行单元。

在我们实现单核更宽的收益递减之前,我们一直这样做而不是构建多核 CPU;时间切片/先发制人的多任务处理通常已经足够好了。对于几乎所有事情,一个更快的内核都比 N 个 1/N 速度的内核要好。但是现在这不是权衡。它是 N 个 1/sqrt(N) 速度的核心或类似的东西。


  • 添加 %eax, (%eax)
  • 添加 %ebx, (%ebx)

这些内存目标添加指令每个需要超过 1 个周期才能完成(并且在现代 Intel 上每条指令至少解码 2 微指令:加载+添加微融合和存储(微融合存储地址 + 存储数据). 如果加载+添加部分运行在同一个物理内核上,则它们都可以在同一个周期中启动。

Ice Lake 也可以在同一周期内执行两个存储,但在此之前现代 x86 CPU 每个时钟只执行 1 个存储。 (例如,从 Haswell 到 Coffee Lake 的 Intel 可以在每个时钟周期进行 2 次加载 + 1 次存储。SnB/IvB 可以为每个周期进行 2 次内存操作的地址生成,并且如果其中最多有一个是存储,则可以维持吞吐量。使用256 位向量的特殊 2+1 情况,为 2 个数据周期重用相同的地址生成。)

除非 EAX 和 EBX 持有相同的指针值,否则这些指令访问不同的内存和不同的寄存器,并且除了执行单元(加载、添加、存储)的资源冲突之外是完全独立的。 (寄存器重命名处理 FLAGS 输出的写后写风险)。

【讨论】:

  • 一个小的历史附录。超标量处理器在 60 年代可用。该技术在 80 年代末被用于微处理器。第一个超标量 x86 是 93 中的 Pentium P5。我不会称之为特别现代的东西。
  • @AProgrammer:同意。但是多core per-socket 系统相对现代,只能追溯到2006 年x86 的Core 2。在此之前是否存在每个芯片或封装具有多个物理内核的非 x86 CPU?无论如何,这就是我在这种情况下所说的“现代 x86”的意思。
  • 我们只在可用区域缺乏更好的事情时才采用多核,因此相对较晚。我认为第一个是 2001 年的 POWER4,但如果我忽略了更小众的东西,我不会感到惊讶。
  • @AProgrammer:是的,在那之前仍然有很多 ILP 留在桌面上,因此为单核增强 OoO exec 和缓存更有意义。感谢 x86 出现之前的至少一个多核示例;我不确定 POWER 是不是这么早就积极地大核了。
【解决方案2】:

是的。 CPU 的很大一部分专门用于所谓的调度,将工作分配给 CPU 的内部资源。只要该调度电路可以证明两条指令不会与它们所需的资源(如不同的 ALU 之类的功能单元,以及最重要的是寄存器)发生冲突,则可以并行调度这些指令。这可能会导致 CPI 小于 1。

相互独立的典型指令是控制流(分支)、整数运算和浮点运算。尤其是后两者几乎总是独立的,因为它们需要非常不同的 ALU,并且对不同类型的数据进行操作。因此,例如,当您的程序运行时

double a = 7.0, factor = 1.1;
for(int i = 42; i--; ) a *= factor;

您可能会发现浮点电路在执行乘法运算的同时整数电路递减并检查循环计数器,而控制流电路执行分支到下一个循环迭代。这样的循环有可能在每次迭代中恰好执行一个循环......


我选择了不同类型指令的示例,因为这样可以很容易地理解每条指令需要不同的资源。但是,现代 CPU 通常包含多个关键资源的副本。例如,它们包含大量能够进行整数加法/减法的 ALU,并使用复杂的寄存器重命名方案来使用比汇编程序程序员可见的寄存器更多的物理寄存器。这允许它们并行执行两条独立的指令,即使它们属于相同类型(例如整数加法)并正式对相同的寄存器进行操作。

基本上,您可以将 CPU 前端视为即时编译器,它将机器代码转换为内部指令集,其中包括一个优化器,它试图让尽可能多的 CPU 资源保持忙碌状态。

【讨论】:

  • a*=factor 是一个串行依赖项,它将成为 FP 乘法延迟循环的瓶颈,例如Skylake 上的 4 个循环。乱序执行可以将循环与周围的代码重叠,并将循环开销的成本隐藏在该 FP 开发链下。但这不是一个完美的例子。 A[i] = B[i] * factor 可能会更好(数据并行可以转换为 SIMD、ILP,甚至是 OpenMP 的线程级并行,用于大型循环)
  • 另外,值得一提的是,任何超标量 CPU 也能够并行运行两条独立的整数指令。我认为不太可能有人会构建具有超标量前端的 CPU,但在后端只提供一个整数 ALU,因此 ILP 只能在整数、FP 和/或分支混合的代码上被利用。至少 2 个整数 ALU 是标准的,尽管标量 FP、加载/存储和分支对于适度或旧的设计是合理的,例如像 P5 Pentium(按顺序 2-wide 与配对规则)
  • @PeterCordes 你在每一个帐户上都是对的 :-) 我刚刚采取了第一个简单易懂的例子,这是我想到的。而且,是的,我使用过 CPU,它会在一个周期内执行我给出的循环。我什至可以添加一个独立的 SIMD 指令,它仍然是一个周期。细节取决于手头的 CPU,但独立资源的一般概念保持不变。
  • 我最担心的是 OP 会误以为 ILP 只能在不同的种类指令之间被利用。是的,这是确保它们独立的一种方法(除了从 ALU 指令读取标志的分支),但这不是唯一的方法。这个答案是正确的;我只是说它可能对初学者更有帮助。循环携带的 dep 链问题很小。
  • @PeterCordes 好点。为了清楚起见,我现在在答案中添加了两段。谢谢。
【解决方案3】:

超标量处理器能够同时获取/解码/执行许多指令。这是通过提供足够的硬件资源来处理多个指令来实现的。例如:Execute 阶段会有多个 ALU 等。

【讨论】:

  • 好简单的答案;我在自己的答案中遗漏了一些关键点。 (直到刚才我编辑我的答案,谢谢提醒)。
【解决方案4】:

了解一些distinction between parallelization and concurrency 很重要。虽然这些术语经常可以互换使用,但它们实际上是不同的。

在多核处理器和多处理器系统存在之前,使用了并发,通常通过time slicing,通过在相邻的串行执行代码的方式出现并行处理CPU 时钟上的时间片。最终的结果是,事情会在大致相同的时间完成,并且似乎是同时完成的。值得注意的是,并发使用的还是蛮多的。

并行化改为运行多个线程并允许在各种内核上异步完成工作,这些内核稍后可以重新组合(或多或少)以在 UI 中提供预期结果或反馈、游戏中的操作等。

一些现代编译器和 CPU/GPU 指令集可以并行化代码中未明确并行的事物。此外,某些基准测试可能会高估或低估给定内核或处理器的线程能力。

【讨论】:

  • 这个问题是关于一个线程中的超标量 CPU 和 ILP,而不是线程级并行性。或者至少问题的第一部分是关于这个的。
  • OP 顺便提到了超级缩放器作为辅助说明。这很难让它“关于”超标量 CPU。
  • 标题和前几段是关于这个的(CPI
  • 哈哈——这很公平。 :)
  • 无论如何,您的回答仍然没有解决 ILP 与 TLP 的问题,有点错过了问题的重点。线程级并行与时间片不是问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-02
  • 1970-01-01
  • 2021-10-13
  • 2017-09-28
  • 1970-01-01
  • 2017-06-19
相关资源
最近更新 更多