【问题标题】:Unnecessary instructions generated for _mm_movemask_epi8 intrinsic in x64 mode在 x64 模式下为 _mm_movemask_epi8 内部生成不必要的指令
【发布时间】:2016-07-01 05:34:16
【问题描述】:

来自 SSE2 的内部函数 _mm_movemask_epi8 由英特尔定义,原型如下:

  int _mm_movemask_epi8 (__m128i a);

这个内在函数直接对应pmovmskb指令,由所有编译器生成。

根据this referencepmovmskb 指令可以在 x64 模式下将生成的整数掩码写入 32 位或 64 位通用寄存器。在任何情况下,结果只有低 16 位可以是非零的,即结果肯定在范围 [0; 65535]。

说到内部函数_mm_movemask_epi8,它的返回值是int类型,在大多数平台上它是一个32位大小的有符号整数。不幸的是,没有替代函数可以在 x64 模式下返回 64 位整数。结果:

  1. 编译器通常会生成带有 32 位目标寄存器的 pmovmskb 指令(例如 eax)。
  2. 编译器不能假定整个寄存器的高 32 位(例如 rax)为零。
  3. 编译器插入不必要的指令(例如mov eax, eax)将 64 位寄存器的上半部分归零,因为该寄存器稍后用作 64 位值(例如,作为数组的索引)。

可以在this answer 中查看具有此类问题的代码示例和生成的程序集。该答案的 cmets 也包含一些相关的讨论。我经常在使用 MSVC2013 编译器时遇到这个问题,但似乎它也存在于 GCC 上。

问题是:

  1. 为什么会这样?
  2. 有什么方法可以可靠地避免在流行的编译器上生成不必要的指令?特别是当结果用作索引时,即在x = array[_mm_movemask_epi8(xmmValue)];
  3. 在现代 CPU 架构上,像 mov eax, eax 这样的不必要指令的大致成本是多少?这些指令是否有可能在内部被 CPU 完全消除,并且它们实际上并不占用执行单元的时间(Agner Fog 的指令表文档提到了这种可能性)。

【问题讨论】:

  • 关于问题 #2,您可以随时下拉到内联汇编,尽管我的记忆告诉我,也许 MSVC 不支持。
  • @JasonR:是的,MSVC x64 没有内联汇编。此外,在 MSVC x32 内联汇编中完全禁止优化。但是在一般情况下重要的是,使用编译器而不是汇编可以从内联函数中编写高效的代码。它甚至允许使用 C++ 模板制作高度可配置和可读的代码。更不用说用内部函数编写对于程序员来说更容易、更快,而且更便携。
  • 我同意;内在函数当然是可取的。关于问题 #3,您是否尝试过在您的应用程序中对其进行分析?除非这是在一个内部循环中,否则我怀疑额外的指令会产生有意义的影响,特别是如果你在那里也有内存访问。
  • cdqe - 将 32 位扩展到 64 位,以 1 个时钟延迟执行。如果你没有触及寄存器的上半部分,我已经看到 gcc clear 一次(比如在循环之外)并跳过这个。

标签: gcc 64-bit x86-64 sse micro-optimization


【解决方案1】:

gcc.godbolt.org 是一个很好的在线资源,用于使用不同的编译器测试此类问题。

clang 似乎在这方面做得最好,例如

#include <xmmintrin.h>
#include <cstdint>

int32_t test32(const __m128i v) {
  int32_t mask = _mm_movemask_epi8(v);
  return mask;
}

int64_t test64(const __m128i v) {
  int64_t mask = _mm_movemask_epi8(v);
  return mask;
}

生成:

test32(long long __vector(2)):                         # @test32(long long __vector(2))
        vpmovmskb       eax, xmm0
        ret

test64(long long __vector(2)):                         # @test64(long long __vector(2))
        vpmovmskb       eax, xmm0
        ret

gcc 在 64 位情况下生成额外的 cdqe 指令:

test32(long long __vector(2)):
        vpmovmskb       eax, xmm0
        ret
test64(long long __vector(2)):
        vpmovmskb       eax, xmm0
        cdqe
        ret

【讨论】:

    【解决方案2】:

    为什么会这样?

    gcc 的内部指令定义告诉它pmovmskb 做了什么,一定没有告诉它rax 的高32 位总是为零。我的猜测是它被视为函数调用返回值,其中 ABI 允许返回 32 位 int 的函数将垃圾留在 rax 的高 32 位中。

    GCC 确实知道 32 位操作通常会免费进行零扩展,但这种错过的优化对于内在函数很普遍,也会影响像 _mm_popcnt_u32 这样的标量内在函数。

    还有 gcc(不)知道实际结果仅在其 32 位 int 结果的低 16 位中设置位的问题(除非您使用 AVX2 vpmovmskb ymm)。所以实际的符号扩展是不必要的;隐式零扩展完全没问题。

    有什么方法可以可靠地避免在流行的编译器上生成不必要的指令?特别是当 result 用作索引时,即在x = array[_mm_movemask_epi8(xmmValue)];

    不,除了修复 gcc。有没有人将此报告为编译器错过优化错误?

    clang 没有这个错误。我在 Paul R 的测试中添加了代码,以实际将结果用作数组索引,clang 仍然可以。

    gcc always either zero or sign extends(在这种情况下到不同的寄存器,可能是因为它想“保留” RAX 底部的 32 位值,而不是因为它正在优化 mov-elimination。

    转换为无符号有助于 GCC6 及更高版本;它将直接使用pmovmskb 结果作为寻址模式的一部分,但也会返回结果为mov rax, rdx

    对于较旧的 GCC,至少可以使用 mov 而不是 movsxdcdqe

    在现代 CPU 架构上,像 mov eax, eax 这样的不必要指令的大致成本是多少?这些指令是否有可能在内部被 CPU 完全消除,并且它们实际上并不占用执行单元的时间(Agner Fog 的指令表文档提到了这种可能性)。

    mov same,same 在 SnB 系列微架构或 AMD zen 上永远不会被淘汰。 mov ecx, eax 将被淘汰。详情请见Can x86's MOV really be "free"? Why can't I reproduce this at all?

    即使它不占用执行单元,它仍然占用管道的融合域部分中的一个槽,以及 uop-cache 中的一个槽。和代码大小。如果你接近前端 4 fused-domain uops 每个时钟限制(流水线宽度),那就有问题了。

    它还会在 dep 链中花费额外的 1c 延迟。

    (不过,后端吞吐量不是问题。在 Haswell 和更新版本上,它可以在没有向量执行单元的端口 6 上运行。在 AMD 上,整数端口与向量端口是分开的。)

    【讨论】:

      猜你喜欢
      • 2020-04-13
      • 1970-01-01
      • 2022-07-07
      • 2020-10-14
      • 2017-05-20
      • 1970-01-01
      • 2014-02-25
      • 1970-01-01
      • 2010-09-13
      相关资源
      最近更新 更多