在AVX寄存器(__m256i)中设置单个位,需要使用“随机访问”运算符。

4

因此,我想设置一个__m256i寄存器的单个位。

假设我的__m256i包含:[1 0 1 0 | 1 0 1 0 |...| 1 0 1 0],如何设置和取消第n位?


2
最简单的方法是创建一个包含256个不同掩码值的查找表,并使用n作为索引来获取设置/清除位的掩码。 - phuclv
你有没有给我一个例子? - S.H
1
只需从LUT[n]加载掩码向量,然后使用_mm256_or_si256 - Paul R
4个回答

4
如果您想避免LUT和/或存储转发停顿,可以使用以下方法设置AVX-256寄存器的第k位:
inline __m256i setbit_256(__m256i x,int k){
// constants that will (hopefully) be hoisted out of a loop after inlining  
  __m256i indices = _mm256_set_epi32(224,192,160,128,96,64,32,0);
  __m256i one = _mm256_set1_epi32(-1);
  one = _mm256_srli_epi32(one, 31);    // set1(0x1)


  __m256i kvec = _mm256_set1_epi32(k);  
// if 0<=k<=255 then kvec-indices has exactly one element with a value between 0 and 31
  __m256i shiftcounts = _mm256_sub_epi32(kvec, indices);
  __m256i kbit        = _mm256_sllv_epi32(one, shiftcounts);   // shift counts outside 0..31 shift the bit out of the element
                                                               // kth bit set, all 255 other bits zero.
  return _mm256_or_si256(kbit, x);                             // use _mm256_andnot_si256 to unset the k-th bit
}

以下是我的之前的回答,不够直接并且现在已经过时。
#include <immintrin.h>

inline __m256i setbit_256(__m256i x,int k){
  __m256i c1, c2, c3;
  __m256i t, y, msk;

  // constants that will (hopefully) be hoisted out of a loop after inlining
  c1=_mm256_set_epi32(7,6,5,4,3,2,1,0);
  c2=_mm256_set1_epi32(-1);
  c3=_mm256_srli_epi32(c2,27);     // set1(0x1f) mask for the shift within elements
  c2=_mm256_srli_epi32(c2,31);     // set1(0x1)

  // create a vector with the kth bit set
  t=_mm256_set1_epi32(k);
  y=_mm256_and_si256(c3,t);        // shift count % 32: distance within each elem
  y=_mm256_sllv_epi32(c2,y);       // set1( 1<<(k%32) )

  t=_mm256_srli_epi32(t,5);        // set1( k>>5 )
  msk=_mm256_cmpeq_epi32(t,c1);    // all-ones in the selected element
  y=_mm256_and_si256(y,msk);       // kth bit set, all 255 other bits zero.

  x=_mm256_or_si256(y,x);   /* use _mm256_andnot_si256 to unset the k-th bit */
  return x;
}

我不确定这是否比其他答案中提出的方法更快。
这段代码使用clang或gcc编译后生成了非常好的汇编代码。在考虑常量将被提取出循环之后,使用Godbolt编译器探索器。通常情况下,clang会打败试图在运行时生成常量的尝试,并从内存中广播加载它们(这对现代CPU来说非常高效)。

使用broadcast+cmpeq来选择应该包含“1”位的向量元素非常巧妙。对于位位置不是编译时常量的情况,这可能至少在延迟方面是最优的。(当它是编译时常量时,ermlg的模板答案有望编译成一个带有预计算常量的单个VPANDN或VPOR)。 - Peter Cordes
1
我对你的代码进行了注释,因为没有描述性的变量名,很难理解。如果你觉得不合适,可以回滚我的修改。由于SO没有让我留下建议等待你的批准的选项,所以我直接进行了修改。 - Peter Cordes
1
我还在考虑实际使用 set1( 1U << (k&31) )set1( k & ~31U ),因为标量移位可以在端口6上运行(不会与向量ALU uops竞争)。因此,它把一些标量指令和额外的 MOVD + VPBROADCASTD 交换成了3个向量指令。我把三个版本都放在了Godbolt上:https://godbolt.org/g/F3NqdW。你应该把最终版本作为答案的主要部分,它绝对是最好的。(不要只是“更新”,重新排列你的答案以首先展示它,如果想要的话,可以将你早期的想法作为脚注。或者只需说“请参阅早期想法的历史记录”。) - Peter Cordes
顺便提一下,在我包含的那个godbolt链接中,我使用了有意义的变量名来编写你的好函数版本。看看它,看看你对这种编码风格的看法。如果你的变量名有意义,你就不需要总是写那么多注释。正如你从汇编输出中所看到的,现代编译器在使用新命名变量而不是重复使用相同临时变量时并不会生成更糟糕的代码。 - Peter Cordes
是的,它不是免费的,这是一个重要的事情要意识到,但据我所知,使用特定的广播内在函数没有任何优势。你得到的只是无法编译为回退的代码(例如没有AVX的__m128代码)。或者使用AVX1而不是AVX2,可能会存储到内存并广播加载浮点数(因为VBROADCASTPS的寄存器源形式仅限于AVX2)。 (尽管我忘记了在测试时是否曾在编译器输出中看到过这个)。然而缺少的内在函数是标量浮点数和向量之间的转换(保留高垃圾):https://dev59.com/q-k6XIcBkEYKwwoYAfO1 - Peter Cordes
显示剩余10条评论

4

这是一个可以在向量中设置单个位的函数实现:

#include <immintrin.h>
#include <assert.h>

void SetBit(__m256i & vector, size_t position, bool value)
{
    assert(position <= 255);
    uint8_t lut[32] = { 0 };
    lut[position >> 3] = 1 << (position & 7);
    __m256i mask = _mm256_loadu_si256((__m256i*)lut);
    if (value)
        vector = _mm256_or_si256(mask, vector);
    else
        vector = _mm256_andnot_si256(mask, vector);
}

int main(int argc, char* argv[])
{
    __m256i a = _mm256_set1_epi8(-1);
    SetBit(a, 54, false);

    __m256i b = _mm256_set1_epi8(0);
    SetBit(b, 54, true);

    return 0;
}

1
只是为了让其他人清楚,由于存储转发停顿,这不是一种有效的方法。因此,这不是您想要放入性能关键循环中的内容。不幸的是,SIMD并不是为此而设计的。因此,可能没有有效的方法来完成它。我想移位+排列可能更快,但也更加复杂。 - Mysticial
在Fortran90中有IBSET、IBSHFT、IBTEST等内置函数。因此,使用混合编程语言的解决方案可能是值得的。 - Holmz
存储转发主要是延迟问题,而不是吞吐量问题,对吗? - BeeOnRope
1
@BeeOnRope:就记录而言,在Sandybridge系列上,慢路径存储转发(又称停顿)不能相互重叠,所以如果你在循环中使用它,也是一个吞吐量问题。但是,成功的存储转发可以与SF停顿进行流水线处理。在x86上,失败的存储到加载转发的成本是多少?在Skylake上进行了测试。 - undefined
@PeterCordes - 谢谢,你说得对! - undefined

4

还有另一种实现方法:

#include <immintrin.h>
#include <assert.h>

template <bool value> void SetMask(const __m256i & mask, __m256i & vector);

template <> inline void SetMask<true>(const __m256i & mask, __m256i & vector)
{
    vector = _mm256_or_si256(mask, vector);
}

template <> inline void SetMask<false>(const __m256i & mask, __m256i & vector)
{
    vector = _mm256_andnot_si256(mask, vector);
}

template <int position, bool value> void SetBit(__m256i & vector)
{
    const uint8_t mask8 = 1 << (position & 7);
    const __m128i mask128 = _mm_insert_epi8(_mm_setzero_si128(), mask8, (position >> 3)&15);
    const __m256i mask256 = _mm256_inserti128_si256(_mm256_setzero_si256(), mask128, position >> 7);
    SetMask<value>(mask256, vector);
}

int main(int argc, char* argv[])
{
    __m256i a = _mm256_set1_epi8(-1);
    SetBit<50, false>(a);

    __m256i b = _mm256_set1_epi8(0);
    SetBit<50, true>(b);

    return 0;
}

3
请允许我说明,这不能用于运行时索引。 - marek094

2
如果您想避免使用LUT,可以使用BTS设置单个位(或BTR清除它)。这个指令在GCC中似乎没有内置函数,因此需要使用内联汇编(仅适用于x86架构)。

0F AB /r --- BTS r/m32, r32 --- 存储所选位于CF标志中并将其设置。

它们与存储器操作数一起运行非常慢,但是这些位串指令允许超出寻址模式引用的字节或双字的位偏移量。手册解释如下:

有些汇编程序支持使用内存操作数的位移字段和立即位移字段结合使用来支持大于31的立即位移。在这种情况下,立即位移字段中存储低3位或5位(16位操作数为3位,32位操作数为5位)立即位移,组合位移字段和立即位移以及地址模式中的字节位移字段。如果不为零,则处理器将忽略高阶位。

当访问存储器中的位时,处理器可能会访问从内存地址开始的4个字节,对于32位操作数大小,使用以下关系式:

Effective Address + (4 ∗ (BitOffset DIV 32))

在纯汇编(Intel-MASM语法)中,它看起来像这样:

.data
  .align 16
  save db 32 dup(0)    ; 256bit = 32 byte YMM/__m256i temp variable space
  bitNumber dd 254     ; use an UINT for the bit to set (here the second to last)
.code
  mov eax, bitNumber
  ...
  lea edx, save
  movdqa xmmword ptr [edx], xmm0    ; save __m256i to to memory
  bts dword ptr [edx], eax          ; set the 255st bit
  movdqa xmm0, xmmword ptr [edx]    ; read __m256i back to register
  ...

如果变量已经在内存中,这将会更加容易。
使用内联汇编,这将导致以下函数:
static inline
void set_m256i_bit(__m256i * value, uint32_t bit)
{
    // doesn't need to be volatile: we only want to run this for its effect on *value.
    __asm__ ("btsl %[bit], %[memval]\n\t"
             : [memval] "+m" (*value) : [bit] "ri" (bit));
}

static inline
void clear_m256i_bit(__m256i * value, uint32_t bit)
{
    __asm__ ( "btrl %[bit], %[memval]\n\t"
              : [memval] "+m" (*value) : [bit] "ri" (bit));
}

这些代码编译后的结果可以在Godbolt编译器浏览器上查看,点击这里
以下是一些类似于上面汇编代码的测试代码:
__m256i value = _mm256_set_epi32(0,0,0,0,0,0,0,0);
set_m256i_bit(&value,254);
clear_m256i_bit(&value,254);

你知道在最近的英特尔CPU上,带有内存操作数的BTS指令需要超过10个微操作吗?这正是因为它疯狂的位串寻址方式,其中要修改的字节(或双字)的地址不仅仅由寻址模式决定。并且在重新加载时仍会导致存储转发停顿。尽管如此,这仍然很有趣。 - Peter Cordes
我相信你可以很容易地通过使用矢量移位+混洗(使用整数指令生成的混洗掩码并使用pmovzx或类似方法扩展)来轻松击败它。避免存储转发停顿是非常重要的。 - Peter Cordes
让我们在聊天中继续这个讨论,我们已经把有关投票的离题评论移到那里了。祝好,继续为你的答案努力。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接