벡터(Vectors)
벡터(Vectors)
여러 숫자를 한꺼번에, 동시에 다뤄야 할 때가 있어요. 벡터는 boolean, 정수(Integer), 실수(Float), 포인터(Pointers)들을 묶어서 병렬로 연산하는 그룹이에요. 가능하면 SIMD 명령어를 사용해서 처리하죠. 벡터 타입은 내장 함수 @Vector로 만들 수 있어요.
본문
벡터는 일반적으로 기반(base) 타입이 지원하는 것과 똑같은 내장 연산자를 지원해요. 유일한 예외는 bool 벡터에 대한 and와 or 키워드예요. 이 연산자들은 **제어 흐름(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를 수행하죠.
배열은 잘 정의된 바이트 레이아웃을 가지지만 벡터는 그렇지 않기 때문에, 둘 사이의 @ptrCast는 Illegal 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 }
더 알아보기
@splat— 스칼라를 벡터로 확장@shuffle— 벡터 안/벡터 사이 요소 재배치@select— 조건에 따라 요소 선택@reduce— 벡터 요소를 스칼라로 축약- Slicing by Length — 슬라이스 슬라이싱
- Destructuring, Destructuring Tuples, Destructuring Arrays — 구조 분해 전반