62

我听说网上有英特尔的书,它描述了特定汇编指令所需的 CPU 周期,但我找不到(经过努力)。谁能告诉我如何找到CPU周期?

这是一个例子,在下面的代码中,mov/lock 是 1 个 CPU 周期,xchg 是 3 个 CPU 周期。

// This part is Platform dependent!
#ifdef WIN32
inline int CPP_SpinLock::TestAndSet(int* pTargetAddress, 
                                              int nValue)
{
    __asm
    {
        mov edx, dword ptr [pTargetAddress]
        mov eax, nValue
        lock xchg eax, dword ptr [edx]
    }
    // mov = 1 CPU cycle
    // lock = 1 CPU cycle
    // xchg = 3 CPU cycles
}

#endif // WIN32

顺便说一句:这是我发布的代码的 URL:http: //www.codeproject.com/KB/threads/spinlocks.aspx

4

5 回答 5

44

现代 CPU 是复杂的野兽,使用流水线超标量执行乱序执行以及其他使性能分析变得困难的技术……但并非不可能

虽然您不能再简单地将指令流的延迟相加以获得总运行时间,但您仍然可以(通常)高度准确地分析某些代码(尤其是循环)的行为,如下所述和其他链接资源。

指令时间

首先,您需要实际的时间。这些因 CPU 架构而异,但目前 x86 时序的最佳资源是 Agner Fog 的指令表。这些表涵盖不少于三十种不同的微架构,列出了指令延迟,这是一条指令从准备好输入到可用输出的最小/典型时间。用阿格纳的话来说:

延迟: 这是指令在依赖链中生成的延迟。数字是最小值。高速缓存未命中、未对齐和异常可能会显着增加时钟计数。在启用超线程的情况下,在另一个线程中使用相同的执行单元会导致性能下降。非正规数、NAN 和无穷大不会增加延迟。使用的时间单位是核心时钟周期,而不是时间戳计数器给出的参考时钟周期。

因此,例如,该add指令具有一个周期的延迟,因此如图所示,一系列相关的加法指令将具有每个周期 1 个周期的延迟add

add eax, eax
add eax, eax
add eax, eax
add eax, eax  # total latency of 4 cycles for these 4 adds

请注意,这并不意味着add每条指令只需 1 个周期。例如,如果 add 指令依赖,那么在现代芯片上,所有 4 个 add 指令都可以在同一个周期内独立执行:

add eax, eax
add ebx, ebx
add ecx, ecx
add edx, edx # these 4 instructions might all execute, in parallel in a single cycle

Agner 提供了一个衡量这种潜在并行性的指标,称为互惠吞吐量

倒数吞吐量: 同一线程中一系列同类独立指令的每条指令的平均核心时钟周期数。

因为add这被列为0.25意味着add每个周期最多可以执行 4 条指令(给出倒数的吞吐量1 / 4 = 0.25)。

倒数的吞吐量数也暗示了指令的流水线能力。例如,在最新的 x86 芯片上,imul指令的常见形式具有 3 个周期的延迟,并且在内部只有一个执行单元可以处理它们(不像add通常有四个可添加的单元)。然而,对于一长串独立imul指令,观察到的吞吐量是 1 个/周期,而不是您可能期望的每 3 个周期 1 个,因为延迟为 3。原因是该imul单元是流水线的:它可以在每个周期开始一个新的,即使在之前的乘法还没有完成。imul

这意味着一系列独立 imul指令每个周期最多可以运行 1 个,但一系列相关 imul指令每 3 个周期只能运行 1 个(因为下一个指令imul在前一个指令的结果准备好之前无法开始)。

因此,有了这些信息,您就可以开始了解如何分析现代 CPU 上的指令时序。

详细分析

尽管如此,以上只是表面上的。您现在可以通过多种方式查看一系列指令(延迟或吞吐量),并且可能不清楚使用哪种方式。

此外,还有其他一些限制,例如某些指令在 CPU 内竞争相同的资源,以及 CPU 流水线其他部分的限制(例如指令解码)可能导致较低的总吞吐量比您仅通过查看延迟和吞吐量来计算的。除此之外,您还有“ALU 之外”的因素,例如内存访问和分支预测:整个主题本身——您可以很好地对这些进行建模,但这需要工作。例如,这是最近的一篇文章,其中答案详细介绍了大多数相关因素。

涵盖所有细节会使这个已经很长的答案的大小增加 10 倍或更多,所以我只会为您指出最佳资源。Agner Fog 有一个Optimizing Asembly 指南,其中详细介绍了包含十几个指令的循环的精​​确分析。请参阅 PDF 当前版本的第 95 页开始的“ 12.7矢量循环中的瓶颈分析示例”。

基本思想是创建一个表,每条指令有一行,并标记每条指令使用的执行资源。这让您可以看到任何吞吐量瓶颈。此外,您需要检查循环中携带的依赖项,以查看是否有任何限制吞吐量(有关复杂情况,请参阅“ 12.16分析依赖项”)。

如果您不想手动进行,英特尔发布了英特尔架构代码分析器,这是一个自动执行此分析的工具。它目前尚未在 Skylake 之外进行更新,但对于 Kaby Lake 而言,结果在很大程度上仍然是合理的,因为微架构没有太大变化,因此时间保持可比性。这个答案涉及很多细节并提供了示例输出,用户指南也不错(尽管它相对于最新版本已经过时)。

其他来源

Agner 通常会在新架构发布后不久为其提供时序,但您也可以查看instlatx64InstLatX86以了解在和InstLatX64结果中类似组织的时序。结果涵盖了很多有趣的旧筹码,而新筹码通常会很快出现。结果与 Agner 的结果基本一致,除了一些例外。您还可以在此页面上找到内存延迟和其他值。

您甚至可以在附录 C:指令延迟和吞吐量的IA32 和 Intel 64 优化手册中直接从 Intel 获得时序结果。我个人更喜欢 Agner 的版本,因为它们更完整,通常在英特尔手册更新之前到达,并且更易于使用,因为它们提供了电子表格和 PDF 版本。

最后,x86 标签 wiki有大量关于 x86 优化的资源,包括指向如何对代码序列进行循环精确分析的其他示例的链接。

如果您想更深入地了解上述“数据流分析”的类型,我会推荐A Whirlwind Introduction to Data Flow Graphs

于 2017-07-07T23:13:21.273 回答
30

鉴于流水线、乱序处理、微码、多核处理器等,无法保证汇编代码的特定部分将占用 x CPU 周期/时钟周期/任何周期。

如果存在这样的参考,它只能提供给定特定架构的广泛概括,并且根据微代码的实现方式,您可能会发现 Pentium M 与 Core 2 Duo 不同,Core 2 Duo 与 AMD 双核不同, ETC。

请注意,这篇文章是在 2000 年更新的,并且写得更早。即使是 Pentium 4 也很难确定指令时序 - PIII、PII 和原始的 pentium 更容易,并且引用的文本可能基于那些具有更明确的指令时序的早期处理器。

如今,人们通常使用统计分析来进行代码时序估计。

于 2009-03-28T13:00:30.223 回答
27

其他答案所说的无法准确预测在现代 CPU 上运行的代码的性能是正确的,但这并不意味着延迟是未知的,或者知道它们是没有用的。

英特尔和 AMD 处理器的确切延迟列在Agner Fog 的指令表中。另请参阅Intel® 64 和 IA-32 架构优化参考手册,以及AMD 和 Intel x86 处理器的指令延迟和吞吐量(来自 Can Berk Güder 现在已删除的仅链接答案)。AMD 在他们自己的网站上也有带有官方价值观的 pdf 手册。

对于(微)优化紧密循环,了解每条指令的延迟对于手动尝试调度代码有很大帮助。程序员可以做很多编译器做不到的优化(因为编译器不能保证不会改变程序的意思)。

当然,这仍然需要你了解 CPU 的很多其他细节,比如流水线的深度、每个周期可以发出多少条指令、执行单元的数量等等。当然,这些数字因不同的 CPU 而异。但是您通常可以得出一个或多或少适用于所有 CPU 的合理平均值。

不过值得注意的是,在这个级别上优化几行代码也需要做很多工作。而且很容易做出结果是悲观的事情。现代 CPU 非常复杂,它们非常努力地从糟糕的代码中获得良好的性能。但也有一些情况他们无法有效地处理,或者你认为你很聪明并且可以编写高效的代码,结果会降低 CPU 的速度。

编辑 查看英特尔的优化手册,表 C-13:第一列是指令类型,然后每个 CPUID 都有许多列延迟。CPUID 指示编号适用于哪个处理器系列,并在文档的其他地方进行了说明。延迟指定在指令结果可用之前需要多少个周期,因此这是您要查找的数字。

吞吐量列显示每个周期可以执行多少此类指令。

在这张表中查找 xchg,我们看到根据 CPU 系列的不同,它需要 1-3 个周期,而一个 mov 需要 0.5-1。这些是用于指令的寄存器到寄存器形式,而不是用于lock xchg内存,这要慢得多。更重要的是,巨大的延迟和对周围代码的影响(当与另一个核心争用时会慢得多),所以只看最好的情况是错误的。(我没有查看每个 CPUID 的含义,但我认为 0.5 用于 Pentium 4,它以双倍速度运行芯片的某些组件,允许它在半个周期内完成任务)

但是,我真的不知道您打算将这些信息用于什么目的,但是如果您知道代码正在运行的确切 CPU 系列,那么将延迟相加会告诉您执行此指令序列所需的最小周期数.

于 2009-03-28T14:02:06.613 回答
15

在 x86 上测量和计算 CPU 周期不再有意义。

首先,问问自己你在计算哪个 CPU 的周期?核心2?速龙?奔腾-M?原子?所有这些 CPU 都执行 x86 代码,但它们都有不同的执行时间。执行甚至在同一 CPU 的不同步进之间有所不同。

最后一个循环计数有意义的 x86 是 Pentium-Pro。

还要考虑到,在 CPU 内部,大多数指令都被转码为微码,并由内部执行单元乱序执行,该执行单元甚至看起来都不像 x86。单条 CPU 指令的性能取决于内部执行单元中有多少资源可用。

因此,一条指令的时间不仅取决于指令本身,还取决于周围的代码。

无论如何:您可以估计不同处理器的吞吐量资源使用和指令延迟。相关信息可在 Intel 和 AMD 网站上找到。

Agner Fog 在他的网站上有一个很好的总结。请参阅指令表了解延迟、吞吐量和 uop 计数。请参阅微架构 PDF 以了解如何解释这些内容。

http://www.agner.org/optimize

但请注意,xchg-with-memory 没有可预测的性能,即使您只查看一种 CPU 型号。即使在 L1D 缓存中缓存线已经很热的无争用情况下,作为一个完整的内存屏障也意味着它的影响在很大程度上取决于加载和存储到周围代码中的其他地址。


顺便说一句-因为您的示例代码是无锁数据结构的基本构建块:您是否考虑过使用编译器内置函数?在 win32 上,您可以包含 intrin.h 并使用 _InterlockedExchange 等函数。

这将为您提供更好的执行时间,因为编译器可以内联指令。内联汇编器总是强制编译器禁用围绕 asm 代码的优化。

于 2009-03-28T13:09:11.047 回答
7

锁定 xchg eax, dword ptr [edx]

请注意,锁会为所有内核的内存获取锁定内存,这在某些多核上可能需要 100 个周期,并且还需要刷新缓存行。它还会使管道停滞。所以我不会担心其余的。

因此,最佳性能又回到了调整算法的关键区域。

请注意,在单核上,您可以通过移除锁来优化这一点,但多核需要它。

于 2010-01-04T14:21:44.513 回答