Zig 学习笔记:向量 (@Vector) 与 SIMD 编程

Ziglings 109: 向量 (Vectors)

在之前的练习中,我们习惯用 for 循环来处理数组数据。但在高性能计算领域,现代 CPU 提供了一种名为 SIMD (Single Instruction, Multiple Data) 的能力,允许我们在一条指令中同时处理多个数据。

通常在 C 或 C++ 中使用 SIMD 需要调用复杂的 CPU 指令集(Intrinsics),但 Zig 将其抽象为了一个原生类型:@Vector

任务:优化成对差值计算

我们要解决的问题是:给定两个包含 4 个浮点数的列表,找出它们对应位置差值的绝对值中最大的那个。

传统方法 (For Loop)

传统的写法是遍历数组,逐个计算并更新最大值。虽然逻辑清晰,但它是串行执行的。

fn calcMaxPairwiseDiffOld(list1: [4]f32, list2: [4]f32) f32 {
    var max_diff: f32 = 0;
    for (list1, list2) |n1, n2| {
        const abs_diff = @abs(n1 - n2);
        if (abs_diff > max_diff) {
            max_diff = abs_diff;
        }
    }
    return max_diff;
}

Zig 的向量化方法 (SIMD)

在 Zig 中,我们可以使用 @Vector(len, type) 定义向量。向量支持直接的算术运算,这会被编译器自动优化为并行的 SIMD 指令。

这是我在练习中实现的优化版本:

const Vec4 = @Vector(4, f32);

fn calcMaxPairwiseDiffNew(a: Vec4, b: Vec4) f32 {
    // 1. 并行减法与绝对值计算
    // a - b 会同时计算 4 对数字的差
    // @abs 会同时计算这 4 个结果的绝对值
    const abs_diff_vec = @abs(a - b);
    
    // 2. 归约 (Reduce)
    // @reduce 会高效地将向量“坍缩”为一个标量值
    // 这里使用 .Max 操作符找出向量中的最大元素
    const max_diff = @reduce(.Max, abs_diff_vec);
    
    return max_diff;
}

知识点总结

通过这个练习,我学到了 Zig 处理 SIMD 的几个核心概念:

1. 定义向量 (@Vector)

不需要导入任何库,直接使用内置函数:

const v = @Vector(4, i32){1, 2, 3, 4};

2. 逐元素运算 (Element-wise Operations)

标准运算符(+, -, *, /)以及许多标准库函数(如 @abs, @sqrt)可以直接作用于向量。

  • v1 + v2:对应位置相加。
  • v1 * 5:向量每个元素都乘以 5。

3. 数组与向量的互换

Zig 允许固定长度的数组 ([N]T) 和相同长度的向量 (@Vector(N, T)) 之间进行隐式或显式的转换,这让代码编写非常灵活。

4. 归约 (@reduce)

这是 SIMD 编程中非常强大的工具。它接受一个操作符(如 .Add, .Min, .Max, .And, .Or)和一个向量,将其缩减为单个值。

  • @reduce(.Add, v):计算向量所有元素的和。
  • @reduce(.Max, v):找出向量中的最大值。

5. 广播 (@splat)

如果想把一个标量变成充满该值的向量,可以使用 @splat

const v: @Vector(4, u32) = @splat(10); // {10, 10, 10, 10}

总结

Zig 的向量语法让编写高性能代码变得异常简单。原本需要十几行循环代码逻辑,现在只需要两行,而且这背后的机器码效率可能高出数倍。这就是 Zig “没有隐藏控制流” 但拥有 “强大编译期能力” 的体现。


Happy Zigling! 🦎