缺少用于掩码的 AVX-512 内在函数?

Missing AVX-512 intrinsics for masks?

Intel 的 AVX-512 K* 掩码指令的内在函数指南 lists a number of intrinsics,但似乎缺少一些:

英特尔开发人员手册声称内在函数不是必需的,因为它们是由编译器自动生成的。一个人如何做到这一点?如果这意味着 __mmask* 类型可以被视为常规整数,那将很有意义,但是测试 mask << 4 之类的东西似乎会导致编译器将掩码移动到常规寄存器,移位它,然后回到面具。这是使用 Godbolt 的最新 GCC 和 ICC 与 -O2 -mavx512bw.

测试的

同样有趣的是,内在函数只处理 __mmask16 而不是其他类型。我没有测试太多,但看起来 ICC 不介意接受不正确的类型,但 GCC 似乎确实尝试确保掩码中只有 16 位,如果你使用内在函数。

我没有查看上述指令以及其他 __mmask* 类型变体的正确内在函数,还是有另一种方法可以在不求助于内联汇编的情况下实现相同的目的?

Intel 的文档说,"not necessary as they are auto generated by the compiler" 实际上是正确的。然而,它并不令人满意。

但要了解为什么会这样,您需要查看 AVX512 的历史。虽然此信息的 none 是官方信息,但根据证据强烈暗示。


掩码内在函数状态陷入混乱的原因可能是因为 AVX512 在多个阶段得到 "rolled out",而没有对下一阶段进行充分的前瞻性规划。

第 1 阶段: 骑士登陆

Knights Landing 添加了 512 位寄存器,只有 32 位和 64 位数据粒度。因此,掩码寄存器永远不需要超过 16 位。

当英特尔设计这些第一组 AVX512 内在函数时,他们继续为几乎所有内容添加内在函数 - 包括掩码寄存器。这就是为什么确实存在的掩码内在函数只有 16 位。而且它们只涵盖了 Knights Landing 中存在的说明。 (虽然我无法解释为什么缺少 KSHIFT

在 Knights Landing 中,面具操作速度很快(2 个周期)。但是在掩码寄存器和通用寄存器之间移动数据真的很慢(5 个周期)。因此,在何处进行掩码操作很重要,并且让用户更细粒度地控制在掩码寄存器和 GPR 之间来回移动内容是有意义的。

第 2 阶段: Skylake Purley

Skylake Purley 扩展了 AVX512 以覆盖字节粒度通道。这将掩码寄存器的宽度增加到完整的 64 位。第二轮还添加了 KADDKTEST,这在 Knights Landing 中是不存在的。

这些新掩码指令(KADDKTEST 和现有指令的 64 位扩展)是缺少其固有对应项的指令。


虽然我们不知道它们失踪的确切原因,但有一些强有力的证据支持它:

Compiler/Syntax:

在 Knights Landing 上,相同的掩码内在函数用于 8 位和 16 位掩码。没有办法区分它们。通过将它们扩展到 32 位和 64 位,它使混乱变得更糟。换句话说,英特尔一开始就没有正确设计掩模内在函数。他们决定完全放弃它们而不是修复它们。

性能不一致:

Skylake Purley 上的位交叉掩码指令很慢。虽然所有按位指令都是单周期的,但 KADDKSHIFTKUNPACK 等都是 4 个周期。但是在mask和GPR之间移动只有2个周期。

正因为如此,将它们移入 GPR 进行处理并移回它们通常会更快。但是程序员不太可能知道这一点。因此,英特尔没有让用户完全控制掩码寄存器,而是选择让编译器做出这个决定。

让编译器做出这个决定,意味着编译器需要有这样的逻辑。英特尔编译器目前的做法是在某些(罕见)情况下生成 kadd 和系列。但是海湾合作委员会没有。在 GCC 上,除了最琐碎的掩码操作外,所有操作都将移至 GPR 并在那里完成。


最后的想法:

在 Skylake Purley 发布之前,我个人写了很多 AVX512 代码,其中包括很多 AVX512 掩码代码。这些是根据某些性能假设(单周期延迟)编写的,这些假设在 Skylake Purley 上被证明是错误的。

根据我自己在 Skylake X 上的测试,我的一些依赖于位交叉操作的掩码固有代码结果比将它们移动到 GPR 并返回的编译器生成的版本慢。原因当然是 KADDKSHIFT 是 4 个周期而不是 1 个。

当然,如果英特尔确实提供了内在函数来为我们提供我想要的控制权,我更愿意这样做。但是如果你不知道自己在做什么,这里很容易出错(就性能而言)。


更新:

目前还不清楚这是什么时候发生的,但最新版本的英特尔内在函数指南有一组新的掩码内在函数和新的命名约定,涵盖了所有指令和宽度。这些新内在函数取代旧内在函数。

所以这解决了整个问题。尽管编译器支持的程度仍不确定。

示例:

  • _kadd_mask64()
  • _kshiftri_mask32()
  • _cvtmask16_u32() 取代 _mm512_mask2int()