zig-simd - 解説
実装の詳細
Zigの@Vector型
// @Vector(長さ, 要素型) でSIMDベクトルを定義
const Vec4f = @Vector(4, f32); // 4つのf32
// ベクトルの初期化
const a: Vec4f = .{ 1.0, 2.0, 3.0, 4.0 };
const b: Vec4f = @splat(5.0); // { 5.0, 5.0, 5.0, 5.0 }
// 演算は要素ごとに適用
const c = a + b; // { 6.0, 7.0, 8.0, 9.0 }
const d = a * b; // { 5.0, 10.0, 15.0, 20.0 }
配列とベクトルの変換
// 配列からベクトルへ
const array: [4]f32 = .{ 1, 2, 3, 4 };
const vec: @Vector(4, f32) = array;
// ベクトルから配列へ
const back_to_array: [4]f32 = vec;
// スライスからベクトルへ
const slice: []const f32 = &array;
const vec_from_slice: @Vector(4, f32) = slice[0..4].*;
@reduceによる集約
const vec: @Vector(4, f32) = .{ 1, 2, 3, 4 };
// 合計
const sum = @reduce(.Add, vec); // 10.0
// 最大値
const max = @reduce(.Max, vec); // 4.0
// 最小値
const min = @reduce(.Min, vec); // 1.0
// 乗算
const product = @reduce(.Mul, vec); // 24.0
よくある間違い
1. ベクトル長の不一致
// 間違い: 配列長とベクトル長が不一致
const array: [5]f32 = .{ 1, 2, 3, 4, 5 };
const vec: @Vector(4, f32) = array; // コンパイルエラー!
// 正しい: 長さを合わせる
const vec: @Vector(4, f32) = array[0..4].*;
2. 残り要素の処理忘れ
// 間違い: 8の倍数でない配列を処理すると末尾がスキップされる
pub fn vectorAddSimd(a: []const f32, b: []const f32, result: []f32) void {
var i: usize = 0;
while (i + 8 <= a.len) : (i += 8) {
// SIMD処理...
}
// 残りの要素を処理していない!
}
// 正しい: 残り要素をスカラー処理
while (i < a.len) : (i += 1) {
result[i] = a[i] + b[i];
}
3. 整数と浮動小数点の混在
// 間違い: 型が合わない
const int_vec: @Vector(4, i32) = .{ 1, 2, 3, 4 };
const float_vec: @Vector(4, f32) = .{ 1.0, 2.0, 3.0, 4.0 };
const result = int_vec + float_vec; // コンパイルエラー!
// 正しい: 型を揃える
const result = @as(@Vector(4, f32), @floatFromInt(int_vec)) + float_vec;
パフォーマンス最適化
ベクトル長の選択
// ハードウェアに応じた最適な長さ
// SSE: 4 * f32 = 128bit
// AVX: 8 * f32 = 256bit
// AVX512: 16 * f32 = 512bit
// コンパイラが最適化するため、8が良いバランス
const optimal_vec_len = 8;
ループアンローリング
// inline for でコンパイル時展開
inline for (0..4) |i| {
result.data[i][0] = @reduce(.Add, row * cols[0]);
// ...
}
アライメントの活用
// アライメントされたメモリは高速
const aligned_data = try allocator.alignedAlloc(f32, 32, 1024);
// Zigは自動的にアライメントを考慮するが
// 明示的に指定すると更に最適化される可能性
ベンチマーク例
const std = @import("std");
pub fn benchmark() void {
const size = 1_000_000;
var a: [size]f32 = undefined;
var b: [size]f32 = undefined;
var result: [size]f32 = undefined;
// データ初期化
for (0..size) |i| {
a[i] = @floatFromInt(i);
b[i] = @floatFromInt(size - i);
}
// スカラー版
const scalar_start = std.time.milliTimestamp();
vectorAdd(&a, &b, &result);
const scalar_time = std.time.milliTimestamp() - scalar_start;
// SIMD版
const simd_start = std.time.milliTimestamp();
vectorAddSimd(&a, &b, &result);
const simd_time = std.time.milliTimestamp() - simd_start;
std.debug.print("Scalar: {}ms\n", .{scalar_time});
std.debug.print("SIMD: {}ms\n", .{simd_time});
std.debug.print("Speedup: {d:.2}x\n", .{
@as(f64, @floatFromInt(scalar_time)) /
@as(f64, @floatFromInt(simd_time))
});
}
発展トピック
マスク操作
// 条件付き処理
const a: @Vector(4, f32) = .{ -1, 2, -3, 4 };
const zeros: @Vector(4, f32) = @splat(0.0);
// 負の値を0に置換
const mask = a > zeros; // { false, true, false, true }
const result = @select(f32, mask, a, zeros); // { 0, 2, 0, 4 }
シャッフル
const v: @Vector(4, f32) = .{ 1, 2, 3, 4 };
// 要素の並び替え
const shuffled = @shuffle(f32, v, undefined, [4]i32{ 3, 2, 1, 0 });
// shuffled = { 4, 3, 2, 1 }