Zig 中,标量(Scalar)和向量(Vector)的区分不仅是数学概念上的差异,更是硬件指令集(SIMD)在语言层面的直接映射 。向量(Vector)是为 SIMD(单指令多数据) 并行计算提供的一等公民。
标量(Scalar)
标量是单一、独立的基本数据值,是 Zig 所有运算的基础原子类型。
标量类型体系
- 布尔型:
bool(值:true/false) - 整型(有符号 / 无符号,位宽明确):
- 有符号:
i8/i16/i32/i64/i128/isize - 无符号:
u8/u16/u32/u64/u128/usize
- 有符号:
- 浮点型:
f16/f32/f64/f128 - 指针:
*T/*const T(地址型标量)
标量核心特性
- 单一值:一个变量仅存一个数据,无内部并行结构。
- 原生运算:支持常规算术、位运算、比较。
- 内存布局:连续、紧凑,无额外开销(如
u8占 1 字节,f32占 4 字节)。
向量(Vector)
向量是固定长度、同类型标量的集合,通过单指令多数据(SIMD) 并行执行元素级运算,直接映射 CPU 硬件加速指令。
- 语法:
@Vector(comptime N: usize, T: type)N:元素个数(编译期常量,建议 2/4/8/16 等 2 的幂,匹配 CPU SIMD 宽度)T:元素类型(仅支持bool/ 整型 / 浮点 / 指针)
定义与初始化
-
直接初始化:用
.{}按元素赋值const v: @Vector(3, i32) = .{10, 20, 30};
-
广播(@splat):标量扩展
const scalar: f32 = 2.0;
const vec = @as(@Vector(4, f32), @splat(scalar)); // 等价 .{2.0,2.0,2.0,2.0}
运算
向量重载了普通算术、位运算和比较运算符, 所有运算符按元素独立执行,自动编译为 SIMD 指令(如 x86 的 SSE/AVX、ARM 的 NEON)。
算术 / 位运算
必须是向量间运算,变量需先要扩展、转换为向量
const a: @Vector(4, f32) = .{ 1.0, 2.0, 3.0, 4.0 };
const b: @Vector(4, f32) = .{ 5.0, 6.0, 7.0, 8.0 };
const c = a + b; // .{6.0,8.0,10.0,12.0}
const d = c * @as(@Vector(4, f32), @splat(2.0)); // .{12.0,16.0,20.0,24.0}
比较运算
比较的结果是一个布尔向量,元素类型为 bool,长度与操作数相同。
zig
const a: @Vector(4, i32) = .{ 1, 5, 3, 8 };
const cond = a2 > @as(@Vector(4, i32), @splat(2)); // {false, true, true, true} (布尔向量)
布尔向量主要用于后面的 @select 选择操作。
元素访问与内建函数
索引单个元素
像数组一样用 [] 访问(只能读,不能直接通过索引修改)。
const v: @Vector(4, f64) = .{ 1.1, 2.2, 3.3, 4.4 };
const x = v[2]; // 3.3 (标量)
若要修改某个元素,通常借助 @shuffle 或转换为数组再操作。
@reduce -- 归约操作
将整个向量归约为一个标量。支持 .Min, .Max, .Add, .Mul, .And, .Or 等。
const sum = @reduce(.Add, a); // 1+2+3+4 = 10
const min = @reduce(.Min, a); // 1
@shuffle -- 重排元素
用编译期掩码从一到两个向量中选取元素,生成新向量。
const v1: @Vector(4, i32) = .{ 1, 2, 3, 4 };
const v2: @Vector(4, i32) = .{ 5, 6, 7, 8 };
// 掩码使用负数表示从 v2 选取 ( -1 代表 v2[0], -2 代表 v2[1] ... )
const shuffled = @shuffle(i32, v1, v2, [_]i32{ 0, -1, 3, -3 });
// 结果: { v1[0], v2[0], v1[3], v2[2] } = {1, 5, 4, 7}
@select -- 按条件选择
根据布尔向量在两个向量之间挑选元素:true 选第一个向量的元素,false 选第二个向量的元素
const mask: @Vector(4, bool) = .{ true, false, false, true };
const a: @Vector(4, i32) = .{ 1, 2, 3, 4 };
const b: @Vector(4, i32) = .{ 10, 20, 30, 40 };
const result = @select(i32, mask, a, b);
// 结果: {1, 20, 30, 4} true 选 a,false 选 b
const c = @select(i32, mask, a, @as(@Vector(4, i32), @splat(0))); // {1, 0, 0, 4}
与数组转换
向量可以显式强制转换为等长的数组,反之亦然:
-
转换不涉及任何代码生成,只是重新解释了内存中的同一份数据
-
利用数组可以方便地对向量元素进行写操作。
const arr: [4]f32 = [_]f32{ 1.0, 2.0, 3.0, 4.0 };
const vec_from_arr: @Vector(4, f32) = arr; // 数组→向量
const arr2: [4]f32 = vec_from_arr; // 向量→数组
与数组异同
| 特性 | 数组 [N]T | 向量 @Vector(N, T) |
|---|---|---|
| 本质定位 | 数据存储结构 (Memory-centric) | 计算原语 (Compute-centric) |
| 硬件映射 | 内存地址空间 / 通用寄存器 | SIMD 向量寄存器 (如 XMM/YMM/ZMM) |
| 运算行为 | 必须通过循环或迭代逐个处理 | 原生支持元素级并行运算 (+, -, *, /) |
| 内存布局 | 紧凑排列,遵循标准对齐 | 高度对齐 (通常由编译器根据硬件自动提升) |
| 灵活性 | 支持任意类型 T (含结构体、指针) |
仅限标量类型 (整数、浮点、布尔、指针) |
| 访问方式 | 索引访问 arr[i] |
索引访问 vec[i] (但主要用于批处理) |
向量内存与性能
- 内存对齐:自动按向量宽度对齐(如 128 位向量对齐 16 字节,256 位对齐 32 字节),满足 SIMD 硬件要求。
- 并行效率:一次指令处理 N 个数据(如 4 路 f32 向量一次算 4 个加法),吞吐量是标量循环的 N 倍(无额外开销)。
- 适用场景:图像处理、音频滤波、物理模拟、密码学、批量数据处理(循环体无依赖)。
SIMD
SIMD = Single Instruction, Multiple Data(单指令、多数据):一条指令,同时对多个数据做相同的操作。
像素处理、矩阵乘法、音频滤波、字符串匹配、CRC 计算等场景里,对数组或缓冲区的每个元素施加几乎相同的操作,且元素间无依赖关系,存在天然的数据并行度。
现代 CPU 提供专门的向量寄存器和向量指令集:
| 平台 | 指令集示例 | 向量寄存器宽度 | 可容纳的常见元素组合 |
|---|---|---|---|
| x86-64 | MMX / SSE / AVX / AVX-512 | 64/128/256/512 bit | 4×f32, 2×f64, 8×i16 等 |
| ARM | NEON | 128 bit | 4×f32, 2×f64, 8×i16 等 |
| RISC-V | V 扩展 (RVV) | 可配置(通常 128~1024 bit) | 灵活长度 |
一条 ADDPS(SSE 加法)就是对两个 128 位寄存器中的 4 个 f32 同时相加。
向量 SIMD 与普通标量计算间差异
Zig 代码与 SIMD 对应示例
| Zig 代码 | x86-64 SIMD 示例指令 |
|---|---|
v1 + v2 |
paddd / addps / paddw |
v1 * v2 |
pmulld / mulps |
v1 & v2 |
pand |
v1 > v2 |
pcmpgtd / cmpltps(生成掩码) |
@reduce(.Add, v) |
水平加:haddps + shufps 等序列 |
@shuffle(i32, v1, v2, mask) |
shufps / pshufd / vperm |
@select(i32, mask, a, b) |
blendvps / bsl (AND/ANDNOT/OR 模拟) |