SSE
数据类型
SSE提供了以下新的数据类型:
Technology | _m64 | _m128 | _m128d | _m128i | _m256 | _m256d | _m256i | _m512 | _m512d | _m512i |
---|---|---|---|---|---|---|---|---|---|---|
Intel® MMX™ Technology Intrinsics | Yes | NA | NA | NA | NA | NA | NA | NA | NA | NA |
Intel® Streaming SIMD Extensions Intrinsics | Yes | Yes | NA | NA | NA | NA | NA | NA | NA | NA |
Intel® Streaming SIMD Extensions 2 Intrinsics | Yes | Yes | Yes | Yes | NA | NA | NA | NA | NA | NA |
Intel® Streaming SIMD Extensions 3 Intrinsics | Yes | Yes | Yes | Yes | NA | NA | NA | NA | NA | NA |
Advanced Encryption Standard Intrinsics + Carry-less Multiplication Intrinsic | Yes | Yes | Yes | Yes | NA | NA | NA | NA | NA | NA |
Half-Float Intrinsics | Yes | Yes | Yes | Yes | NA | NA | NA | NA | NA | NA |
Intel® Advanced Vector Extensions Intrinsics | Yes | Yes | Yes | Yes | Yes | Yes | Yes | NA | NA | NA |
Intel® Advanced Vector Extensions 512 Intrinsics | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
- __m128i: 128位整型向量,可以容纳 16 个 8 位、8 个 16 位、4 个 32 位或 2 个 64 位整数值
- __m128d: 128位浮点型向量
- __m128: 容纳四个32位浮点值
对于
__m128d
和__m128i
,编译器自动把局部和全局变量在栈上对齐16字节。若需要对齐整型
、float
和double
数组,使用__declspec(align)
- 要访问8位数据
__m128i a = _mm_set1_epi8(1); // 在目标 __m128i 向量中将 每个 8-位整型元素(共 16 个字节)都置为相同的常量 val
int b = _mm_extract_epi8(a, n); // 取出第n位的8位整型数值
- 要访问16位数据
_mm_extract_epi16(__m128i a, int imm) // 取出第imm位的16位整型数值
- 要访问32位数据
_mm_extract_epi32(x, imm) // 取出第imm位的32位整型数值
内部函数(SSE Intrinsics)调用注意事项
- 指令集不直接支持某些内部函数,例如 _mm_loadr_ps 和 _mm_cmpgt_ss 。虽然这些内部函数是方便的编程辅助工具,但请注意,它们可能包含多个机器语言指令。
- 作为 __m128 对象加载或存储的浮点数据通常必须 16 字节对齐。
- 由于指令的性质,一些内部函数要求它们的参数是即时的,即常量整数(文字)。
- 作用于两个 NaN (非数字) 参数的算术运算的结果是未定义的。因此,使用 NaN 参数的 FP 运算将与相应汇编指令的预期行为不匹配。
- 某些 intrinsic 并没有对应的单条机器指令?
x86 指令集中并没有同名的一条机器指令可直接执行。它们只是编译器提供的“快捷接口”。
- 它们可能由多条机器指令组成?
例如,_mm_loadr_ps 要把内存中的 4 个 float 装载到 XMM 寄存器,并且按“反向”顺序排列。没有单条指令能同时完成“加载”与“字元素反序”两个操作,于是编译器可能先用一条 MOVAPS(或 MOVUPS)加载,然后再用一条 PSHUFD(或类似的 shuffle)重排字元素。
- 内存对齐?
对齐访问可在一次内存总线事务中完成全部数据加载/存储,降低时钟周期和硬件复杂度。未对齐访问可能被拆成多次访问,增加延迟;在部分架构上甚至会引发异常。
例如__m128 表示一个 128 位宽的 SIMD 向量,可存放 4 个单精度浮点(4×32 bit = 128 bit),在内存中,它占用 16 字节 连续空间
早期 SSE/SSE2 指令(如 MOVAPS/MOVAPD)对内存操作数必须是 16 字节对齐,否则 CPU 会产生一般保护异常(#GP),后续 SSE3/SSE4.1 引入了可处理非对齐数据的指令(如 MOVUPS),但性能仍低于对齐版本
内存对齐
- 定义
若内存地址 A 能被 n(且通常为 2 的幂)整除,则称该地址是 n字节对齐
- 直观意义
CPU 按其总线宽度或缓存线长度访问内存,若数据跨越对齐边界,可能需要多次总线访问或额外硬件处理。
- 如何理解n字节对齐
- “能被 n 整除” 的数学含义
- 整数除法:地址 A 能被 n 整除 ⇔ 存在整数 k,使得 A = k × n
- 换言之,A ÷ n 的余数(A mod n)等于 0。
- 例如,若 n=8,则地址 A=0x1000(4096)能被 8 整除(4096 mod 8=0),说明它是 8-字节对齐;而 A=0x1003(4099)除以 8 余 3,就不是对齐的。
- 为什么 n 通常选 2 的幂
- 硬件总线与缓存线
- CPU 的内存总线宽度、缓存行大小往往是 2 的幂(如 64 B、128 B)。对齐到相同边界可以保证一次传输正好包含完整数据块,无需跨两次传输。
- 例如,缓存行大小 64 B,若你读取一个 64 B 对齐的结构,则一次缓存填充即可,否则可能触发两次缓存行读取。
- 地址位模式
- 在二进制下,2 的幂 n=2ᵏ 对齐要求地址低 k 位全为 0。
- 举例:16 字节对齐 (n=16=2⁴) ⇒ 地址二进制最低 4 位必须是 0000。
- 硬件总线与缓存线
- 对齐的好处
好处 | 解释 |
---|---|
性能 | 一次总线/缓存访问完成整个数据,减少周期和延迟。 |
简化硬件 | 硬件无需处理跨边界访问的复杂拆分与重组。 |
指令要求 | SIMD 指令(如 MOVAPS)在早期 SSE 中强制要求对齐,否则异常。 |
- 举例说明
- 8-字节对齐(n=8):地址 0x1000、0x1008、0x1010… 都是 8-字节对齐;0x1003 不是。
- 16-字节对齐(n=16):地址 0x2000、0x2010、0x2020… 二进制末 4 位 0000。
地址 → … | 0x1000 | 0x1010 | 0x1020 | …
[ 块0 ][ 块1 ][ 块2 ]
内部函数(SSE Intrinsics)
命名和使用语法
_mm[_<width>]_<intrin_op>_<suffix>()
字段表明向量寄存器宽度隐藏时默认为 128 位
<intrin_op>
指示 intrinsic 的基本作;例如,add 表示加法,sub 表示减法。
操作码 | 含义 | 示例 Intrinsic | 引用 |
---|---|---|---|
add | 加法 (addition) | _mm_add_ps, _mm256_add_epi16 | Stack Overflow |
sub | 减法 (subtraction) | _mm_sub_pd, _mm512_sub_epi32 | Intel |
mul | 乘法 (multiplication) | _mm_mul_ps, _mm256_mul_epi32 | Stack Overflow |
div | 除法 (division) | _mm_div_ss, _mm256_div_pd | assets.ctfassets.net |
load | 加载 (load) | _mm_load_ps, _mm256_loadu_si256 | 科学直接 |
store | 存储 (store) | _mm_store_si128, _mm512_store_epi64 | 科学直接 |
set | 设定向量元素 (set) | _mm_set_epi8, _mm256_set1_epi32 | Stack Overflow |
cmp | 比较 (compare) | _mm_cmpgt_epi8, _mm512_cmpeq_pd | Stack Overflow |
shuffle | 重排 (shuffle) | _mm_shuffle_ps, _mm256_shuffle_epi8 | 科学直接 |
blend | 混合 (blend) | _mm_blend_ps, _mm256_blend_pd | Intel |
align | 对齐/拼接 (align/concatenate) | _mm_alignr_epi8, _mm256_alignr_epi8 | Intel |
abs | 绝对值 (absolute value) | _mm_abs_epi16, _mm256_abs_epi32 | Intel |
+
前 1–2 个字母表示数据布局——Packed (p)、Extended Packed (ep) 或 Scalar (s);剩下的字母/数字则表示元素的数据类型(如浮点/整数、位宽、有无符号)
前缀 | 含义 | 语义 |
---|---|---|
p | packed | 在一个向量寄存器里并行存放多个元素(SIMD) |
ep | extended packed | “扩展的 packed”,最初用来区分 SSE2+ 的 128-bit integer intrinsics 与 MMX 的 64-bit intrinsics,本质同 packedStack Overflow |
s | scalar | 仅在向量寄存器的最低位元素上操作,其它元素保持不变 |
后缀 | 含义 | 例子 |
---|---|---|
s | single-precision FP (32-bit float) | _mm_add_ss |
d | double-precision FP (64-bit double) | _mm_add_sd |
ps | packed single-precision FP | _mm_add_ps (4×32-bit float) |
pd | packed double-precision FP | _mm_add_pd (2×64-bit double) |
i8 | signed 8-bit integer | _mm_add_epi8 |
u8 | unsigned 8-bit integer | _mm_add_epu8 |
i16 | signed 16-bit integer | _mm_add_epi16 |
u16 | unsigned 16-bit integer | _mm_add_epu16 |
i32 | signed 32-bit integer | _mm_add_epi32 |
u32 | unsigned 32-bit integer | _mm_add_epu32 |
i64 | signed 64-bit integer | _mm_add_epi64 |
u64 | unsigned 64-bit integer | _mm_add_epu64 |
i128 | signed 128-bit integer (AVX-512) | _mm512_add_epi128 |
si128 | scalar 128-bit integer (AVX-512) | _mm512_cvtsi128_si32 |
- 举例
//并行 4 个 float 相加
__m128 a, b;
__m128 c = _mm_add_ps(a, b); // “add” + “ps”:并行 (packed) single-precision
//仅对第 0 元素做标量 double 相加
__m128d a, b;
__m128d c = _mm_add_sd(a, b); // “add” + “sd”:scalar double-precision
//并行 4 个有符号 32-bit 整数相加
__m128i a, b;
__m128i c = _mm_add_epi32(a, b); // “add” + “epi32”:extended packed signed-32