performance - 英特尔内在函数中的延迟与吞吐量

标签 performance x86 sse intrinsics micro-optimization

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

例如,让我们考虑:

_mm_cmpestrc

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

除了了解单个指令相对于不同版本的代码需要多长时间之外,我不确定如何使用延迟和吞吐量。

最佳答案

有关 CPU 性能的更完整图片,请参阅 Agner Fog's microarchitecture guide and instruction tables 。 (他的 Optimizing C++ 和 Optimizing Assembly 指南也很棒)。另请参阅 标签维基中的其他链接,尤其是英特尔的优化手册。
有关分析短代码序列的示例,请参阅

  • What is the efficient way to count set bits at a position or lower?
  • 也许我的一些其他答案,稍后我有时间挖掘它们时会编辑更多。

  • 单条指令的延迟和吞吐量实际上不足以为使用混合向量指令的循环获得有用的图片。这些数字不会告诉您哪些内在函数(asm 指令)相互竞争吞吐量资源(即它们是否需要相同的执行端口)。它们仅适用于 super 简单的循环,例如加载/做一件事/存储,或例如用 _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 个 vector-FP 添加操作。因此,您需要 8 个独立的向量累加器(最后将它们相加)来暴露那么多的并行性。 (例如,使用 8 个单独的 __m256 sum0, sum1, ... 变量手动展开循环。编译器驱动的展开(使用 -funroll-loops -ffast-math 编译)通常会使用相同的寄存器,但循环开销不是问题)。

    这些数字还忽略了英特尔 CPU 性能的第三个主要维度:融合域 uop 吞吐量。 大多数指令解码为单个 uop,但有些指令解码为多个 uop。 (特别是 SSE4.2 字符串指令,如您提到的 _mm_cmpestrc:PCMPESTRI 在 Skylake 上为 8 uops)。即使在任何特定的执行端口上都没有瓶颈,您仍然可以在前端保持乱序核心处理工作的能力上遇到瓶颈。英特尔 Sandybridge 系列 CPU 每个时钟最多可以发出 4 个融合域 uops,实际上,当其他瓶颈不发生时,通常可以接近该值。 (有关不同循环大小的一些有趣的最佳情况前端吞吐量测试,请参阅 Is performance reduced when executing loops whose uop count is not a multiple of processor width?。)由于加载/存储指令使用与 ALU 指令不同的执行端口,因此当 L1 缓存中的数据很热时,这可能是瓶颈。
    除非您查看编译器生成的 asm,否则您将不知道编译器必须使用多少额外的 MOVDQA 指令来在寄存器之间复制数据,以解决以下事实:如果没有 AVX,大多数指令将它们的第一个源寄存器替换为结果。 (即破坏性目的地)。您也不会知道循环中任何标量操作的循环开销。

    I think I have a decent understanding of the difference between latency and throughput


    你的猜测似乎没有意义,所以你肯定错过了一些东西。
    CPUs are pipelined ,它们内部的执行单元也是如此。一个“完全流水线化”的执行单元可以在每个周期开始一个新的操作(吞吐量 = 每个时钟一个)
  • (reciprocal) 吞吐量是当没有数据依赖强制它等待时操作可以开始的频率,例如此指令每 7 个周期一个。
  • 延迟是一个操作的结果准备好所需的时间,通常只有当它是循环携带的依赖链的一部分时才重要。
    如果循环的下一次迭代独立于前一次运行,那么乱序执行可以“看到”足够远的距离以找到两次迭代之间的 instruction-level parallelism 并保持自己忙碌,仅在吞吐量上成为瓶颈。

  • (未完全编辑完成,稍后将整理。)

    关于performance - 英特尔内在函数中的延迟与吞吐量,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40878534/

    相关文章:

    c++ - 什么是 __asm volatile ("pause"::: "memory");做?

    c++ - 使用 g++ 进行 sse 内联汇编

    c++ - SSE 比常规功能慢得多

    c - 数组乘法与 sse 内在函数乘法的时间?

    javascript - 选择器 : Id vs. 上下文

    performance - 为什么我的超便携笔记本电脑 CPU 不能在 HPC 中保持最佳性能

    assembly - 为什么我的代码显示垃圾?

    performance - 比较 QString 和 char* 的最有效方法是什么

    ruby-on-rails - rails : Cache or not Cache

    assembly - VMware Workstation Pro 16.2.3 下的 WinDBG 单步执行时将 x87 FPUInstructionPointer 归零