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 }