벡터(Vectors)

벡터(Vectors)

여러 숫자를 한꺼번에, 동시에 다뤄야 할 때가 있어요. 벡터는 boolean, 정수(Integer), 실수(Float), 포인터(Pointers)들을 묶어서 병렬로 연산하는 그룹이에요. 가능하면 SIMD 명령어를 사용해서 처리하죠. 벡터 타입은 내장 함수 @Vector로 만들 수 있어요.

출처: Zig Documentation — Vectors

본문

벡터는 일반적으로 기반(base) 타입이 지원하는 것과 똑같은 내장 연산자를 지원해요. 유일한 예외는 bool 벡터에 대한 andor 키워드예요. 이 연산자들은 **제어 흐름(control flow)**에 영향을 주기 때문에 벡터에서는 허용되지 않죠. 그 밖의 모든 연산은 **요소별(element-wise)**로 수행되고, 입력 벡터와 같은 길이의 벡터를 반환해요. 여기에는 다음이 포함돼요.

  • 산술(Arithmetic): +, -, /, *, @divFloor, @sqrt, @ceil, @log
  • 비트 연산(Bitwise): >>, <<, &, |, ~
  • 비교 연산(Comparison): <, >, ==
  • 불리언 부정(Boolean not): !

스칼라(개별 숫자)와 벡터를 섞어서 수학 연산자를 쓰는 건 금지돼요. Zig는 스칼라에서 벡터로 쉽게 바꿔주는 @splat 내장 함수를 제공하고, 벡터에서 스칼라로 바꾸려면 @reduce와 배열 인덱싱 문법을 지원해요. 벡터는 또한 comptime에 길이가 정해진 고정 길이 배열(fixed-length arrays)과의 할당도 지원합니다.

벡터의 안에서, 그리고 벡터 사이에서 요소를 재배치할 때는 Zig가 @shuffle@select 함수를 제공해요.

대상 머신의 네이티브 SIMD 크기보다 짧은 벡터 연산은 보통 단일 SIMD 명령어로 컴파일되고, 네이티브 SIMD 크기보다 벡터는 여러 SIMD 명령어로 컴파일돼요. 만약 대상 아키텍처에서 특정 연산에 SIMD 지원이 없다면, 컴파일러는 각 벡터 요소를 하나씩 차례로 처리하는 방식으로 기본 설정돼요. Zig는 2^32-1까지의 어떤 comptime-known 벡터 길이든 지원하지만, 작은 2의 거듭제곱(2–64)이 가장 흔하죠. 지나치게 긴 벡터 길이(예: 2^20)는 현재 Zig 버전에서 컴파일러 크래시를 일으킬 수 있으니 주의하세요.

여기 벡터의 기본적인 사용과, 배열·슬라이스와의 변환을 보여주는 예제예요.

const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "Basic vector usage" {
    // Vectors have a compile-time known length and base type.
    const a = @Vector(4, i32){ 1, 2, 3, 4 };
    const b = @Vector(4, i32){ 5, 6, 7, 8 };

    // Math operations take place element-wise.
    const c = a + b;

    // Individual vector elements can be accessed using array indexing syntax.
    try expectEqual(6, c[0]);
    try expectEqual(8, c[1]);
    try expectEqual(10, c[2]);
    try expectEqual(12, c[3]);
}

test "Conversion between vectors, arrays, and slices" {
    // Vectors can be coerced to arrays, and vice versa.
    const arr1: [4]f32 = [_]f32{ 1.1, 3.2, 4.5, 5.6 };
    const vec: @Vector(4, f32) = arr1;
    const arr2: [4]f32 = vec;
    try expectEqual(arr1, arr2);

    // You can also assign from a slice with comptime-known length to a vector using .*
    const vec2: @Vector(2, f32) = arr1[1..3].*;

    const slice: []const f32 = &arr1;
    var offset: u32 = 1; // var to make it runtime-known
    _ = &offset; // suppress 'var is never mutated' error
    // To extract a comptime-known length from a runtime-known offset,
    // first extract a new slice from the starting offset, then an array of
    // comptime-known length
    const vec3: @Vector(2, f32) = slice[offset..][0..2].*;
    try expectEqual(slice[offset], vec2[0]);
    try expectEqual(slice[offset + 1], vec2[1]);
    try expectEqual(vec2, vec3);
}
$ zig test test_vector.zig
1/2 test_vector.test.Basic vector usage...OK
2/2 test_vector.test.Conversion between vectors, arrays, and slices...OK
All 2 tests passed.

배열과의 관계 (Relationship with Arrays)

벡터와 배열은 각각 잘 정의된 비트 레이아웃을 가지고 있어서 서로 @bitCast를 지원해요. 타입 강제 변환(Type Coercion)은 암묵적으로 @bitCast를 수행하죠.

배열은 잘 정의된 바이트 레이아웃을 가지지만 벡터는 그렇지 않기 때문에, 둘 사이의 @ptrCastIllegal Behavior예요.

벡터 구조 분해 (Destructuring Vectors)

벡터도 구조 분해(destructuring)할 수 있어요. 아래 예제는 punpckldq 명령어를 흉내 내서 두 벡터의 요소를 섞어요 — x의 짝수 인덱스와 y의 짝수 인덱스를 번갈아 꺼내 새 벡터를 만들어요.

const print = @import("std").debug.print;

// emulate punpckldq
pub fn unpack(x: @Vector(4, f32), y: @Vector(4, f32)) @Vector(4, f32) {
    const a, const c, _, _ = x;
    const b, const d, _, _ = y;
    return .{ a, b, c, d };
}

pub fn main() void {
    const x: @Vector(4, f32) = .{ 1.0, 2.0, 3.0, 4.0 };
    const y: @Vector(4, f32) = .{ 5.0, 6.0, 7.0, 8.0 };
    print("{}", .{unpack(x, y)});
}
$ zig build-exe destructuring_vectors.zig
$ ./destructuring_vectors
{ 1, 5, 2, 6 }

더 알아보기