c - 固有地基于BitMask设置数组中的值

标签 c x86 bit-manipulation intel intrinsics

是否有一个内在函数将在输入数组的所有位置设置一个值,其中相应的位置在提供的位掩码中有一个1位?
10101010是位掩码
值为121
它将用值121设置位置0、2、4、6

最佳答案

使用AVX512,是的。蒙面商店是AVX512中的一流操作。
使用_mm512_mask_storeu_epi8 (void* mem_addr, __mmask64 k, __m512i a)vmovdqu8将位掩码用作数组向量存储的AVX512掩码。(平均512BW。对于AVX512F,只能使用32或64位元素大小。)

#include <immintrin.h>
#include <stdint.h>

void set_value_in_selected_elements(char *array, uint64_t bitmask, uint8_t value) {
    __m512i broadcastv = _mm512_set1_epi8(value);
    // integer types are implicitly convertible to/from __mmask types
    // the compiler emits the KMOV instruction for you.
    _mm512_mask_storeu_epi8 (array, bitmask, broadcastv);
}

它将(with gcc7.3 -O3 -march=skylake-avx512)编译为:
    vpbroadcastb    zmm0, edx
    kmovq   k1, rsi
    vmovdqu8        ZMMWORD PTR [rdi]{k1}, zmm0
    vzeroupper
    ret

如果要在位图为零的元素中写入零,请使用零遮罩移动从遮罩创建常量并存储它,或者使用AVX512BW或DQ__m512i _mm512_movm_epi8(__mmask64 )创建0/-1向量。其他元件尺寸可供选择。但是,当数组大小不是向量宽度的倍数时,使用屏蔽存储可以安全地使用它,因为未经修改的元素不会被读取/重写或任何东西;它们确实是未经修改的。(不过,如果任何未触及的元素在实际存储上出现故障,CPU可能需要缓慢的微码辅助。)
如果没有AVX512,您仍然需要“一个内在的”(单数)。
这里有pdep,您可以使用它将位图扩展为字节映射。有关使用_pdep_u64(mask, 0x0101010101010101);mask中的每个位解压为一个字节的示例,请参见my AVX2 left-packing answer。这将在uint64_t中为您提供8个字节。在C语言中,如果在它和数组之间使用union,那么它会给你一个0/1元素的数组。但是,当然,如果数组没有在第一个地方溢出,则要求编译器发出移位指令。您可能只想将memcpy放入一个永久数组中。)
但是在更一般的情况下(更大的位图),或者甚至当您想要混合基于位掩码的新值时使用8个元素时,您应该使用多个内部函数来实现uint64_t的逆,并使用它来混合。(请参见下面的“无pmovmskb部分)
通常,如果数组适合64位(例如8元素char数组),则可以使用pdep。或者如果它是一个由4位字节组成的数组,那么您可以做一个16位掩码,而不是8位掩码。
否则就没有单一的指令,也就没有内在的指令对于较大的位图,可以将其处理为8位块,并将8字节块存储到数组中。
如果数组元素的宽度大于8位(并且没有AVX512),则可能仍应使用pdep将位扩展到字节,但随后使用pdep将字节扩展到dword或向量中的任何内容。例如
// only the low 8 bits of the input matter
__m256i bits_to_dwords(unsigned bitmap) {
    uint64_t mask_bytes = _pdep_u64(bitmap, 0x0101010101010101);  // expand bits to bytes
    __m128i byte_vec = _mm_cvtsi64x_si128(mask_bytes);
    return _mm256_cvtepu8_epi32(byte_vec);
}

如果要保留未修改的元素,而不是在位掩码为零的地方将其设置为零,或者保留以前的内容,而不是分配/存储。
这在C/C++(与ASM相比)中是相当不方便的。要将8个字节从[v]pmovzx复制到char数组中,您可以(而且应该)只使用uint64_t(以避免由于指针别名或未对齐而导致的任何未定义行为)。这将用现代编译器编译成一个8字节的存储区。
但是对于或它们,您要么在memcpy的字节上写一个循环,要么将char数组强制转换为uint64_t*。这通常工作得很好,因为uint64_t可以对任何内容进行别名,所以以后读取char数组时不会有任何严格的别名UB。但是,如果编译器假设在自动矢量化时它是对齐的,则即使在x86上,未对齐的uint64_t*也可能导致问题。Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?
指定0/1以外的值
使用乘char*将0/1字节的掩码转换为0/-1掩码,然后使用uint64_t*将值广播到所有字节位置。
如果希望不修改元素,而不是将其设置为零或0xFF,那么即使数组中有字节元素,也应该使用SSE2/SSE4或AVX2。加载旧内容,uint64_tvalue=121,使用字节掩码作为控制向量。
vpblendvb只使用每个字节的高位,因此您的set1(121)常量可以vpblendvb将输入位分散到每个字节的高位,而不是低位。(所以不需要乘以pdep就可以得到AND掩码)。
如果元素是dword或更大,则可以使用0x8080808080808080。(将掩码从字节扩展到DWORD时,使用0xFF而不是zx复制符号位)。这可能是一个性能战胜变量混合+总是读/写。Is it possible to use SIMD instruction for replace?
_mm256_maskstore_epi32
pmovsx在Ryzen上的速度非常慢,甚至在Intel上,这也不是最好的选择。
另一种方法是将位掩码转换为矢量掩码:
is there an inverse instruction to the movemask instruction in intel avx2?
How to perform the inverse of _mm256_movemask_epi8 (VPMOVMSKB)?
也就是说,将位图广播到矢量的每个位置(或将其洗牌,使位图的正确位在相应的字节中),并使用SIMD来屏蔽该字节的适当位。然后对和掩码使用pdep来查找已设置位的元素。
如果不想在位图为零的地方存储零,则可能需要加载/混合/存储。
使用“比较”掩码混合您的pdep,例如使用pcmpeqb/w/d或256bit AVX2版本。您可以处理16位块中的位图,只需使用value就可以生成16字节向量,将其中的字节发送到正确的元素。
即使多个线程的位图不相交,在同一个数组上同时执行此操作也是不安全的,除非使用屏蔽存储。

关于c - 固有地基于BitMask设置数组中的值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48538254/

相关文章:

c - ~x + ~y == ~(x + y) 总是假的?

iphone - 在 objective-c 中忽略方法的返回值是否有效?

c++ - 迭代 OpenCl 内核

linux - 虚拟化环境下的CLFLUSH

c - C 程序的主函数是否会回收堆栈?

c - 如何从字节数组中提取少量位或字节?

c - 如何使用按位运算符将位与 C 中的无符号整数隔离?

c - C中的strtok函数错误

c - 从recvfrom()缓冲区访问值

c - 检查 Visual Studio C++ 编译器生成的代码,第 1 部分