19

当我曾经对嵌入式系统和早期的 8/16 位 PC(6502、68K、8086)进行编程时,我对每条指令执行所需的时间(以纳秒或微秒为单位)有很好的把握。根据系列,一个(或四个)周期相当于一个“内存获取”,并且无需担心缓存,您可以根据所涉及的内存访问次数猜测时间。

但是对于现代CPU,我很困惑。我知道它们要快得多,但我也知道如果不知道每条指令需要多少个时钟周期,标题千兆赫的速度是没有帮助的。

因此,任何人都可以为(假设)2GHz Core 2 Duo 上的两个示例指令提供一些时间。最好和最坏的情况(假设缓存中没有任何内容/缓存中的所有内容)将很有用。

指令#1:将一个 32 位寄存器加到一秒。

指令 #2:将 32 位值从寄存器移动到内存。

编辑:我问这个的原因是尝试开发一个“经验法则”,它可以让我查看简单的代码并粗略地衡量最接近的数量级所花费的时间。

编辑#2:有很多有趣的答案,但没有人(还)写下一个及时测量的数字。我很欣赏这个问题有“复杂性”,但是来吧:如果我们可以估计NYC 的钢琴调音师的数量,我们应该能够估计代码运行时间......

采取以下(愚蠢的)代码:

int32 sum = frigged_value();

// start timing
 for (int i = 0 ; i < 10000; i++)
 {
   for (int j = 0 ; j < 10000; j++)
   {
     sum += (i * j)
   }
   sum = sum / 1000;
 }

// end timing

我们如何估算运行... 1 飞秒需要多长时间?1 千兆年?

4

15 回答 15

40

您提到的Core 2 Duo等现代处理器既是超标量又是流水线的. 它们每个内核有多个执行单元,实际上每个内核一次处理多个指令;这是超标量部分。流水线部分意味着从读入和“发出”指令到完成执行之间存在延迟,该时间取决于该指令与同时通过其他执行单元的其他指令之间的依赖关系。因此,实际上,任何给定指令的时间取决于它周围的东西和它所依赖的东西。这意味着给定指令具有基于多种因素的最佳情况和最坏情况执行时间。由于有多个执行单元,每个核心时钟实际上可以有不止一条指令完成执行,

以上只是从CPU核心本身来看。然后,您将与缓存进行交互,并与其他内核争用带宽。CPU的总线接口单元处理将指令和数据输入内核并将结果通过缓存从内核放回内存。

粗略的数量级经验法则与一粒盐一起使用:

  • 寄存器到寄存器操作需要 1 个内核时钟来执行。这通常应该是保守的,尤其是当更多这些按顺序出现时。
  • 内存相关的加载和存储操作需要 1 个内存总线时钟来执行。这应该是非常保守的。如果高速缓存命中率较高,它将更像是 2 个CPU 总线时钟,这是 CPU 内核和高速缓存之间总线的时钟速率,但不一定是内核的时钟。
于 2009-01-11T16:16:48.197 回答
14

几乎不可能以对您有用的方式提供您期望的准确时间信息。

以下概念会影响指令时序;有些可能会随时变化:

  • 微操作分解
  • 操作流水线
  • 超标量执行
  • 乱序执行
  • SMT/SMP执行
  • 浮点模式
  • 分支预测/预取
  • 缓存延迟
  • 内存延迟
  • 时钟速度限制
  • ETC

如果您需要对上述概念的任何进一步解释,请查阅有关现代计算机体系结构的书。

衡量代码速度的最佳方法是(惊喜!)衡量代码在“现实世界”中运行相同工作负载和相同条件下的速度。

于 2009-01-11T16:37:05.567 回答
8

使用主要基于 Intel Pentium 架构的描述来缩短一个非常长的故事:

  • 处理器有许多“执行单元”,可以执行不同类型的“微操作”;指令可以分成几个微操作
  • 不同的执行单元本质上是并行运行的
  • 每个微操作都会占用相应的执行单元一定数量的时钟周期,因此同时没有其他指令可以使用该执行单元:例如“浮点加法”可能会占用“FP 执行”单元 2 个时钟周期
  • 执行单元按“端口”分组,每个时钟周期,可以向每个端口发送一个新的微操作(假设此时相关执行单元空闲);一些单位也可以在循环中途被发送一个“额外的操作”;所以每个时钟周期,一定数量的操作可以开始执行;
  • 处理器可以在不破坏依赖关系的情况下(或仍然可以重建结果)重新排序微操作,以利用在给定时刻哪些执行单元是空闲的
  • 所以指令可以并行执行,但是在任何时候执行的指令的哪些部分是相当复杂的情况
  • 因此,给定指令的总时间取决于它必须“等待”必要的执行单元可用的时间、这些操作在给定单元上运行所花费的实际时间,以及“占用结果”

由于指令的时序取决于周围的指令,因此在实践中,通常最好对具有代表性的代码段进行计时,而不是尝试担心单个指令。然而:

  • 英特尔(可能还有其他制造商)发布指令吞吐量延迟时间列表
  • 吞吐量是相关执行单元上实际需要的时钟周期数
  • 延迟是“最坏情况”所需的时钟周期数,一旦指令开始执行,在该执行结果可用作另一条指令的输入之前

因此,例如,如果浮点加法和乘法指令的吞吐量为 2,延迟为 5(实际上,乘法我认为它要大一些),这意味着向自身添加一个寄存器或将其乘以本身可能需要两个时钟周期(因为没有其他相关值),而将它添加到先前乘法的结果将需要大约或略小于 2+5 个时钟周期,具体取决于您开始/结束计时的位置,并且关于其他各种事情。(在其中一些时钟周期中,可能会发生另一个加法/乘法运算,因此无论如何您实际上将多少个周期归因于各个加法/乘法指令是有争议的......)

哦,作为一个具体的例子。对于以下 Java 代码

public void runTest(double[] data, double randomVal) {
  for (int i = data.length-1; i >= 0; i--) {
    data[i] = data[i] + randomVal;
  }
}

Hotspot 1.6.12 JIT 将内部循环序列编译为以下英特尔代码,包括数组中每个位置的加载-添加-存储(在这种情况下,“randomVal”保存在 XMM0a 中):

  0b3     MOVSD  XMM1a,[EBP + #16]
  0b8     ADDSD  XMM1a,XMM0a
  0bc     MOVSD  [EBP + #16],XMM1a
  0c1     MOVSD  XMM1a,[EBP + #8]
  0c6     ADDSD  XMM1a,XMM0a
  0ca     MOVSD  [EBP + #8],XMM1a
  ...

每组load-add-store 似乎需要 5 个时钟周期

于 2009-01-11T16:48:55.980 回答
7

没那么简单。您的两条指令的时间安排不会帮助您评估大量指令的性能。这是因为现代处理器可以并行执行许多操作,并且具有大型缓存,因此“将值移动到内存”发生在与指令执行完全不同的时间。

因此,最佳情况为零(与其他指令并行执行时)。但这对你有什么帮助?

网页显示了一些基准测试,包括一些 %MIPS/MHz 结果。如您所见,在许多基准测试中,每个时钟周期执行多条指令。这些图表还显示了缓存大小和内存速度的影响。

于 2009-01-11T16:01:28.837 回答
7

现代处理器做的事情甚至更棘手。

乱序执行。如果可以在不影响正确行为的情况下这样做,处理器可能会以与程序中列出的顺序不同的顺序执行指令。这可以隐藏长时间运行指令的延迟。

注册重命名。处理器通常在其指令集中拥有比可寻址寄存器更多的物理寄存器(所谓的“架构”寄存器)。这可以是为了向后兼容,也可以只是为了启用有效的指令编码。当程序运行时,处理器会将其使用的架构寄存器“重命名”为任何空闲的物理寄存器。这允许处理器实现比原始程序中更多的并行性。

例如,如果您对 EAX 和 ECX 进行长序列操作,然后执行将 EAX 和 ECX 重新初始化为新值并执行另一长序列操作的指令,则处理器可以为这两个任务使用不同的物理寄存器,并执行他们并行。

英特尔 P6 微架构同时进行乱序执行和寄存器重命名。Core 2 架构是 P6 的最新衍生产品。

要真正回答您的问题 - 面对所有这些架构优化,您基本上不可能手动确定性能。

于 2009-01-11T16:38:15.297 回答
7

你要求的那种预测是没有希望的。

如果你想要一个经验法则,这里有一些经验法则:

  • 在从二级缓存中获取一个字所需的时间内,一个处理器可以执行至少 10 条指令。所以担心内存访问,而不是指令计数——寄存器中的计算几乎是免费的。

  • 在从 RAM 中获取一个字所需的时间中,处理器可以执行数千条指令(这个数字会根据您的硬件细节有几个数量级的变化)。确保这只发生在冷缓存上;否则没有其他事情了。

  • 如果您在 x86 CPU 上运行,则没有足够的寄存器。任何时候都尽量不要在代码中包含超过 5 个实时变量。或者更好的是,转向 AMD64 ( x86_64) 并将寄存器数量翻倍。有 16 个寄存器,并且参数在寄存器中传递,您可以不用担心寄存器。

每年都有一段时间我会问架构师我应该使用什么经验法则来预测我的编译器生成的代码的成本。我已经停下来了,因为我最后一次收到有用的答案是在 1999 年。(答案是“确保你的循环适合重新排序缓冲区”。所有知道什么是重新排序缓冲区的人现在都可以举手。奖金如果您可以发现您当前使用的任何计算机上的重新排序缓冲区的大小,则为积分。)

于 2009-01-11T20:45:32.757 回答
5

这仅回答了您的部分问题,但我发现 Wikipedia 中关于参考位置的此表很有帮助。它描述了内存层次结构不同级别的访问速度和内存量,使用了大约 2006 次:

  • CPU 寄存器(8-32 个寄存器)——立即访问(0-1 个时钟周期)
  • L1 CPU 缓存(32 KiB 到 128 KiB)——快速访问(3 个时钟周期)
  • L2 CPU 缓存(128 KiB 到 12 MiB)——访问速度稍慢(10 个时钟周期)
  • 主物理内存 (RAM)(256 MiB 到 4 GiB)——慢速访问(100 个时钟周期)
  • 磁盘(文件系统)(1 GiB 到 1 TiB)——非常慢(10,000,000 个时钟周期)
  • 远程内存(例如其他计算机或互联网)(几乎无限制)- 速度各不相同
于 2009-02-21T08:55:31.853 回答
4

您可以在此处下载 Intel 64 和 IA-32 手册。

但是你真正需要的是来自Agner Fog的东西。

他有很多额外的信息,例如他的手册“指令表:Intel 和 AMD CPU 的指令延迟、吞吐量和微操作故障列表”

或者测试用于计算时钟周期的程序(他使用时间戳计数器)。

于 2009-01-26T16:55:48.043 回答
4

在这个线程上已经有很多很好的答案,但是到目前为止没有提到一个主题:分支错误预测

因为所有现代处理器都是流水线的,所以当指令解码器运行到类似“如果相等时跳转”的指令时,它不知道指令会跳转到哪个方向,所以它只是猜测。然后它继续根据该猜测将指令输入管道。如果它做出了正确的预测,跳转指令的吞吐量和延迟基本上为零。如果猜错了,同一条跳转指令的吞吐量和延迟可能是 50 或 100 个周期。

请注意,同一条指令第一次在循环中执行时可能具有“零成本”,而下一次执行同一条指令时的成本非常高!

于 2009-01-27T05:32:21.433 回答
3

您所需要的只是相应的 CPU 手册。AMD 和 Intel 都在他们的网站上提供了 PDF,描述了每条指令的延迟。

请记住现代 CPU 的复杂性。它们一次不执行一条指令,每个周期可以加载 3-4 条指令,并且几乎所有指令都是流水线的,因此当加载下一条指令时,当前的指令还远未完成。它还对指令重新排序以实现更有效的调度。现代 CPU 一次可以轻松处理 50 条指令。

所以你问错问题了。一条指令所花费的时间因测量方式和时间而异。除了缓存等简单问题外,这取决于指令解码器的繁忙程度、分支预测器、调度以及正在调度哪些其他指令。

于 2009-01-11T17:59:34.667 回答
3

我建议下载 AMD软件优化指南

于 2009-01-27T05:38:10.000 回答
2

正如 Doug 已经指出的,最好的情况是零(超标量处理器、多个执行单元、数据已经在 L1 缓存中)。

最坏的情况是长达几毫秒(当操作系统处理页面错误并且必须从磁盘获取数据/指令时)。排除磁盘/交换它仍然取决于您是否有 NUMA 机器,它具有哪种拓扑,数据位于哪个内存节点,是否有来自另一个 CPU 的并发访问(总线锁定和缓存同步协议)等。

于 2009-01-11T16:15:22.833 回答
2

Alan Kay 在 2004 年的一句有趣的话:

顺便说一句,为您提供一个有趣的基准测试——在大致相同的系统上,以相同的方式进行大致优化,Xerox PARC 1979 年的基准测试现在只快 50 倍。摩尔定律在那段时间给我们带来了 40,000 到 60,000 倍的改进。因此,糟糕的 CPU 架构损失了大约 1,000 倍的效率。

这似乎意味着 CPU 性能增强似乎集中在它们对我们真正编写的软件影响相对较小的领域。

于 2009-01-11T18:14:33.543 回答
0

我不认为最坏的情况仅限于某些平台。当您有多个内核和处理器争夺相同的位置或相邻的内存位置时,您会看到各种性能下降。缓存行必须在处理器之间移动。对于现代平台上的内存操作,我还没有看到一个好的最坏情况数字。

于 2009-01-26T17:32:46.830 回答
0

这花了将近 11 年,但我有一个估计。您的循环大约是 10 ops* 1 亿次迭代,所以大约 10 亿次ops。在 2.3 GHz 机器上,我估计大约需要 0.4 秒。当我测试它时,我实际上得到了 1.2 秒。所以它在一个数量级之内。

只需获取您的核心频率,估计ops,然后划分。这给出了一个非常粗略的估计,每当我进行经验测试时,我的误差从未超过一个数量级。只要确保你的op估计是合理的。

于 2019-11-14T01:55:44.743 回答