如何实现四个 i8 元素组的高效 _mm256_madd_epi8 点积?

2024-01-12

Intel 提供了一个名为 _mm256_madd_epi16 的 C 风格函数,基本上

__m256i _mm256_madd_epi16(__m256i a,__m256i b)

将 a 和 b 中的压缩有符号 16 位整数相乘,生成中间有符号 32 位整数。将相邻的中间 32 位整数对水平相加,并将结果打包到 dst 中。

现在我有两个 __m256i 变量,每个变量都有 32 个 8 位 int 。

我想实现相同的功能_mm256_madd_epi16确实如此,但结果 __m256i 中的每个 int32_t 元素都是四个有符号字符的乘积之和,而不是两对有符号字符的乘积之和int16_t.四的点积int8_t每个 32 位块内的元素。

我可以在标量循环中做到这一点:

  alignas(32) uint32_t res[8] = {0};
  for (int i = 0; i < 32; ++i)
      res[i / 4] += _mm256_extract_epi8(a, i) * _mm256_extract_epi8(b, i);
  return _mm256_load_si256((__m256i*)res);

Note that the multiply result is sign-extended to int before adding, and that the _mm256_extract_epi8 helper function1 . Nevermind that the total is uint32_t instead of int32_t; it can't overflow anyway with only four 8x8 => 16-bit numbers to add.

它看起来非常丑陋,并且无法有效运行,除非编译器使用 SIMD 进行一些魔法,而不是按照写入标量提取的方式进行编译。


脚注1:_mm256_extract_epi8不是内在的。vpextrb仅适用于 256 位向量的低通道,并且此辅助函数可能允许不是编译时常量的索引。


pmaddubsw http://felixcloutier.com/x86/PMADDUBSW.html:如果至少一个输入为非负(因此可以被视为无符号),则可用

如果已知您的输入之一始终为非负数,则可以将其用作无符号输入pmaddubsw; 8->16 位等价于pmaddwd。它添加了对u8*i8 -> i16产品,带符号饱和度为 16 位。但如果一次输入最多为 127 而不是 255,饱和是不可能的。(127*-128 = -0x3f80,所以两倍仍然适合 i16。)

After pmaddubsw, use pmaddwd反对_mm256_set1_epi16(1)对元素对进行 hsum 并正确处理符号。 (这通常比手动将 16 位元素符号扩展至 32 位来相加更有效。)

__m256i sum16 = _mm256_maddubs_epi16(a, b);   // pmaddubsw
__m256i sum32 = _mm256_madd_epi16(sum16, _mm256_set1_epi16(1)); // pmaddwd

(pmaddwd对于 4 字节元素内的水平 16=>32 位对和,在某些 CPU 上的延迟比移位 / 和 / 添加更高,但确实将两个输入视为有符号以进行符号扩展至 32 位。而且它只是一个微指令,因此对吞吐量有好处,特别是如果周围的代码不会在相同的执行端口上出现瓶颈。)


一般情况(两个输入都可能为负)

最近的一个回答AVX-512BW _mm512_dpbusd_epi32 AVX-512VNNI 指令仿真 https://stackoverflow.com/questions/67999580/avx-512bw-emulation-of-mm512-dpbusd-epi32-avx-512vnni-instruction/76327936#76327936想出了一个好技巧,将一个输入分成 MSB 和低 7 位,这样vpmaddubsw (_mm256_maddubs_epi16)可以在没有溢出的情况下使用。我们可以借用这个技巧并在求和时求反,因为 MSB 的位值为-2^7而不是2^7的无符号输入vpmaddubsw将其视为。

// Untested.  __m128i version would need SSSE3
__m256i dotprod_i8_to_i32(__m256i v1, __m256i v2)
{
    const __m256i highest_bit = _mm256_set1_epi8(0x80);

    __m256i msb = _mm256_maddubs_epi16(_mm256_and_si256(v1, highest_bit), v2);     // 0 or 2^7
    __m256i low7 = _mm256_maddubs_epi16(_mm256_andnot_si256(highest_bit, v1), v2);

    low7 = _mm256_madd_epi16(low7, _mm256_set1_epi16(1));  // hsum i16 pairs to i32
    msb  = _mm256_madd_epi16(msb,  _mm256_set1_epi16(1));
    return _mm256_sub_epi32(low7, msb);  // place value of the MSB was negative

   // equivalent to the below, but that needs an extra constant
//    msb = _mm256_madd_epi16(msb,  _mm256_set1_epi16(-1));   // the place-value was actually - 2^7
//    return _mm256_add_epi32(low7, msb);

   // also equivalent to vpmaddwd with -1 for both parts
   // return sub(msb, low7)
   // which is cheaper because set1(-1) is just vpcmpeqd not a load.
}

这可以避免有符号饱和:一侧的最大乘数为 128(MSB 被设置并视为无符号)。128 * -128= -16384,两倍,即 -32768 = -0x8000 = 位模式 0x8000。或者128 * 127 * 2= 0x7f00 作为最高的正结果。

对于下面的版本,这是 7 uops(4 个乘法单元)与 9 uops(4 个移位 + 2 个乘法)。

AVX-512VNNI_mm256_dpbusd_epi32(或 512),或 AVX_VNNI_mm256_dpbusd_avx_epi32 (VPDPBUSD https://www.felixcloutier.com/x86/vpdpbusd) 就好像vpmaddubsw (u8*i8产品),但添加到现有总和,并在单个指令中对一个字节内的 4 个产品求和。 (i32 += four u8 * i8)。同样的分割技巧也有效,_mm256_sub_epi32(low7_prods, msb_prods)但我们可以跳过madd_epi16 (vpmaddwd) i16 到 i32 水平总和步长。

(Other VNNI https://en.wikipedia.org/wiki/AVX-512#VNNI说明包括vpdpbusds(与...一样vpdpbusd但用有符号饱和而不是换行)。不管怎样,饱和度是i32,而不是i16vpmaddubsw,因此仅当累加器输入非零时才会饱和。如果一个输入为非负数,那么可以将其视为无符号,这将在一条指令中完成整个工作,而无需拆分。和vpdpwssd[s],带有或不带有饱和度的签名词的 MAC,例如vpmaddwd但带有累加器操作数。)

// Ice Lake (AVX-512 version only) or Alder Lake (AVX_VNNI), or Zen 4
__m256i dotprod_i8_to_i32_vnni(__m256i v1, __m256i v2)
{
    const __m256i highest_bit = _mm256_set1_epi8(0x80);
    __m256i msb = _mm256_and_si256(v1, highest_bit);
    __m256i low7 = _mm256_andnot_si256(highest_bit, v1);

   // or just _mm256_dpbusd_epi32 for the EVEX version
    msb = _mm256_dpbusd_avx_epi32(_mm256_setzero_si256(), msb, v2);     // 0 or 2^7
    low7 = _mm256_dpbusd_avx_epi32(_mm256_setzero_si256(), low7, v2);

    return _mm256_sub_epi32(low7, msb);  // place value of the MSB was negative
}

没有 AVX-512VNNI 的 AVX-512 可以不加更改地使用 AVX2 版本,或扩大到 512。或者可以通过移位将其转换为掩码来应用符号位(vptestmb)并将输入的一些字节归零(零掩码vpmovdqu8) 将 4 字节块水平求和为 32 位元素 (vdbpsadbw https://www.felixcloutier.com/x86/vdbpsadbw与身份洗牌控制的零)。但不,在添加 8 位输入之前不会对其进行符号扩展,因为它是无符号差异。也许首先将范围转移到无符号(例如,零掩码异或0x80)然后添加4*128?无论如何,那么msb = _mm256_slli_epi32(dword_hsums_of_input_b, 7)使用方式与上面的代码使用它的方式相同msb多变的。如果这有效的话,我不知道它是否可以节省微指令。欢迎反馈,或发布 AVX-512BW 答案。


另一种方式:解包并符号扩展为 16 位

显而易见的解决方案是将输入字节解压缩为带有零或符号扩展的 16 位元素。然后你可以使用pmaddwd两次,并将结果相加。

如果您的输入来自内存,则加载它们vpmovsxbw可能有道理。例如

__m256i a = _mm256_cvtepi8_epi16(_mm_loadu_si128((const __m128i*)&arr1[i]);
__m256i b = _mm256_cvtepi8_epi16(_mm_loadu_si128((const __m128i*)&arr2[i]);

但现在你有了想要分散的 4 个字节two双字,所以你必须打乱其中一个的结果_mm256_madd_epi16(a,b)。你也许可以使用vphaddd打乱并将两个 256 位乘积向量添加到您想要的一个 256 位结果向量中,但这需要大量打乱。

因此,我认为我们希望从每个 256 位输入向量生成两个 256 位向量:一个将每个字中的高字节符号扩展为 16,另一个将低字节符号扩展。我们可以通过 3 个轮班来做到这一点(对于每个输入)

 __m256i a = _mm256_loadu_si256(const  __m256i*)&arr1[i]);
 __m256i b = _mm256_loadu_si256(const  __m256i*)&arr2[i]);

 __m256i a_high = _mm256_srai_epi16(a, 8);     // arithmetic right shift sign extends
     // some compilers may only know the less-descriptive _mm256_slli_si256 name for vpslldq
 __m256i a_low =  _mm256_bslli_epi128(a, 1);   // left 1 byte = low to high in each 16-bit element
         a_low =  _mm256_srai_epi16(a_low, 8); // arithmetic right shift sign extends

    // then same for b_low / b_high

 __m256i prod_hi = _mm256_madd_epi16(a_high, b_high);
 __m256i prod_lo = _mm256_madd_epi16(a_low, b_low);

 __m256i quadsum = _m256_add_epi32(prod_lo, prod_hi);

作为替代方案vplldq按 1 个字节,vpsllw按 8 位__m256i a_low = _mm256_slli_epi16(a, 8);是在每个单词中从低到高移动的更“明显”的方法,如果周围的代码在随机播放时遇到瓶颈,可能会更好。但通常情况下情况会更糟,因为this代码在 shift + vec-int 乘法上存在严重瓶颈。

在 KNL 上,您可以使用 AVX512vprold z,z,i(Agner Fog 没有显示 AVX512 的时间vpslld z,z,i)因为你将什么移位或洗牌到每个单词的低字节并不重要;这只是算术右移的设置。

执行端口瓶颈:

Haswell 仅在端口 0 上运行向量移位和向量整数乘法,因此这会造成严重瓶颈。 (Skylake 更好:p0/p1)。http://agner.org/optimize/ http://agner.org/optimize/.

我们可以使用随机播放(端口 5)代替左移作为算术右移的设置。这可以提高吞吐量,甚至通过减少资源冲突来减少延迟。

But 我们可以通过使用来避免洗牌控制向量vpslldq进行向量字节移位。它仍然是通道内洗牌(在每个通道末尾移入零),因此它仍然具有单周期延迟。 (我的第一个想法是vpshufb与控制向量类似14,14, 12,12, 10,10, ..., then vpalignr,然后我想起了那个简单的老pslldq有AVX2版本。同一条指令有两个名称。 我喜欢因为b与元素内位移不同,字节移位将其区分为随机播放。我没有检查哪个编译器支持 128 位或 256 位版本的内在函数的名称。)

这对 AMD Zen 1 也有帮助。向量移位仅在一个执行单元 (P2) 上运行,但洗牌可以在 P1 或 P2 上运行。

我没有研究过 AMD Ryzen 执行端口冲突,但我很确定这在任何 CPU 上都不会更糟(KNL Xeon Phi 除外,其中对小于双字的元素的 AVX2 操作都非常慢)。移位和通道内洗牌具有相同的微指令数和相同的延迟。

如果任何元素已知为非负,则符号扩展 = 零扩展

(或者更好的是,使用pmaddubsw如第一节所示。)

零扩展比手动符号扩展更便宜,并且避免了端口瓶颈。a_low and/or b_low可以创建为_mm256_and_si256(a, _mm256_set1_epi16(0x00ff)).

a_high and/or b_high可以通过随机播放而不是移位来创建。 (pshufb当洗牌控制向量具有其高位设置时将元素归零)。

 const _mm256i pshufb_emulate_srl8 = _mm256_set_epi8(
               0x80,15, 0x80,13, 0x80,11, ...,
               0x80,15, 0x80,13, 0x80,11, ...);

 __m256i a_high = _mm256_shuffle_epi8(a, pshufb_emulate_srl8);  // zero-extend

在主流 Intel 上,随机播放吞吐量也限制为每个时钟 1,因此如果过度,可能会出现随机播放瓶颈。但至少它与乘法不是同一个端口。如果仅知道高字节为非负,则替换vpsra/lw with vpshufb有帮助。未对齐的负载,因此那些高字节是低字节可能会更有帮助,设置为vpand for a_low and/or b_low.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何实现四个 i8 元素组的高效 _mm256_madd_epi8 点积? 的相关文章

随机推荐

  • 安装 Visual Studio 2013 后出现 Visual Studio 问题

    我有 Windows 8 机器 我的计算机上安装了 Visual Studio 2010 和 Visual Studio 2012 我安装了 Visual Studio 2013 预览版和 8 1 WDK 安装 Visual Studio
  • iOS7 Webview导航栏下初始滚动位置

    我有一个网络视图 它在导航栏下方根据需要滚动 但是 当我第一次加载控制器时 Web 视图中加载的页面会滚动 以便它与导航栏的顶部对齐 当我滚动网页视图时 正确的插图出现在顶部以正确放置 只是初始位置不正确 如何使初始位置完全滚动到顶部 包括
  • Terraform,如何在现有资源上运行配置程序?

    我的问题与这个 github 帖子类似 https github com hashicorp terraform issues 745 https github com hashicorp terraform issues 745 这也与我
  • 我可以检查指针是否由 malloc/calloc/realloc 分配吗? [复制]

    这个问题在这里已经有答案了 我想知道是否可以检查传递给函数的指针是否由 malloc calloc realloc 分配 int main struct something o struct something a a malloc siz
  • Spark 如何使用图像格式读取我的图像?

    这可能是一个愚蠢的问题 但我无法弄清楚 Spark 如何使用spark read format image load 争论 导入我的图像后 它给出以下内容 gt gt gt image df select image height imag
  • 一个存储库中的多个微服务

    我对微服务和存储库有疑问 我们是一个小团队 5 人 我们在微服务中创建新项目 我们项目中预期的微服务应用程序在 10 15 个之间 我们正在考虑为所有微服务建立一个存储库 其结构如下 app1 app2 app3 script sh scr
  • 来自控制台的 ActionCable.server.broadcast 始终返回 0

    我试图让 actioncable 工作 但是当我在 Rails 控制台中发送 ActionCable server broadcast discussion asdf true 时 它总是返回 0 我已经在cable yml 中运行并配置了
  • 如何在 macOS 10.14 上重置 HIDIdleTime

    在过去的几天里 我一直在尝试编写一个应用程序来重置IORegistry gt IOHIDSystem gt HIDIdleTime入口 最终目标是防止其他读取该值的应用程序将用户标记为空闲 这不仅与电源管理或防止睡眠有关 假设沙箱已禁用并且
  • 如何判断我的页面选项卡应用程序安装在哪个 Facebook 页面上 [重复]

    这个问题在这里已经有答案了 可能的重复 如何查明哪个主页安装了我的 Facebook Canvas 应用程序 http facebook stackoverflow com questions 5587784 how can i find
  • Python 字节码在 CPython 中运行究竟如何?

    我试图了解 Python 是如何工作的 因为我一直在使用它 据我了解 当您运行 python script py 之类的东西时 脚本会转换为字节码 然后解释器 VM CPython 实际上只是一个 C 程序 读取 python 字节码并相应
  • 提取最后一次出现模式 C# 后的所有字符

    字符串具有以下模式 1 0 0 0 1 0 0 1 1 0 0 2 我正在寻找一个代码 它将读取最后创建的字符串并将最后一个数字增加 1 并将其保存为新字符串 我该怎么做 此致 Magic 如果您的意图是始终获取特定字符之后的最后一个子字符
  • 在 SYB 中匹配更高种类的类型

    一般来说 我想知道是否有一种方法可以编写一个通用折叠来概括应用一个函数forall键入如下 f forall a Data D a gt D a gt b 给定某种数据类型D为此instance Data D a 可能有限制a 具体来说 考
  • 如何使用JavaScript判断目录中是否存在文件?

    如何使用javascript判断目录中是否存在文件 如果它在服务器上 你可以做一个HTTP头 http www w3 org Protocols rfc2616 rfc2616 sec9 html sec9 4通过 Ajax 请求 并查看
  • PhpStorm v2017.3.4 与 Codeception v2.4.0 不兼容

    我正在努力使代码接收与 PhpStorm 一起工作 我已经设置了 CLI 解释器 我还设置了 Codeception 这是 Codeception 的配置 当我从 PhpStorm 运行测试时 我收到以下信息 在左侧 以及右侧的文字 Tes
  • 实现不同类型数组集合的更好方法

    我正在寻找 C 中的半通用数据结构来存储不同整数和浮点类型的数组 在某些情况下 整数是位字段 其中每个位都同等重要 并且不能容忍精度损失 由于 C 类型系统和我对 C 的不熟练 我发现这既困难又混乱 项目 Ethercat周期性数据包到达并
  • 使用 AppCompat 时如何更改 ActionBar 标题字体

    我想将自定义字体应用于 ActionBar 上显示的应用程序标题 以前我没有使用任何支持库和这个解决方案 int titleId getResources getIdentifier action bar title id android
  • 如何创建/在哪里获取多个实例的 Wix Bootstrapper

    我目前正在学习如何使用 WiX 创建 msi 安装程序 但似乎遇到了困难 当我创建一个包含单个环境所需的所有内容的 MSI 时 一切都很好 我有功能和组件 并且可以安装我的 MSI 现在我正处于想要将其转换为 SQL Server 样式安装
  • R 中邻居的空间数据/计算指标

    我有 xBin yBin value 形式的二维空间数据 例如 DT data table x c rep 1 3 rep 2 3 rep 3 3 y rep c 1 2 3 3 value 100 c 1 9 对于每个垃圾箱 我想计算所有
  • 从 JS 文件中获取 Vue

    我对现代前端开发工具还很陌生 我安装了 Nodejs 和 NPM 下载了一些软件包 例如 jquery 并且一切正常 然后我安装了 Webpack 第 2 版 我创建了这个演示配置文件 module exports entry entry
  • 如何实现四个 i8 元素组的高效 _mm256_madd_epi8 点积?

    Intel 提供了一个名为 mm256 madd epi16 的 C 风格函数 基本上 m256i mm256 madd epi16 m256i a m256i b 将 a 和 b 中的压缩有符号 16 位整数相乘 生成中间有符号 32 位