第2章:数据类型与寄存器——SSE 的三驾马车
做 SSE 优化,第一个门槛就是搞懂那三个看着眼生的类型。__m128、__m128d、__m128i,还有它们背后的 XMM 寄存器。你想想看,寄存器就那么多,怎么把不同类型的数据塞进去?又怎么保证不出错?我当年第一次写 SSE 代码时,就是因为混用了类型,结果跑出来的数据全是乱码,查了两天才发现——嗯,类型不匹配。
2.1 三个核心类型:专治各种数据
SSE 指令集的本质,就是一次性操作 128 位的数据。但 128 位能装的东西太多样了:4 个 float、2 个 double、16 个 byte、8 个 short、4 个 int、2 个 long long……所以 Intel 给了我们三个 C 语言层面的“包装类型”。
| 类型 | 容纳内容 | 对应寄存器视作 | 常用场景 |
|---|---|---|---|
__m128 | 4 × 单精度浮点 (float) | 128 位,4 个 32 位 float | 图形、音频浮点运算 |
__m128d | 2 × 双精度浮点 (double) | 128 位,2 个 64 位 double | 科学计算、高精度模拟 |
__m128i | 整数(任意子宽度的整数) | 128 位,内部格式由你定 | 整数向量化、编解码、哈希 |
关键区别:__m128 和 __m128d 参与算术运算时,编译器直接帮你排好顺序;__m128i 是“裸”的 128 位,你得自己告诉指令怎么切分(比如 _mm_add_epi32 认为切成了 4 个 int32)。
我的经验:如果数据全是 float,就老老实实用 __m128。别为了省代码把 float 强转成 __m128i 再做算术——不仅慢,还容易踩不对齐的坑。我曾经在优化一个 3D 变换库时试过,结果每条指令都得加 cast,寄存器分配也乱套了,后来全部改回 __m128,性能反而提升了 15%。
2.2 XMM 寄存器:真家伙长什么样
你写的 __m128 a;,编译器会在背后帮你分配一个 XMM 寄存器(xmm0 ~ xmm15,64 位模式下)。每个 XMM 寄存器物理上就是 128 位,不带类型。类型信息只在 C 语言的编译期有用,运行时它就是一串 0 和 1。
所以使用规范第一条:
- 不能乱取地址:
&__m128不是返回 XMM 寄存器的地址,而是内存中的临时变量。真正的寄存器没有内存地址。 - 传递参数要小心:按值传递
__m128会触发寄存器拷贝(movaps/movdqa),性能损失不大,但引用传递可能导致编译器无法内联。 - 避免混合类型赋值:
__m128 a = __m128i b;不合法,必须用_mm_castsi128_ps系列函数做 bitcast。
千万不要这样做:
__m128i vi = _mm_set1_epi32(42);
__m128 vf = (__m128)vi; // 直接强转?编译器可能报错或生成错误的mov
正确写法是 vf = _mm_castsi128_ps(vi);,它在寄存器层面什么都不做,只是告诉编译器“把里面的位当作浮点数”。
2.3 数据类型、对齐与内存操作
SSE 对内存对齐非常敏感。默认情况,__m128 对象按 16 字节对齐,如果你用 malloc(只保证 8 字节对齐)然后强转成 __m128*,那就能品尝到 segfault 的滋味了。我有个血泪教训:优化网络包解析时,用 new 分配了一个结构体,里面嵌了 __m128,结果 VS 没给我自动对齐,debug 版跑得好好的,release 版一跑就崩,查了一天发现是 movaps 要求的对齐没能满足。
所以内存操作的规矩就三条:
- 尽量用栈上的局部变量(编译器自动对齐)。
- 必须用堆时,用
_mm_malloc或aligned_alloc,指定 16 字节(或 32 字节,如果你在走 AVX)。 - 加载/存储用匹配的 intrinsic:
_mm_load_ps要求地址 16 字节对齐,_mm_loadu_ps不要求,但慢一点。
速记:对齐 → load / store;不对齐 → loadu / storeu。如果你确保对齐,就用不带 u 的版本,一条指令完成;否则用带 u 的,可能会多几条 uop。我一般在热路径里尽量做对齐,实在不行才用 loadu。
2.4 一张图理顺关系
2.5 实战中我怎么选
讲个实际案例。我在做视频编码的色彩空间转换时,数据流是这样的:
- 像素值用
uint8_t存储,但运算需要int16或float。 - 如果做整数变换(比如色度子采样),我用
__m128i配合_mm_loadu_si128加载 16 个 byte,然后_mm_unpacklo_epi8/_mm_unpackhi_epi8转成 16 位。 - 如果做浮点色彩校正,我先用
_mm_cvtepu8_epi32把字节扩展到 int32,再转成 float,装进__m128去算。
你看,__m128i 更像一个“中间载体”,__m128 才是计算主力。双精度场景我用得少,但做物理模拟的同学天天跟它打交道——两倍精度换来一半的向量宽度,取舍你要想清楚。
我最常用的几个 intrinsic(新手必背):_mm_set1_ps / _mm_loadu_ps / _mm_add_ps / _mm_mul_ps / _mm_storeu_ps。先练熟这五个,再深入其他。数据类型搞对了,性能调优就成功了一半。