可以通过使用输入寄存器来寻址输出 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 寄存器的主要内容,如果未能解决你的问题,请参考以下文章