← 목록으로

Go의 플랫폼 독립적 SIMD

요약
  • Go 1.27에 추가된 실험적 simd 패키지를 통해 개발자는 어셈블리를 직접 작성하지 않고도 다양한 플랫폼에서 SIMD 가속을 활용할 수 있다.
  • 이 공통 API는 amd64, arm64, WebAssembly 등을 지원하며, 하드웨어에 맞는 최적화된 연산이나 소프트웨어 에뮬레이션을 제공하여 계산 집약적인 작업의 성능을 높여준다.
  • Go 1.27에 추가된 실험적 simd 패키지로 어셈블리를 직접 작성하지 않고도 여러 데이터에 같은 연산을 한꺼번에 적용하는 SIMD 가속을 활용할 수 있음
  • CPU마다 다른 명령어와 벡터 크기를 공통 API로 감싸, 한 번 작성한 코드를 amd64, arm64, WebAssembly에서 사용할 수 있음
  • 하드웨어에 맞는 연산은 어셈블리에 가까운 성능을 목표로 하며, SIMD를 지원하지 않는 환경에서도 소프트웨어 구현으로 실행
  • 컴파일러가 하드웨어별 코드를 생성하고 적절한 실행 경로를 선택해, 계산 도중 지원 기능을 반복해서 확인하는 비용을 줄임
  • 빌드 시 GOEXPERIMENT=simd 로 활성화하며, 공통 API에 없는 연산은 archsimd로 보완 가능. Go 1.28에서는 지원 연산과 하드웨어 범위를 확대할 계획
어셈블리 없이 SIMD를 사용하는 실험적 API
  • SIMD(Single Instruction Multiple Data) 는 데이터 벡터에 동일한 연산을 빠르게 적용하는 CPU 기능으로, 단일 명령으로 float64 값 8쌍을 더하는 작업 등이 가능함
    • 암호화, 데이터 처리, AI처럼 계산량이 많은 작업을 가속할 수 있음
    • Go의 Green Tea 가비지 컬렉터도 살아 있는 객체를 찾는 메모리 스캔에 SIMD를 활용함
  • 기존 Go에서는 Go 어셈블리를 작성해야 SIMD를 사용할 수 있었음
    • 성능이 특히 중요한 계산 커널에서만 투자할 만한 방식이어서, SIMD의 이점을 얻을 수 있는 소프트웨어도 CPU 기능을 충분히 활용하지 못했음
  • archsimd 는 아키텍처 종속 SIMD API로, Go 1.26에서 amd64 지원을 도입하고 Go 1.27에서 arm64 NEON과 wasm 지원을 추가함
  • Go 1.27의 simd 는 C++용 Highway를 느슨하게 참고한 완전 이식형 인터페이스임
    • 특정 벡터 크기에 묶이지 않는 여러 데이터 처리 알고리듬을 지원하는 것이 목표임
    • 소스 연산이 하드웨어와 맞으면 어셈블리 수준의 효율을, 그렇지 않으면 가능한 한 효율적인 에뮬레이션을 지향함
    • 사람이 읽고 이해하기 쉬워야 하며, LLM이 코드를 작성하는 경우에도 이 원칙을 중시함
플랫폼마다 다른 벡터 크기와 명령 체계
  • 벡터 크기는 아키텍처별로 고정 여부와 범위가 다름
    • wasm, PowerPC, s390x는 128비트 고정 크기를 제공함
    • amd64는 128/256/512비트, loong64는 128/256비트를 제공함
    • Riscv64는 128~65,536비트 범위에서 2의 거듭제곱 크기를 지원하며, 크기가 사전에 정해져 있지 않음
    • arm64는 128비트 고정 NEON과 128~2,048비트 범위의 2의 거듭제곱 크기를 지원하는 가변 SVE가 있음
  • 같은 아키텍처에서도 기능 검사가 필요함
    • amd64의 AVX/AVX2/AVX512 지원 여부, arm64의 NEON/SVE 지원 여부를 확인해야 함
    • SVE는 벡터 크기뿐 아니라 SVE/SVE2/SVE2.1 중 어떤 변형인지도 확인해야 함
  • 마스킹은 벡터의 조건부 연산을 구현하지만, 플랫폼마다 표현과 처리 방식이 다름
    • wasm, AVX, AVX2, NEON은 일반 벡터 비트마스크와 불리언 연산을 사용함
    • AVX512와 RVV는 벡터 요소당 1비트를 사용하는 전용 마스크 레지스터가 있음
    • SVE는 벡터 바이트당 1비트를 할당하고, 각 요소에 해당하는 마스크 비트 중 최하위 비트로 동작을 결정함
    • AVX2의 마스크 로드/스토어는 일반 벡터를 마스크로 사용하며 최상위 비트로 동작을 결정함
  • 지원 연산에도 차이가 있음
    • 요소 재배열 연산은 플랫폼별로 다르고, 입력을 상수로 제한하는 경우와 변수 입력을 지원하는 경우가 있음
    • 암호화 관련 연산도 다르며, wasm은 64비트 정수 벡터의 비교 연산이 부족함
    • 같은 아키텍처와 벡터 길이에서도 명령 지원 여부는 추가 기능에 따라 달라짐
  • archsimd는 최대한 균일한 API를 지향하지만, 효율을 유지하면서 차이를 숨기는 데 한계가 있어 여러 플랫폼용 SIMD 코드의 설계, 작성, 테스트가 복잡함
simd의 타입과 기본 사용 방식
  • simd 는 고정 벡터 크기를 타입에서 없애고 플랫폼 간 공통 연산을 중심으로 API를 구성하며, 빈틈은 다른 SIMD 명령을 이용한 효율적인 에뮬레이션으로 메움
    • SIMD 명령이 없거나 archsimd가 지원하지 않는 플랫폼에서는 모든 연산을 에뮬레이션해 코드가 계속 실행되도록 함
    • 실험적 archsimd와 마찬가지로 빌드 시 GOEXPERIMENT=simd 를 설정해야 함
  • 벡터 타입 이름은 기본 타입의 첫 글자를 대문자로 바꾸고 복수형으로 만든 simd.Uint8s, simd.Float32s 등의 형태임
    • 슬라이스에서 벡터를 로드하고 슬라이스에 저장함
    • Len()으로 현재 벡터의 요소 수를 얻음
  • 내적 예제는 LoadFloat32s와 MulAdd 로 벡터 단위 누적 계산을 수행함
    • 끝에 남은 요소는 LoadFloat32sPart로 처리함
    • Go 1.27에는 벡터 전체 요소를 더하는 공통 연산이 없어, 결과를 슬라이스에 저장한 뒤 스칼라 루프로 합산함
    • 다음 릴리스에는 ReduceSum 이 추가되어 이 합산 함수를 simd.ReduceSum으로 대체할 예정임
  • 비교 결과는 요소 너비별 마스크 타입으로 반환됨
    • Int8s 비교는 Mask8s를 생성하는 식이며, 마스크는 벡터 선택과 필터링에 사용할 수 있음
Go 1.27에서 지원하는 연산
  • 데이터 읽기와 저장: 정수와 부동소수점 데이터를 슬라이스에서 벡터로 읽고 다시 저장하며, 같은 값을 모든 요소에 채우거나 일부 요소만 처리할 수 있음
  • 산술 연산: 덧셈, 곱셈, 최솟값과 최댓값, 제곱근 등을 제공하며, MulAdd로 곱셈과 덧셈을 함께 수행할 수 있음
    • 값이 범위를 넘으면 최댓값이나 최솟값으로 제한하는 포화 덧셈과 뺄셈도 지원
  • 비교와 조건부 처리: 요소별 비교 결과를 마스크로 만들고, IfElse 등으로 조건에 따라 값을 선택하거나 걸러낼 수 있음
  • 비트 연산: AND, OR, XOR, 시프트와 회전을 지원하며, 암호화와 CRC 체크섬에 쓰이는 캐리 없는 곱셈도 제공
  • 타입과 형태 변환: 정수와 부동소수점 사이의 값 변환 외에, 같은 비트 데이터를 다른 요소 크기나 타입으로 해석하는 비용 없는 변환도 지원
  • 연산마다 지원하는 타입이 다르며, 전체 함수와 타입별 지원 여부는 공식 연산 표에서 확인 가능
아키텍처별 코드로 부족한 연산 보완하기
  • 공통 API가 부족하거나 필요한 기능의 에뮬레이션이 아직 적절하지 않다면 ToArch() 로 아키텍처별 SIMD 타입으로 전환할 수 있음
    • 반환값은 any이며, 타입 단언으로 플랫폼별 타입을 얻음
    • 다시 공통 타입으로 돌아올 때는 simd.<SimdType>FromArch 함수를 사용함
    • 이식성을 유지하려면 각 플랫폼용 구현과 에뮬레이션을 모두 마련해야 함
  • Go 1.27에 없는 Int8s.OnesCount() 는 전체 알고리듬을 플랫폼별로 다시 쓰지 않고, 해당 연산만 구현하는 예시임
    • Go 1.28에서 추가할 예정인 연산임
    • amd64의 AVX/AVX2 경로는 4비트 단위 조회 테이블과 재배열, 덧셈을 조합하고, AVX512 경로는 OnesCount()를 사용함
    • NEON과 wasm은 해당 연산을 지원하므로 Int8x16으로 변환해 호출한 뒤 되돌리는 방식으로 구현함
    • 이 NEON/wasm 예제는 향후 SVE가 추가되면 그대로 사용할 수 없다는 제한이 있음
  • 공통 에뮬레이션 함수는 SIMD 미지원 플랫폼과 GODEBUG=simd=0 경로의 대체 구현을 맡음
    • 예제는 벡터를 두 개의 uint64로 저장하고 비트마스크, 시프트, 덧셈으로 각 바이트에서 값이 1인 비트 수를 계산한 뒤 벡터로 복원함
  • 인터페이스 변환과 타입 스위치는 비용이 커 보이지만, 컴파일러 특수화가 타입 스위치를 최적화해 제거함
공통 API와 에뮬레이션의 절충
  • 모든 플랫폼이 지원하는 로드, 스토어, 산술, 일부 비교 연산은 공통 API에 넣기 쉽지만, 단순한 교집합만으로는 기능이 부족함
    • 부족한 연산은 아키텍처별 archsimd API에 에뮬레이션을 추가해 보완함
  • archsimd에는 이미 사용 편의를 위한 추상화가 있음
    • 부호 있는 정수와 부호 없는 정수 덧셈은 같은 명령으로 컴파일되더라도 Int8x16.Add와 Uint8x16.Add를 각각 제공함
    • 부동소수점 부호 반전과 절댓값도 필요한 경우 비트 조작으로 구현해, 사용자가 직접 처리하지 않도록 함
  • 일부 에뮬레이션은 2~3개 명령만으로 가능함
    • 요소별 시프트 거리만 지원하는 하드웨어에서는 벡터 시프트로 모든 요소에 같은 거리를 적용하는 시프트를 구현함
    • 일부 부호 없는 비교는 부호 있는 비교와 상수에 대한 XOR 두 번으로 구현함
  • 캐리 없는 곱셈(carryless multiply) 은 암호화와 CRC 체크섬에 중요해, 구현이 복잡하더라도 공통 API에서 제외하지 않고 에뮬레이션을 제공함
    • 암호화 사용을 고려해 실행 시간이 입력값에 따라 달라지지 않도록 함
  • 다음 릴리스에는 쌍별 덧셈인 수평 덧셈보다 그 주된 용도인 합계 축약(sum reduction) 을 제공할 예정임
    • 하드웨어에 쌍별 덧셈이 있어도 필요한 축약 단계 수는 벡터 길이에 따라 달라지므로, 상위 수준 연산이 벡터 길이 의존성을 줄임
  • 향후 약 1년 안에 archsimd 지원이 추가될 것으로 예상하는 Riscv64, ppc64, s390x, loong64까지 고려해야 하므로, 공통 메서드 추가에는 보수적인 접근이 필요함
GODEBUG로 하드웨어 구성별 실행 테스트
  • 하드웨어 SIMD 지원이 있는 플랫폼에서는 실행 전에 GODEBUG 를 설정해 다른 SIMD 구성에서의 동작을 테스트할 수 있음
  • GODEBUG=simd=0 은 하드웨어 지원 여부와 관계없이 에뮬레이션을 사용함
  • GODEBUG=simd=128 은 128비트 벡터와 해당 기능을 사용하며, 필요한 기능이 없으면 즉시 패닉을 일으킴
  • GODEBUG=simd=256, GODEBUG=simd=512 는 가능하면 각각 256비트, 512비트 벡터와 기능을 사용함
  • +128, +256, +512 설정은 일부 기능이 없어도 지정한 벡터 크기를 사용함
    • GODEBUG=simd=+128과 GODEBUG=simd=+256에서는 미지원 명령을 실행하면 패닉이 발생하지만, 해당 명령을 사용하지 않는 코드는 실행될 수 있음
    • Raspberry Pi는 NEON을 지원하지만 캐리 없는 곱셈 명령인 PMULL이 없음
    • Apple Silicon의 amd64 에뮬레이션은 AVX2를 지원하지만 캐리 없는 곱셈 명령인 VPCLMULQDQ가 없음
컴파일러 특수화와 디스패치 위치
  • simd 구현은 공개 패키지, 내부 구현 패키지, 컴파일러 프런트엔드의 AST 재작성으로 구성되어 디버깅이나 스택 추적에서 추가 타입과 메서드가 나타남
  • AST 재작성은 simd 타입을 참조하는 함수, 변수, 타입의 특수화된 복사본을 만듦
    • simd 타입은 simd/internal/bridge의 크기별 타입으로 대체됨
    • 각 브리지 타입은 메서드 집합을 제한한 archsimd 타입으로 정의됨
    • 특수화된 이름에는 @simdNNN 접미사가 붙으며, NNN은 128/256/512 또는 에뮬레이션을 뜻하는 0임
  • 시그니처에는 simd 타입이 없지만 함수 내부에서 사용하는 경우, 프로그램 시작 시 감지한 SIMD 수준에 따라 적절한 특수화 함수를 호출하는 래퍼로 변환됨
    • 특수화된 함수끼리는 추가 디스패치 없이 직접 호출하고, 인라인화될 수도 있음
  • 이 방식은 코드 중복과 SIMD 성능 사이의 절충임
    • SIMD 계산 내부에서 디스패치가 발생하지 않을 만큼만 분기 위치를 상위 호출로 끌어올림
    • 디스패치가 너무 낮은 위치에서 발생하면 상위 함수에서 simd 타입을 의도적으로 참조해 위치를 올릴 수 있음
    • 벤치마크 예제에서는 var _ simd.Uint64s를 추가해 반복문이 특수화된 계산 함수를 직접 호출하도록 함
Go 1.28 이후 계획
  • archsimd 상세 소개를 별도 블로그 글로 공개할 계획임
  • Go 1.28에서는 archsimd의 SVE 지원을 추가할 예정이며, simd에도 SVE 지원을 넣는 것을 목표로 함
  • 추가 SIMD 연산으로 OnesCount, 마스크 연산, 축약 연산, 벡터 셔플 연산 등을 확장하고자 함
  • Go 1.28에는 소수의 기능 변형(feature variants) 도 포함할 예정임
    • Raspberry Pi처럼 벡터 하드웨어는 있지만 일부 연산만 없는 플랫폼이 전체 에뮬레이션으로 내려가지 않도록 하기 위한 기능임
그냥 목록으로
원문 보기 ↗