TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 23 READS

Go 1.27, 플랫폼을 가리지 않는 SIMD API를 실험적으로 도입하다

Go 1.27, 플랫폼을 가리지 않는 SIMD API를 실험적으로 도입하다
SOURCE IMAGE · HACKER NEWS

현대 CPU 대부분은 하나의 명령으로 여러 데이터를 동시에 처리하는 SIMD(Single Instruction Multiple Data) 기능을 내장하고 있다. 예를 들어 float64 8쌍을 한 번의 명령으로 더하는 식이다. 암호화, 데이터 처리, AI 연산처럼 계산이 몰리는 작업의 속도를 크게 끌어올릴 수 있어, Go 런타임의 Green Tea 가비지 컬렉터조차 살아 있는 객체를 찾기 위해 메모리를 훑을 때 SIMD를 활용한다. 문제는 그동안 Go에서 이 기능을 쓰려면 Go 어셈블리를 직접 작성하는 길밖에 없었다는 점이다. 그 수고는 정말 성능이 절박한 소수의 연산 커널에만 감당할 만했고, 결과적으로 SIMD 이점을 볼 수 있었던 많은 코드가 CPU 성능을 그냥 놀린 채 남아 있었다.

Go 1.26은 amd64용 SIMD API를 처음 넣었고, Go 1.27은 arm64(NEON)와 wasm까지 대상을 넓혔다. 다만 이들은 아키텍처마다 다른 특성을 그대로 노출하는 archsimd 패키지에 속한다. SIMD는 지원 연산뿐 아니라 벡터를 표현하는 방식 자체가 플랫폼마다 제각각이기 때문이다. 어떤 플랫폼은 128~512비트 사이의 고정 크기 벡터를 제공하고, 어떤 플랫폼은 벡터 크기를 빌드 시점에 알 수 없어 프로그램 시작 때 질의해야 한다.

파편화가 만든 설계 난제

실제 편차는 상당하다. wasm·PowerPC·s390x는 128비트 단일 고정 크기를, amd64는 128·256·512비트를, loong64는 128·256비트를 제공한다. riscv64는 2의 거듭제곱으로 제한되지만 128~65536비트 사이 임의 크기를 지원하고, arm64는 고정 크기 NEON(128비트)과 가변 크기 SVE(128~2048비트)를 함께 갖는다. 같은 아키텍처라도 특정 장비가 AVX인지 AVX2인지 AVX512인지, NEON인지 SVE인지, SVE라면 버전과 크기가 무엇인지 실행 시점의 기능 검사로 확인해야 한다.

벡터 마스킹 처리도 갈린다. AVX·AVX2·NEON·wasm은 별도 마스크 없이 벡터 비트마스크와 불리언 연산으로 처리하고, AVX512와 RVV는 요소당 1비트짜리 전용 마스크 레지스터를 둔다. SVE는 바이트당 1비트를 할당하되 각 요소의 최하위 비트가 마스크를 좌우하며, AVX2는 일반 벡터를 마스크로 쓰되 최상위 비트가 동작을 결정한다. 여기에 요소 재배치 방식, 암호 관련 명령, 심지어 wasm에 64비트 정수 벡터 비교가 없는 것처럼 기본 산술 지원까지 다르다. archsimd가 최대한 균일하게 설계됐어도 이런 잔재가 남아, 여러 플랫폼을 아우르는 SIMD 코드를 짜고 테스트하는 일은 여전히 번거롭다.

simd 패키지의 접근법

Go 1.27이 실험적으로 추가한 portable simd 패키지는 C++의 Highway를 느슨하게 참고해 이 차이를 감춘다. 핵심 전략은 두 가지다. 첫째, 고정 크기 벡터를 타입 시스템에서 걷어낸다. 벡터 타입은 simd.Uint8s, simd.Float32s처럼 기본 타입을 대문자·복수형으로 쓴 형태이며, 슬라이스에서 로드하고 슬라이스로 저장한다. 둘째, 모든 플랫폼의 교집합에 해당하는 연산만 노출하고 빈 곳은 다른 SIMD 명령을 조합한 효율적 에뮬레이션으로 메운다. SIMD 명령이 아예 없는 플랫폼에서도 전부 에뮬레이션되므로, simd로 짠 코드는 어디서든 실행된다. 사용하려면 archsimd와 마찬가지로 빌드 시 GOEXPERIMENT=simd를 지정하면 된다.

비교 연산은 요소 폭에 맞는 마스크 값을 만든다. Int8s 비교는 Mask8s를 낳는 식이고, 이 마스크로 벡터를 선택·필터링한다. 다만 첫 실험판인 만큼 한계도 분명하다. 벡터 전체 요소를 합산하는 공통 방법이 없어 sum이 아직 빠져 있는데, 다음 릴리스에서 simd.ReduceSum으로 채워질 예정이다. 캐리 없는 곱셈처럼 암호화·CRC에 중요한 명령은 지원이 들쭉날쭉해 에뮬레이션으로 제공하되, 암호 용도를 고려해 실행 시간이 입력값에 따라 달라지지 않도록 설계했다.

탈출구와 내부 동작

simd가 특정 작업에 부족하거나 아직 적절한 에뮬레이션이 없을 때를 위해, 각 벡터 타입은 ToArch() 메서드로 any를 반환하고 이를 아키텍처별 타입으로 타입 단언할 수 있다. 되돌릴 때는 simd.FromArch 함수를 쓴다. 예컨대 1.27에 없는 Int8s.OnesCount()는 AVX512에만 명령이 있어 amd64에서 분기 처리하고, 이를 지원하는 NEON·wasm은 더 단순하게, 하드웨어 SIMD가 없는 환경은 공유 폴백 함수로 구현하는 식이다. 인터페이스 변환과 타입 스위치가 비효율적으로 보이지만, 컴파일러가 코드를 특수화하며 타입 스위치를 제거한다. 다만 이식 가능한 코드를 유지하려면 각 플랫폼과 에뮬레이션까지 직접 작성해야 하는 의무가 따른다.

내부적으로 simd는 패키지이자 내부 구현 패키지이며, 컴파일러 프런트엔드의 AST 재작성이기도 하다. simd 타입을 언급하는 함수·변수·타입은 크기별로 특수화된 복사본으로 바뀌고 @simd128, @simd256, @simd512, 또는 에뮬레이션을 뜻하는 @simd0 접미사가 붙는다. 프로그램 시작 시 감지한 SIMD 수준에 따라 적절한 특수화 버전을 부르는 래퍼로 전환되며, 특수화 함수끼리는 분기 부담 없이 직접 호출한다. 그래서 스택 트레이스에 낯선 타입과 메서드가 보일 수 있다. Go 1.28에서는 archsimd에 SVE를 더하고 simd에도 OnesCount·마스크·리덕션·셔플 같은 연산을 확장할 계획이며, 라즈베리 파이처럼 하드웨어 벡터는 있으나 일부 명령만 없는 장비가 전면 에뮬레이션으로 떨어지지 않도록 '기능 변형'도 소규모로 도입한다. 실무자 입장에서는 어셈블리 없이 이식성과 준(準)어셈블리 성능을 노릴 길이 열린 셈이지만, 아직 실험 단계이자 교집합 중심의 보수적 설계라는 점을 감안해 성능이 절박한 커널에 시험 적용하며 지켜보는 편이 현실적이다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://go.dev/blog/simd-experiment
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기 →
처리 중...