You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何从__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支持),就得手动合并原有内存数据:
      1. 用_mm_loadu_si128加载目标地址所在的16字节(不管对齐与否);
      2. 构造一个掩码,要保留的原有字节设为0xFF,要替换的设为0;
      3. 用_mm_and_si128把原有数据的有效部分保留下来;
      4. 把__m128i中要写入的字节用掩码提取出来;
      5. 用_mm_or_si128合并两部分,最后写回内存。

复制X个比特的场景

比特级的复制要麻烦些,核心是处理位对齐:

  1. 先明确目标内存中要写入的比特起始位置和长度X;
  2. 加载目标地址所在的16字节(如果比特段跨了两个16字节块,还要加载下一个块);
  3. 构造位掩码,把目标位置的原有比特清掉(对应位设为0,其余为1);
  4. 把__m128i中的目标比特段移位到正确的位置,并用掩码只保留这些比特;
  5. 把移位后的比特段和清除后的原有数据做_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专门加了掩码存储指令,能直接指定哪些元素需要写入,非常适合这种边界情况:

  1. 先算出剩余的int16元素数量remaining(比如remaining = smallArrayTotalLen - currentProcessedIndex,且remaining < 8);
  2. 构造一个16位元素的掩码:前8 - remaining个元素设为0,后remaining个设为0xFFFF(表示这些位置要写入)。可以用_mm_set_epi16手动构造,也可以用_mm_cmpgt_epi16生成更通用的掩码;
  3. 用_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(老硬件),没有原生掩码存储,那就得手动合并原有数据和计算结果:

  1. 加载目标地址的原有16字节数据;
  2. 构造掩码:要保留的原有元素设为0xFFFF,要替换的设为0;
  3. 用_mm_and_si128保留原有数据的有效部分;
  4. 用_mm_andnot_si128提取计算结果中需要写入的部分;
  5. 合并两部分后写回内存。

示例代码如下:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 22:42:59