본문 바로가기
개발2026년 9월 25일11분 읽기

Go 팀, 플랫폼 독립 SIMD 패키지 공개 설명 — 어셈블리 없이 AVX·NEON·Wasm을 한 코드로

YS
김영삼
조회 5
Go 팀, 플랫폼 독립 SIMD 패키지 공개 설명 — 어셈블리 없이 AVX·NEON·Wasm을 한 코드로

Go 팀의 데이비드 체이스(David Chase)와 준양 샤오(Junyang Shao)가 9월 24일(현지시간) Go 공식 블로그에 "Platform-independent SIMD in Go"를 올리고, Go 1.27에 실험 기능으로 들어간 이식형 simd 패키지의 설계와 사용법을 공개했다.

지금까지 Go에서 SIMD를 쓰려면 Go 어셈블리를 직접 짜야 했다. 새 simd 패키지는 벡터 크기를 타입 시스템에서 지우고, 모든 플랫폼의 교집합 연산만 제공하되 빈 곳은 에뮬레이션으로 채운다. "한 번 짜면 어셈블리에 가까운 성능"을 목표로 한다.

3줄 요약
  • Go 1.26은 amd64용, Go 1.27은 arm64(NEON)·wasm용 아키텍처별 API(simd/archsimd)를 넣었고, 1.27에서 그 위에 이식형 simd 패키지를 실험 기능으로 추가했다.
  • simd.Float32s처럼 크기가 정해지지 않은 벡터 타입을 쓰며, AVX·AVX2·AVX-512·NEON·Wasm SIMD를 지원하고 하드웨어 지원이 없으면 에뮬레이션으로 동작한다.
  • GOEXPERIMENT=simd로 켜고 GODEBUG=simd로 벡터 길이를 강제해 시험할 수 있다. 1.28에서 SVE 지원과 ReduceSum 같은 연산 추가가 예고됐다.

WHAT HAPPENED무슨 일이 있었나

SIMD(단일 명령 다중 데이터)는 한 명령으로 여러 값을 동시에 계산하는 CPU 기능이다. 예를 들어 float64 쌍 8개를 명령 하나로 더할 수 있다. 암호화, 데이터 처리, AI 연산에서 큰 속도 차이를 만들고, Go의 그린 티(Green Tea) 가비지 컬렉터도 살아 있는 객체를 찾는 메모리 스캔에 SIMD를 쓴다고 블로그는 설명했다.

문제는 접근 방법이었다. 이 실험 API 이전에는 Go 어셈블리를 직접 작성하는 것이 유일한 길이었고, 그래서 정말 성능이 결정적인 커널에만 쓰였다. 블로그는 그 결과 "SIMD 덕을 볼 수 있는 많은 소프트웨어가 CPU의 상당 부분을 놀려 왔다"고 진단했다. Go 1.27 릴리스 노트도 새 실험 패키지로 simd를 명시하고, 모든 아키텍처에서 사용할 수 있으며 하드웨어 명령이 있으면 활용한다고 적었다.

DESIGN두 층의 구조: archsimd와 simd

simd/archsimd아키텍처별
  • Go 1.26에서 amd64로 시작, 1.27에서 NEON·Wasm 추가
  • 128·256·512비트처럼 크기가 고정된 타입
  • 플랫폼 고유 연산까지 최대한 노출
  • 코드가 이식되지 않는 것이 의도된 설계
simd이식형
  • Go 1.27에서 실험 기능으로 추가
  • simd.Uint8s·simd.Float32s 등 크기 미정 타입
  • 모든 플랫폼의 교집합 연산 + 효율적 에뮬레이션
  • C++ 라이브러리 Highway를 느슨하게 참고

왜 두 층이 필요한지에 대한 설명이 이 글의 핵심이다. SIMD 아키텍처는 차원이 여러 개다. wasm·PowerPC·s390x는 128비트 하나, amd64는 128·256·512비트, loong64는 128·256비트를 쓴다. riscv64는 128비트에서 65536비트 사이의 2의 거듭제곱 길이를, arm64는 고정 128비트(NEON)와 128~2048비트 가변(SVE)을 지원한다. 같은 amd64라도 AVX인지 AVX2인지 AVX-512인지 실행 시점에 확인해야 한다.

마스크 처리 방식도 제각각이다. wasm·AVX·AVX2·NEON은 별도 마스크 레지스터 없이 비트마스크 벡터로 처리하고, AVX-512와 RVV는 원소당 1비트의 마스크 레지스터를, SVE는 바이트당 1비트를 쓴다. 지원 연산도 다르다. 예컨대 wasm에는 64비트 정수 벡터 비교가 없다. 아키텍처별 패키지를 아무리 균일하게 만들어도 이런 차이가 남기 때문에, 효율을 해치지 않는 선에서 차이를 숨기는 별도 층이 필요했다는 설명이다.

Go 블로그 예제: 벡터 길이를 몰라도 되는 내적 계산
// innerProduct returns the inner product of x and y.
func innerProduct(x, y []float32) float32 {
    var a simd.Float32s
    var i int
    for i = 0; i < len(x)-a.Len()+1; i += a.Len() {
        u := simd.LoadFloat32s(x[i : i+a.Len()])
        v := simd.LoadFloat32s(y[i : i+a.Len()])
        a = u.MulAdd(v, a)
    }
    if i < len(x) {
        u, _ := simd.LoadFloat32sPart(x[i:])
        v, _ := simd.LoadFloat32sPart(y[i:])
        a = u.MulAdd(v, a)
    }
    return sum(a) // 1.27에는 ReduceSum이 없어 직접 구현
}

코드에 벡터 길이가 등장하지 않는다는 점이 눈에 띈다. a.Len()이 실행 중인 하드웨어에 맞는 길이를 알려 주고, 남은 꼬리는 LoadFloat32sPart로 처리한다. 블로그는 첫 실험 릴리스의 한계도 숨기지 않았다. 벡터 원소 전체를 더하는 공통 방법이 없어 1.27의 simd는 합계 연산을 지원하지 않으며, 다음 릴리스에서 simd.ReduceSum이 나온다고 했다.

UNDER THE HOOD컴파일러가 하는 일과 시험 방법

simd는 패키지이자 내부 구현 패키지이며, 동시에 컴파일러 프런트엔드의 AST 재작성이다. simd 타입을 언급하는 함수·변수·타입은 벡터 길이별로 특화된 복사본이 만들어지고, 이름에 128·256·512 또는 에뮬레이션을 뜻하는 0이 붙은 @simdNNN 접미사가 생긴다. 시그니처에 simd 타입이 없는 함수는 프로그램 시작 때 감지한 SIMD 수준에 따라 알맞은 특화 버전을 부르는 래퍼가 된다. 디스패치 비용을 계산 루프 밖으로 끌어올리기 위한 절충이라는 설명이다.

부족한 연산은 ToArch()로 아키텍처별 타입으로 내려갔다가 FromArch 함수로 돌아오는 방식으로 메울 수 있다. 블로그는 1.27에 없는 Int8s.OnesCount()를 amd64·NEON·Wasm과 에뮬레이션 버전으로 각각 구현하는 예제를 보여 주면서, 인터페이스 변환과 타입 스위치는 컴파일러가 특화 과정에서 최적화로 없앤다고 설명했다.

Go 1.27 기준, Go 블로그 설명 정리
GODEBUG 값동작
simd=0하드웨어 지원이 있어도 에뮬레이션 사용
simd=128128비트 벡터와 기능 사용, 기능이 없으면 즉시 패닉
simd=256 / simd=512가능하면 256·512비트 벡터 사용
simd=+128 등일부 기능이 없어도 해당 길이 사용, 미지원 명령을 실제로 쓰면 패닉. 예: NEON은 있으나 PMULL이 없는 라즈베리 파이

IMPACT개발자에게 미치는 영향

당장 프로덕션 코드를 갈아엎을 일은 아니다. 실험 기능이라 GOEXPERIMENT 플래그가 필요하고, API는 안정판이 아니다. 하지만 바이트 처리, 해시, 압축, 이미지 필터, 벡터 검색처럼 반복 루프가 성능을 좌우하는 코드를 Go로 짜는 사람에게는 어셈블리를 배우지 않고도 SIMD를 시험할 수 있는 길이 처음 열렸다. 블로그는 "LLM이 코드를 쓰게 되더라도 읽고 이해하기 쉬운 것"을 설계 목표 중 하나로 꼽기도 했다.

시험해 볼 것
✓Go 1.27에서 GOEXPERIMENT=simd go build로 빌드해 기존 핫 루프 하나를 simd로 옮겨 벤치마크한다.
✓GODEBUG=simd=0으로 에뮬레이션 경로도 함께 테스트해, 하드웨어 차이로 결과가 달라지지 않는지 확인한다.
✓CI에서 simd=128·256·512를 번갈아 돌려 AVX-512가 없는 기기에서도 동작하는지 본다.
✓어셈블리 커널을 유지 중이라면 1.28의 SVE 지원과 추가 연산(OnesCount·마스크·리덕션·셔플)이 나올 때까지 병행을 검토한다.
영삼
영삼의 한마디
나는 Go를 주력으로 쓰진 않지만, 이 설계는 다른 언어보다 한발 실용적이라고 본다. 러스트의 SIMD 논의가 몇 년째 안정화와 이식성 사이에서 줄다리기를 하는 동안, Go는 "교집합만 주고 나머지는 에뮬레이션"이라는 보수적인 선을 먼저 그었다. 최고 성능은 포기하더라도 아무 기기에서나 돌아가는 코드를 원하는 Go의 성격과 맞는다. 다만 ReduceSum조차 없는 첫 버전은 아직 장난감에 가깝다. 실제로 쓸 만해지는 시점은 1.28이 될 것이다.

Go 팀은 1.28에서 archsimd에 SVE 지원을 추가하고 simd에도 넣기를 바란다고 밝혔다. 또 하드웨어 벡터는 있지만 연산 한두 개가 빠진 라즈베리 파이 같은 플랫폼이 통째로 에뮬레이션으로 떨어지지 않도록 소수의 "기능 변형"을 넣을 계획이다. archsimd를 자세히 다루는 별도 블로그 글도 곧 낼 예정이라고 했다.

자주 묻는 질문

Go에서 SIMD를 쓰려면 어떻게 해야 하나?

Go 1.27 이상에서 GOEXPERIMENT=simd 환경 변수를 켜고 빌드하면 이식형 simd 패키지와 아키텍처별 simd/archsimd 패키지를 쓸 수 있다. 실험 기능이라 API가 바뀔 수 있으며, 이전에는 Go 어셈블리가 유일한 방법이었다.

Go simd 패키지는 어떤 CPU를 지원하나?

Go 1.27 기준 amd64의 AVX·AVX2·AVX-512, arm64의 NEON, WebAssembly SIMD를 지원한다. 하드웨어 지원이 없는 플랫폼에서는 모든 연산이 에뮬레이션으로 동작하므로 코드는 어디서나 실행된다.

simd와 simd/archsimd는 무엇이 다른가?

archsimd는 128·256·512비트처럼 크기가 고정된 아키텍처별 타입과 플랫폼 고유 연산을 제공해 이식성이 없다. simd는 크기가 정해지지 않은 벡터 타입과 모든 플랫폼 공통 연산만 제공하고, 부족한 연산은 ToArch로 archsimd에 내려가 보완한다.

Go 1.28에서 simd는 어떻게 달라지나?

Go 팀은 1.28에서 arm64 SVE 지원과 ReduceSum·OnesCount·마스크·셔플 같은 연산 추가를 예고했다. 일부 연산만 빠진 하드웨어가 전부 에뮬레이션으로 떨어지지 않도록 기능 변형도 도입할 계획이다.

참고 자료

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록