Go 1.27는 실험적인 플랫폼 독립 SIMD 패키지를 도입합니다
TL;DR
Go 1.27는 고정 크기 벡터 타입을 추상화하여, AVX/AVX2/AVX512 (amd64), NEON (arm64), WebAssembly에서 어셈블리 수준에 가까운 성능으로 실행되며, SIMD 지원이 없는 CPU에서는 빠른 소프트웨어 에뮬레이션으로 대체되는 실험적인 simd 패키지를 제공합니다. 한 번 작성하면 모든 플랫폼에서 동작합니다.
포터블 SIMD 계층이 필요한 이유?
다른 CPU 패밀리들은 SIMD를 매우 다른 방식으로 노출합니다:
- 벡터 폭은 고정될 수 있습니다 (예: wasm, PowerPC, s390x) 또는 가변적일 수 있습니다 (RISC‑V V-확장, ARM SVE).
- 마스크 세미antics는 다릅니다: 일부 아키텍처는 전용 마스크 레지스터를 사용합니다 (AVX‑512, RVV), 다른 것은 벡터 불리언을 사용합니다 (NEON, AVX2), 일부는 마스크를 완전히 지원하지 않습니다.
- 명령어 세트는 패밀리뿐 아니라 기능 플래그에 따라 다릅니다 (AVX vs AVX2 vs AVX‑512, NEON vs SVE 등).
Go의 이전 archsimd 패키지가 이러한 특이점을 직접 노출해야 했기 때문에, 포터블 SIMD 코드를 작성하려면 아키텍처별 조건문과 수작업 어셈블리가 필요했습니다. 새로운 simd 패키지는 고정 크기 벡터를 타입 시스템에서 제거하고, 모든 지원되는 플랫폼에서 수행할 수 있는 연산의 교집합만 구현하며, 부족한 부분은 효율적인 에뮬레이션으로 채웁니다.
실험 API를 사용하는 방법
프로그램을 빌드하거나 실행할 때 환경 변수 GOEXPERIMENT=simd를 설정하면 됩니다. 이전의 archsimd 실험과 마찬가지로, 컴파일러는 프로그램 시작 시 감지된 하드웨어에 따라 simd 타입 호출을 아키텍처별 구현 또는 에뮬레이션 대체로 재작성합니다.
핵심 설계 원칙
- 교집합 우선 API – 모든 대상 플랫폼에서 공통되는 연산만 노출합니다. 부족한 원시 연산은 기존 SIMD 명령어로 매핑되는 에뮬레이션을 통해 제공됩니다.
- 어셈블리 수준에 가까운 성능 – 소스 연산이 하드웨어 기능과 일치할 경우, 생성된 코드는 수작업 어셈블리와 동일한 속도로 실행됩니다.
- 부드러운 대체 – SIMD 지원이 없는 CPU(또는
GODEBUG=simd=0설정 시)에서는 동일한 코드가 순수 Go 구현으로 실행됩니다. - 가독성 – 타입은
simd.Uint8s,simd.Float32s등으로 이름 붙여지고, 메서드는 일반 Go 메서드처럼 읽히므로 인간과 대규모 언어 모델 모두 접근하기 쉽습니다.
예제: 벡터화된 내적
func innerProduct(x, y []float32) float32 {
var acc simd.Float32s
var i int
for i = 0; i < len(x)-acc.Len()+1; i += acc.Len() {
u := simd.LoadFloat32s(x[i : i+acc.Len()])
v := simd.LoadFloat32s(y[i : i+acc.Len()])
acc = u.MulAdd(v, acc) // acc += u*v
}
if i < len(x) {
u, _ := simd.LoadFloat32sPart(x[i:])
v, _ := simd.LoadFloat32sPart(y[i:])
acc = u.MulAdd(v, acc)
}
return simd.ReduceSum(acc) // Go 1.28에서 추가됨
}
루프는 각 반복에서 acc.Len()개의 요소를 처리하며, acc.Len()은 런타임에 감지된 벡터 폭(128, 256, 또는 512비트)입니다. 동일한 소스 코드는 최신 x86에서는 AVX‑512로, Apple Silicon에서는 NEON으로 컴파일되며, SIMD가 사용 불가능할 경우 순수 Go 루프로 컴파일됩니다.
지원되는 연산 (Go 1.27)
아래 표는 현재 사용 가능한 메서드를 요약합니다. V는 벡터 타입, M은 마스크 타입, E는 스칼라 요소 타입을 나타냅니다.
로드 / 브로드캐스트
LoadV([]E) V– 전체 슬라이스를 벡터로 로드합니다.LoadVPart([]E) (V, int)– 부분 슬라이스를 로드하고 실제로 로드된 요소 수를 반환합니다.BroadcastV(E) V– 스칼라를 모든 레인에 브로드캐스트합니다.
저장 / 문자열
Store([]E)– 벡터를 슬라이스로 다시 씁니다.StorePart([]E) int– 맞는 만큼의 레인을 저장합니다.String() string– 인간이 읽을 수 있는 표현입니다.
산술 (선택적)
Add,Sub,Mul,Div(실수만),Neg,Abs(실수만).MulAdd– 실수용 융합 곱셈-덧셈.IfElse(mask, y)– 마스크가 참인 위치에서y의 요소를 선택합니다.Masked(mask)– 마스크가 거짓인 위치의 레인을 0으로 만듭니다.
불리언 및 마스킹
- 벡터에 대한
And,Or,Xor,AndNot. Mask타입 (Mask8s,Mask16s, …)은And,Or,String,ToIntWs를 지원합니다.
비교
Equal,NotEqual,Greater,GreaterEqual,Less,LessEqual– 동일한 요소 폭의 마스크를 생성합니다.
변환 및 재구성
ConvertToFloatW,ConvertToIntW,ConvertToUintW.ToBits,ReshapeToUint*,BitsToFloatW,BitsToIntW– 비용 없이 재解석됩니다.
시프트 및 회전 (정수 벡터만)
- 16-, 32-, 64비트 레인에 대한
ShiftAllLeft/Right,RotateAllLeft/Right.
참고: 첫 번째 릴리스에는 제네릭 축약(
ReduceSum)과 일부 마스크 중심 유틸리티가 누락되어 있으며, 이는 Go 1.28에 추가될 예정입니다.
아키텍처별 코드로 연결하기
필요한 연산이 아직 포터블 API에 없을 경우, 개발자는 ToArch()와 FromArch 도우미를 통해 archsimd 패키지로 돌아갈 수 있습니다. amd64에서 누락된 Int8s.OnesCount 구현 예시:
//go:build goexperiment.simd && amd64
func OnesCount(v simd.Int8s) simd.Int8s {
switch x := v.ToArch().(type) {
case archsimd.Int8x16:
// AVX/AVX2에 대한 룩업 테이블 기법 사용.
lut := archsimd.LoadInt8x16Array(&popcnt4x16)
mask := archsimd.BroadcastInt8x16(0x0f)
lo := x.And(mask)
hi := x.ToBits().ReshapeToUint16s().ShiftAllRight(4).
ReshapeToUint8s().BitsToInt8().And(mask)
return simd.Int8sFromArch(lut.PermuteOrZero(lo).Add(lut.PermuteOrZero(hi)))
default:
return OnesCountEmulated(v)
}
}
컴파일러는 바이너리가 컴파일 타임에 대상 플랫폼에 존재하는 archsimd 타입을 알고 있기 때문에, 타입 스위치를 특수화하여 제거합니다.
GODEBUG을 통한 런타임 제어
개발자는 테스트를 위해 특정 벡터 폭을 강제하거나 전체 에뮬레이션을 사용할 수 있습니다:
GODEBUG=simd=0– 항상 에뮬레이션 경로를 사용합니다.GODEBUG=simd=128,256,512– 해당 폭을 요구하며, 사용 불가능할 경우 패닉합니다.GODEBUG=simd=+128(또는+256,+512) – 일부 선택적 명령어가 누락되어도 해당 폭을 허용합니다. 지원되지 않는 명령어를 사용하면 런타임에 패닉합니다.
구현 노트
컴파일러는 simd 타입을 언급하는 모든 함수에 대해 크기 특화된 복사본을 생성하는 AST 재작성 작업을 수행합니다. 이러한 복사본은 simd/internal/bridge에 있으며, 구체적인 archsimd 타입(예: Int8x16)으로 인스턴스화됩니다. 디스패치는 프로그램 시작 후 한 번만 발생하며, 이후 핫 루프는 추가적인 분기 없이 특화된 버전을 실행합니다. simd 타입에 더미 참조(예: var _ simd.Uint64s)를 추가하면 디스패치 지점을 주변 루프로 올려 특화된 버전이 사용되도록 보장할 수 있습니다.
커뮤니티 반응 (선택된 HN 댓글)
"이 경우 포터블 SIMD는 비포터블 SIMD보다 약 11% 느리지만, 둘 다 비-SIMD보다 약 5배 빠릅니다." – ImJasonH (wasm에서의 벤치마크)
"C++는
std::simd를 받고 있습니다; 최소한의 인트린식으로 벡터 코드를 작성하는 데 동참하고 싶습니다." – beached_whale
"누군가 이걸 원하지 않았지만, 그들은 올바르게 시간을 들여 만들었고, Go를 메모리 안전하고 고수준의 시스템 언어로 계속 발전시키고 있습니다." – u8
"이것은 이미 멀티코어를 실행하는 Go 프로젝트에서 저수준 성능을 위한 많은 문을 열어줍니다. 거의 모든 언어가 표준 라이브러리 SIMD 지원을 갖추지 않았습니다." – qprofyeh
"인터페이스 변환과 타입 스위치는 비효율적으로 보이지만, 컴파일러가 이들을 제거합니다; 스위치는 반복마다가 아니라 프로그램 시작 시 해결됩니다." – vlovich123
다음 단계?
- Go 1.28은
archsimd에 SVE 지원을 추가하고, 결국 포터블simd패키지에도 추가될 예정입니다. OnesCount, 풍부한 마스크 유틸리티, 축약 원시 연산, 벡터 셔플 등의 새로운 연산이 계획되어 있습니다.- 기능 변형 플래그를 통해, 몇 가지 선택적 명령어만 부족한 하드웨어에서도 코드가 실행될 수 있도록 할 예정입니다 (예: PMULL이 없는 Raspberry Pi의 NEON).
archsimd내부 구조에 대한 전용 블로그 게시물이 곧 출시될 예정입니다.
결론
Go의 실험적 SIMD 계층은 과거 아키텍처별 어셈블리의 장벽을 제거하여, 다양한 CPU에서 거의 네이티브 성능을 제공하면서도 모든 플랫폼에서 기능적인 대체를 보장하는 깔끔하고 크기 독립적인 API를 제공합니다. 이 설계는 C++의 Highway와 유사한 철학을 반영하며, SVE와 같은 미래 확장도 예측하고 있어, 고성능이고 크로스플랫폼인 작업에 Go를 더 매력적인 선택지로 만듭니다.
Sources
관련
- 프로젝트
- 프로젝트
- 프로젝트
- 프로젝트