Skip to content

Day 79: SIMD高级技巧:Shuffle和Mask

我们已经学习了如何使用@Vector进行基本的按分量运算(如加法和乘法)。然而,许多高级的SIMD算法需要更复杂的数据操作,例如在向量内重新排列元素,或者根据一个条件(掩码)来选择性地合并两个向量。

Zig通过@shuffle和@select这两个强大的内置函数,提供了直接控制这些底层SIMD操作的能力,让你能够实现高度优化的并行算法。

@shuffle可以从一个或两个输入向量中,根据一个索引掩码,创建一个新的向量。这对于矩阵转置、数据类型转换(如AoSoA)和各种复杂的数据重排任务至关重要。

它的原型是:@shuffle(T: type, a: Vector, b: ?Vector, mask: Vector) Vector

  • T: 向量元素类型。
  • a, b: 一个或两个源向量。
  • mask: 一个整数向量,其每个元素指定了结果向量相应位置的来源。如果索引i小于a的长度,则从a[i]取值;否则从b[i - a.len]取值。
const std = @import("std");
const Vec4f = @Vector(4, f32);
const Vec4u = @Vector(4, u32);
const v: Vec4f = .{ 10, 20, 30, 40 };
// 逆序向量
const reversed = @shuffle(f32, v, undefined, .{ 3, 2, 1, 0 });
// reversed is .{ 40, 30, 20, 10 }
// 复制元素 (广播)
const broadcast = @shuffle(f32, v, undefined, .{ 0, 0, 0, 0 });
// broadcast is .{ 10, 10, 10, 10 }
const v2: Vec4f = .{ 50, 60, 70, 80 };
// 从两个向量中交错取值
const blended = @shuffle(f32, v, v2, .{ 0, 4, 1, 5 });
// blended is .{ 10, 50, 20, 60 }

@select的作用类似于三元运算符?:,但作用于整个向量。它接收一个布尔向量(掩码)和两个数据向量,并根据掩码的每个分量是true还是false,来选择性地从两个数据向量中取值,组合成最终结果。

原型:@select(T: type, mask: Vector, a: Vector, b: Vector) Vector

const Vec4f = @Vector(4, f32);
const Vec4b = @Vector(4, bool);
const a: Vec4f = .{ 1, 2, 3, 4 };
const b: Vec4f = .{ -1, -2, -3, -4 };
const mask: Vec4b = .{ true, false, true, false };
// 如果mask中为true,从a取值;否则从b取值
const result = @select(f32, mask, a, b);
// result is .{ 1, -2, 3, -4 }

这对于实现无分支的条件逻辑非常高效。

我们可以结合比较和@select来实现一个并行计算绝对值的函数,避免了对每个元素进行分支判断。

const std = @import("std");
fn abs_vec(v: @Vector(4, f32)) @Vector(4, f32) {
const zero = @splat(0.0, @Vector(4, f32));
const neg_v = zero - v;
const mask = v < zero; // 如果v的分量是负数,则为true
return @select(f32, mask, neg_v, v);
}
pub fn main() void {
const v: @Vector(4, f32) = .{ 1.0, -2.0, -3.0, 4.0 };
const result = abs_vec(v);
std.debug.print("Abs: {any}\n", .{result}); // { 1, 2, 3, 4 }
}

使用@shuffle来实现一个4x4浮点矩阵的转置。矩阵可以表示为4个@Vector(4, f32)。你需要一系列的@shuffle操作来将行向量的元素重新排列成列向量。

  • @shuffle vs. intrinsics 像Intel的_mm_shuffle_ps这样的内建函数(intrinsics)是特定于平台的。而Zig的@shuffle是一个更高层次的抽象,编译器会负责将其翻译成目标平台(如SSE, AVX, NEON)最高效的shuffle指令。

  • 性能可移植性 虽然@shuffle和@select是可移植的,但它们在不同架构上的性能可能会有差异,因为底层的硬件支持不同。尽管如此,使用这些内置函数仍然是编写可移植、高性能SIMD代码的最佳方式。

  • 自动向量化(Auto-vectorization) 一些编译器会尝试自动将普通的循环转换为SIMD代码,这个过程称为自动向量化。然而,它的效果往往不可预测。Zig的方法是让你明确地控制向量化,从而获得可预测的高性能。

@shuffle和@select是实现高级SIMD算法的两个核心构建块。它们将数据重排和条件选择这两个底层硬件能力,以一种类型安全、可移植的方式暴露给Zig程序员。

掌握了这些工具,你就可以开始实现各种复杂的并行算法,从图像处理的卷积和滤镜,到科学计算中的矩阵运算,充分释放出现代CPU的全部计算潜力。