【问题标题】:latency vs throughput in intel intrinsics英特尔内在函数的延迟与吞吐量
【发布时间】:2017-04-14 04:42:11
【问题描述】:

总的来说,我认为我对延迟和吞吐量之间的区别有相当的了解。但是,对于 Intel Intrinsics,我不清楚延迟对指令吞吐量的影响,尤其是在按顺序(或几乎按顺序)使用多个内部调用时。

例如,让我们考虑:

_mm_cmpestrc

这在 Haswell 处理器上具有 11 的延迟和 7 的吞吐量。如果我在循环中运行此指令,我会在 11 个周期后获得连续的每个周期输出吗?由于这需要一次运行 11 条指令,并且由于我的吞吐量为 7,我是否会用完“执行单元”?

我不知道如何使用延迟和吞吐量,除了了解一条指令相对于不同版本的代码需要多长时间。

【问题讨论】:

  • 吞吐量 = 7 表示每 7 个周期可以启动一次。延迟 = 11 意味着单个结果需要 11 个周期。因此,平均而言,在任何给定时间大约有 1.5 个在进行中,并且不超过 2 个。(尽管它是一个多指令指令,因此调度程序可能最终会因某种原因将更多指令与指令交错)。顺便说一句,Agner Fog 在 Haswell 上的 PCMPESTRI 数字与英特尔的不匹配。)

标签: performance x86 sse intrinsics micro-optimization


【解决方案1】:

要更全面地了解 CPU 性能,请参阅Agner Fog's microarchitecture guide and instruction tables。 (他的 Optimizing C++ 和 Optimizing Assembly 指南也很棒)。另请参阅 标签 wiki 中的其他链接,尤其是英特尔的优化手册。

另见


单个指令的延迟和吞吐量实际上不足以为使用混合矢量指令的循环获得有用的图片。这些数字不会告诉您哪些内在函数(asm 指令)相互竞争吞吐量资源(即它们是否需要相同的执行端口)。它们仅适用于超级简单的循环,例如加载/做一件事/存储,或者例如用_mm_add_ps 或_mm_add_epi32 对数组求和。

您可以使用多个累加器来获得更多 instruction-level parallelism,但您仍然只使用一个内在函数,因此您确实有足够的信息来查看它,例如Skylake 之前的 CPU 只能维持每个时钟一个_mm_add_ps 的吞吐量,而 SKL 每个时钟周期可以启动两个(倒数吞吐量为每 0.5c 一个)。它可以在其完全流水线的 FMA 执行单元上运行 ADDPS,而不是使用单个专用的 FP-add 单元,因此比 Haswell(3c lat,每 1c tput 一个)具有更好的吞吐量但更短的延迟。

由于 _mm_add_ps 在 Skylake 上的延迟为 4 个周期,这意味着可以同时进行 8 个向量-FP 添加操作。因此,您需要 8 个独立的向量累加器(最后将它们相互添加)来展示这么多的并行性。 (例如,使用 8 个单独的 __m256 sum0, sum1, ... 变量手动展开循环。编译器驱动的展开(使用 -funroll-loops -ffast-math 编译)通常会使用相同的寄存器,但循环开销不是问题)。


这些数字还忽略了英特尔 CPU 性能的第三个主要维度:融合域 uop 吞吐量。 大多数指令解码为单个 uop,但有些指令解码为多个 uop。 (特别是 SSE4.2 字符串指令,如您提到的 _mm_cmpestrc:PCMPESTRI 在 Skylake 上是 8 微指令)。即使在任何特定的执行端口上都没有瓶颈,您仍然可以在前端保持无序核心处理工作的能力上遇到瓶颈。英特尔 Sandybridge 系列 CPU 每个时钟最多可以发出 4 个融合域微指令,实际上,当其他瓶颈不发生时,通常可以接近这一点。 (请参阅Is performance reduced when executing loops whose uop count is not a multiple of processor width?,了解针对不同循环大小的一些有趣的最佳前端吞吐量测试。)由于加载/存储指令使用与 ALU 指令不同的执行端口,因此当 L1 缓存中的数据很热时,这可能成为瓶颈。

除非您查看编译器生成的 asm,否则您不会知道编译器必须使用多少额外的 MOVDQA 指令来在寄存器之间复制数据,以解决没有 AVX 的情况下,大多数指令会替换它们的第一个源的事实注册结果。 (即破坏性目的地)。您也不会知道循环中任何标量操作的循环开销。


我认为我对延迟和吞吐量之间的区别有一个不错的了解

你的猜测似乎没有道理,所以你肯定漏掉了什么。

CPUs are pipelined,它们内部的执行单元也是如此。一个“完全流水线”的执行单元可以在每个周期开始一个新的操作(吞吐量 = 每个时钟一个)

  • (reciprocal) 吞吐量是指当没有数据依赖项迫使操作等待时,操作可以启动的频率,例如该指令每 7 个周期一个。

  • 延迟是一个操作的结果准备就绪所需的时间,通常仅在它是循环承载的依赖链的一部分时才重要。

    如果循环的下一次迭代独立于前一次运行,那么乱序执行可以“看到”足够远的距离,以在两次迭代之间找到instruction-level parallelism,并使自己保持忙碌,仅在吞吐量上成为瓶颈。

【讨论】:

  • 在一个简单的层面上,这证实了我的怀疑,即这些数字只有在单独使用内部函数时才真正简单。从您的回答中我仍然不明白的是哪些资源限制了多条指令(通常是相同类型)的执行顺序执行。正如您所提到的,执行单元的数量是一个限制。最大化 SIMD 寄存器的数量怎么样? Agner 的文档,尤其是微架构指南,在理解各种设计方法的含义方面似乎特别有趣且相关。
  • 是的,他们竞争的主要吞吐量资源是执行端口。例如在 Haswell 及以后,所有 shuffle 都在端口 5 上运行,因此它们都相互竞争。 PADD* (_mm_add_epi8/16/32/64) 可以在 p1 或 p5 上运行,因此 shuffle 会降低最大添加吞吐量。 (并且由于不完善的乱序调度,一些 PADDB 指令将窃取端口 5,即使 shuffle 在关键路径上但 add 不在。由于 uops 在其操作数之后必须等待执行端口而产生额外延迟准备好被称为“资源冲突”。)
  • @Jimbo:如果编译器用完了向量寄存器,它必须使用一些额外的加载指令。 (如果它必须溢出临时文件而不是仅仅重新加载已经需要在某个时刻进入内存的东西(或者一开始是只读的),那么也可能存储。)额外指令=额外的融合域哎呀。顺便说一句,感谢您对这个答案不清楚的确切内容的反馈。如果/当我在匆忙发布后重新改进它时,这将有所帮助。
  • 阅读 Peter 从顶部链接的指南,尤其是 optimizing in assembly 指南,我怎么强调都不过分——它通过几个工作示例确切地展示了它是如何工作的- 并回答您甚至不知道的问题。不要被愚弄——你可能是“用 C/C++ 编写”,但是当使用内在函数时,它比 C 更接近汇编(无论如何你应该知道汇编以检查编译器没有做任何可怕的事情——它经常这样做)。
  • @Jimbo:完全同意 BeeOnRope 的观点。要获得真正的高性能,您需要检查编译器输出。而且您需要将 C + 内在函数视为“可移植的汇编语言”,因此您编写的代码类似于最佳 asm 的外观(包括内在函数周围的代码)。尽管这不是真的,因为 clang 通常会优化您的内在函数(比 gcc 或 icc 更多)。例如它有自己的洗牌的内部表示,所以它只知道什么去哪里,忘记你在选择发出什么指令时使用的内在。
猜你喜欢
  • 1970-01-01
  • 2015-04-16
  • 2022-12-25
  • 1970-01-01
  • 2018-12-14
  • 2021-04-16
  • 2017-11-19
  • 2019-08-27
  • 2016-11-16
相关资源
最近更新 更多