可以通过使用输入寄存器来寻址输出 SIMD 寄存器

Posted

技术标签:

【中文标题】可以通过使用输入寄存器来寻址输出 SIMD 寄存器【英文标题】:Is possible to address the output SIMD register by using an input register 【发布时间】:2015-04-29 22:11:38 【问题描述】:

是否可以使用输入向量的标量值来索引输出向量?我尝试在 SIMD 中实现以下功能,但找不到任何解决方案。

 void shuffle(unsigned char * a,    // input a
              unsigned char * r)   // output r
     for (i=0; i < 16; i++)
            r[i] = 0;
     for (i=0; i < 16; i++)
            r[a[i] % 16] = 1;
 

输入/输出向量示例如下所示

unsigned char * a = 0, 0, 0, 10, 0, 0, 0, 2, 0, 0, 0, 0, 3, 1, 0, 0 ;
... do SIMD magic
//                   0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
unsigned char * r = 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 ;

我无法找到任何合适的指令来动态处理分配的左侧。也许这个功能可以通过移位操作来实现?有人实现了类似的东西吗?

【问题讨论】:

不,你不能轻易做到这一点。最接近的是_mm_shuffle_epi8,这是一个通用置换,但我看不到任何明显的方法可以在这里应用它。你真的想要这条指令的逆,它不存在。 在你的例子中,元素 r[0] 不应该也是 1 吗? 是的,我在考虑是否可以使用 _mm_shuffle_epi8 指令。但我想不出解决办法。我很害怕听到这个。感谢您的快速回答。是的,r[0] 应该是 1。 FWIW 你可以用一个循环来做,这并不比原来的标量版本更有效,但如果这要与一堆其他 SIMD 代码混合,那么它可能是值得的,为了避免在 SIMD 指令流中间出现标量代码。 是否考虑将寄存器移位 16 次并与索引位置进行比较? 【参考方案1】:

看来 _mm_shuffle_epi8 确实是解决问题的关键。这个想法是根据输入向量 a 的值设置各个位。 这些位分布在(水平或)128 位宽的字节上 注册。

#include <stdio.h>
#include <immintrin.h>
/*  gcc -O3 -Wall -mavx test4.c                                  */
/*  gcc -O3 -Wall -msse2 -mssse3 -msse4.1 test4.c                */

int print_char128(__m128i * x);

int print_char128(__m128i * x)
  unsigned char v_x[16];
  _mm_storeu_si128((__m128i *)v_x,*x);
  printf("%4u %4u %4u %4u | %4u %4u %4u %4u | %4u %4u %4u %4u | %4u %4u %4u %4u  \n",
  v_x[0],  v_x[1],  v_x[2],  v_x[3],  v_x[4],  v_x[5],  v_x[6],  v_x[7],
  v_x[8],  v_x[9],  v_x[10], v_x[11], v_x[12], v_x[13], v_x[14], v_x[15] );
  return 0;



int main()

unsigned char  a_v[] = 0, 0, 0, 10, 0, 0, 0, 2, 0, 0, 0, 0, 3, 1, 0, 0 ;
/*unsigned char  a_v[] = 13, 30, 0, 10, 0, 6, 0, 2, 0, 0, 7, 0, 3, 11, 0, 0 ;*/
  __m128i t0, t1, t2, t3;
  __m128i a, r, msk0, msk1, msk0_1, zero, bin_ones, one_epi8;

  /* set some constants */
  unsigned char  msk0_v[] =1, 2, 4, 8, 16, 32, 64, 128, 0, 0, 0, 0, 0, 0, 0, 0;
  msk0=_mm_loadu_si128((__m128i *)msk0_v);
  msk1=_mm_shuffle_epi32(msk0,0b01001110);
  msk0_1=_mm_blend_epi16(msk0,msk1,0b11110000);
  zero=_mm_setzero_si128();
  bin_ones=_mm_cmpeq_epi32(zero,zero);
  one_epi8=_mm_sub_epi8(zero,bin_ones);

  /* load indices */
  a=_mm_loadu_si128((__m128i *)a_v);

  /* start of 'SIMD magic'                                            */
  /* index a_i sets the a_i -th bit within a byte of t0 if 0<=a_i<8  */
  /* or set (a_i-8)-th bit within a byte of t1 if 8<=a_i<16          */
  t0=_mm_shuffle_epi8(msk0,a);
  t1=_mm_shuffle_epi8(msk1,a);
  /* horizontal OR of the bytes in t0 and t1: */
  t2=_mm_blend_epi16(t0,t1,0b11110000);
  t3=_mm_alignr_epi8(t1,t0,8);
  t0=_mm_or_si128(t2,t3);
  t1=_mm_shuffle_epi32(t0,0b10110001);
  t0=_mm_or_si128(t0,t1);
  t1=_mm_slli_si128(t0,2);
  t0=_mm_or_si128(t0,t1);
  t1=_mm_slli_si128(t0,1);
  t0=_mm_or_si128(t0,t1);
  t0=_mm_shuffle_epi32(t0,0b11110101);  /* end of horizontal OR */
  /* filter out the relevant bits */
  t0=_mm_and_si128(t0,msk0_1);
  t0=_mm_cmpeq_epi8(t0,zero);
  r=_mm_andnot_si128(t0,one_epi8);      /* the result is in r */
  print_char128(&r);

  return 0;

这应该工作得很快:除了设置常量和加载数据的指令之外,它只有 15 个 SSEx 指令。在今天的处理器上,这些指令都只有 1 个周期的延迟。 (倒数)吞吐量甚至更小:1/2 或 1/3 周期。 内在 _mm_blend_epi16 是 SSE4.1,其他一些是 SSSE3。

【讨论】:

哇,您的解决方案看起来很棒。非常感谢分享。我仍然试图了解所有细节。 您不需要零作为_mm_cmpeq 的输入来生成全1。任何输入都等于它自己。当两个操作数相同时,CPU 甚至通过生成不依赖于寄存器先前值的微指令来利用这一点。 (即它被认为是一个依赖破坏指令。) NVM,看起来您正在算法中使用您的zero reg。否则,您可以从_mm_sign_epi8(bin_ones, bin_ones) 获得one_epi8。我以为有一个普通的向量否定指令,但可能没有。

以上是关于可以通过使用输入寄存器来寻址输出 SIMD 寄存器的主要内容,如果未能解决你的问题,请参考以下文章

Linux汇编教程04:寻址方式

STM32输入上拉下拉 寄存器怎么设置实现

将数据放入 SIMD 寄存器需要多少个周期?

汇编书中“存于寄存器内的地址可用来指向内存的某个位置,即寻址”

汇编寄存器,寻址方式,lea指令解释

汇编寄存器,寻址方式,lea指令解释