如何从__m128i复制指定字节/位至常规内存?掩码存储处理方法
关于__m128i数据复制与部分存储的解决方案
嘿,这两个都是SIMD编程里很常见的边界处理问题,我来一步步给你拆解清楚~
1. 如何从__m128i中复制X个字节或比特到常规内存?
复制X个字节的场景
分几种情况处理,优先选最高效的方式:
- 如果X是16的整数倍:直接用
_mm_store_si128(对齐内存)或_mm_storeu_si128(非对齐内存)一次性写入就行,这是最省心也最快的操作。 - 如果X < 16:
- 要是只需要低X字节(比如X=8),可以用
_mm_storel_epi64专门写入低64位(8字节),比通用方法更直接。 - 通用场景(任意位置的X字节):如果你的硬件支持AVX2及以上,强烈推荐用掩码存储指令
_mm_maskstore_epi8——先构造一个字节掩码,要写入的字节对应位设为0xFF,其余设为0,然后指令会自动只写入掩码为1的位置,不用手动处理原有数据。 - 要是只能用SSE(没有AVX2支持),就得手动合并原有内存数据:
- 用
_mm_loadu_si128加载目标地址所在的16字节(不管对齐与否); - 构造一个掩码,要保留的原有字节设为0xFF,要替换的设为0;
- 用
_mm_and_si128把原有数据的有效部分保留下来; - 把__m128i中要写入的字节用掩码提取出来;
- 用
_mm_or_si128合并两部分,最后写回内存。
- 用
- 要是只需要低X字节(比如X=8),可以用
复制X个比特的场景
比特级的复制要麻烦些,核心是处理位对齐:
- 先明确目标内存中要写入的比特起始位置和长度X;
- 加载目标地址所在的16字节(如果比特段跨了两个16字节块,还要加载下一个块);
- 构造位掩码,把目标位置的原有比特清掉(对应位设为0,其余为1);
- 把__m128i中的目标比特段移位到正确的位置,并用掩码只保留这些比特;
- 把移位后的比特段和清除后的原有数据做
_mm_or_si128,最后写回内存。
举个简单例子:如果要把__m128i的低5比特写入内存地址ptr的第3到第7位,先加载ptr所在的16字节,用掩码清除第3-7位,然后把__m128i的低5比特左移3位,再和原有数据OR后写回就行。
2. 处理不足128位的int16加法结果存储
你的场景是小数组走到末尾时,剩下的int16元素不足8个(也就是不够128位),不想把整个128位结果都写回去,这里分两种方案,看你的硬件支持情况:
方案一:AVX2掩码存储(强烈推荐,若硬件支持)
AVX2专门加了掩码存储指令,能直接指定哪些元素需要写入,非常适合这种边界情况:
- 先算出剩余的int16元素数量
remaining(比如remaining = smallArrayTotalLen - currentProcessedIndex,且remaining < 8); - 构造一个16位元素的掩码:前
8 - remaining个元素设为0,后remaining个设为0xFFFF(表示这些位置要写入)。可以用_mm_set_epi16手动构造,也可以用_mm_cmpgt_epi16生成更通用的掩码; - 用
_mm_mask_storeu_epi16(非对齐内存)或_mm_mask_store_si128(对齐内存)写入,只会更新掩码为1的元素。
给你写个示例代码:
// 假设当前处理到largeArray的pInLargeArray位置,剩余remaining个int16元素(remaining <8) int remaining = ...; // 你自己计算得到的剩余元素数 __m128i mask; // 手动构造掩码,比如remaining=3时,低3个int16设为0xFFFF,其余为0 switch(remaining) { case 1: mask = _mm_set_epi16(0,0,0,0,0,0,0,0xFFFF); break; case 2: mask = _mm_set_epi16(0,0,0,0,0,0,0xFFFF,0xFFFF); break; case 3: mask = _mm_set_epi16(0,0,0,0,0,0xFFFF,0xFFFF,0xFFFF); break; case 4: mask = _mm_set_epi16(0,0,0,0,0xFFFF,0xFFFF,0xFFFF,0xFFFF); break; case 5: mask = _mm_set_epi16(0,0,0,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF); break; case 6: mask = _mm_set_epi16(0,0,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF); break; case 7: mask = _mm_set_epi16(0,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF); break; default: mask = _mm_set1_epi16(0xFFFF); break; // 理论上不会走到这里 } __m128i inSmallArray = _mm_loadu_si128((__m128i*)(smallArray + currentIndex)); __m128i inLargeArray = _mm_loadu_si128(pInLargeArray); __m128i result = _mm_add_epi16(inLargeArray, inSmallArray); // 掩码存储,只写入需要的remaining个元素 _mm_mask_storeu_epi16((int16_t*)pInLargeArray, mask, result);
方案二:SSE下的手动合并(无AVX2支持时)
如果只能用SSE(老硬件),没有原生掩码存储,那就得手动合并原有数据和计算结果:
- 加载目标地址的原有16字节数据;
- 构造掩码:要保留的原有元素设为0xFFFF,要替换的设为0;
- 用
_mm_and_si128保留原有数据的有效部分; - 用
_mm_andnot_si128提取计算结果中需要写入的部分; - 合并两部分后写回内存。
示例代码如下:
int remaining = ...; __m128i origData = _mm_loadu_si128(pInLargeArray); __m128i inSmallArray = _mm_loadu_si128((__m128i*)(smallArray + currentIndex)); __m128i inLargeArray = origData; __m128i result = _mm_add_epi16(inLargeArray, inSmallArray); // 构造掩码:remaining个元素要替换,所以这些位置设为0,其余为0xFFFF __m128i keepMask; switch(remaining) { case 1: keepMask = _mm_set_epi16(0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0); break; case 2: keepMask = _mm_set_epi16(0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0,0); break; case 3: keepMask = _mm_set_epi16(0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0,0,0); break; // 以此类推,对应remaining的情况 default: keepMask = _mm_setzero_si128(); break; } __m128i keptOrig = _mm_and_si128(origData, keepMask); __m128i writeResult = _mm_andnot_si128(keepMask, result); __m128i finalData = _mm_or_si128(keptOrig, writeResult); _mm_storeu_si128(pInLargeArray, finalData);
另外提一句:如果你的内存是对齐的(比如用_mm_malloc分配的),可以把_mm_loadu_si128/_mm_storeu_si128换成_mm_load_si128/_mm_store_si128,性能会更好哦。
内容的提问来源于stack exchange,提问作者GlassBeaver
相关产品推荐
相关产品推荐

