Reddit

The state of SIMD in Rust in 2026

2026년 Rust SIMD 현황

Rust의 SIMD 생태계를 자동 벡터화, portable SIMD 라이브러리, 플랫폼별 인트린식 관점에서 비교합니다. 라이브러리별 기능과 런타임 디스패치의 차이, 컴파일러 최적화 문제를 짚고 x86·ARM 등 하드웨어의 특성도 살펴봅니다.

AI 요약

작성자는 지난해 SIMD 라이브러리를 조사한 뒤 Fearless SIMD 개발에 참여해 현재는 메인테이너로 일하고 있습니다. 이번 글에서는 std::simd, wide, pulp, macerator를 포함한 주요 라이브러리를 비교하고, 여러 SIMD 명령어 세대가 공존하는 x86의 하드웨어 특성과 컴파일러 문제까지 다룹니다. 다른 라이브러리 작성자들에게 초안을 검토받았지만 편집 권한은 본인이 유지했다고 밝힙니다.

SIMD와 명령어 세대

SIMD는 한 명령어로 여러 데이터를 처리해 CPU 연산 장치를 더 효율적으로 쓰는 방식입니다. 최근 x86의 벡터 폭은 최대 512비트이며, 이론상 f64 연산은 8배, u8 연산은 64배까지 처리량을 높일 수 있습니다. 실제 성능은 메모리 병목과 명령어 구성에 따라 달라집니다.

x86_64는 특정 SIMD 확장을 모든 CPU가 지원하지 않으므로 기본 설정에서 컴파일러는 SSE2보다 새로운 명령어를 쓰지 않습니다. 자체 서버처럼 실행 환경을 통제한다면 -C target-cpu=x86-64-v3로 AVX2를 전제할 수 있습니다. 배포 바이너리라면 같은 함수를 여러 명령어 세대로 컴파일하고 실행 시 CPU 기능을 확인해 고르는 함수 멀티버저닝(function multiversioning)이 필요합니다. 64비트 ARM은 NEON을 기본 지원하며, WebAssembly는 SIMD 사용 여부에 따라 바이너리를 나눠야 합니다.

세 가지 SIMD 작성 방식

일반 Rust 코드를 컴파일러가 벡터화하게 두는 자동 벡터화는 의존성이 없고 여러 명령어 세대를 자동으로 지원합니다. 다만 코드 형태나 컴파일러 버전에 따라 벡터화 여부와 성능이 달라지므로, as_chunks() 같은 작성 방식과 벤치마크, 어셈블리 확인이 필요합니다. 부동소수점 연산은 결과 정밀도를 바꿀 수 있어 자동 벡터화에 제약이 있었지만, 글은 Rust 1.98에서 안정화된 algebraic_add() 같은 연산을 사용하면 컴파일러가 재배치할 여지가 생긴다고 설명합니다.

multiversion 크레이트는 함수에 #[multiversion(targets = "simd")]를 붙여 여러 CPU용 코드를 만들지만, 아주 작은 함수에서는 호출 비용이 성능에 드러날 수 있습니다. 글은 루프가 있는 함수에는 멀티버저닝을 적용하고, 짧은 함수에는 #[inline(always)]를 쓰는 방식을 권합니다.

Portable SIMD는 벡터 연산을 명시적으로 작성하는 방식입니다. 비교표에서 Fearless SIMD는 멀티버저닝, 고정·하드웨어 폭 벡터, 제네릭, 인트린식 안전 접근을 한데 제공하는 크레이트로 소개됩니다. 대신 함수에 #[simd]와 SIMD 타입 매개변수를 적는 보일러플레이트가 있습니다. v1.0을 출시했으며, 글에서 꼽은 큰 공백은 삼각함수 지원입니다.

std::simd는 LLVM이 지원하는 다양한 플랫폼을 대상으로 삼을 수 있지만 nightly 전용이고 API 변경도 이어집니다. 일부 연산은 대응하는 LLVM 연산이 없으면 벡터화되지 않으며, 특히 부동소수점 삼각함수와 합산의 성능·정확도를 주의해야 한다고 지적합니다. wide는 안정 버전과 플랫폼 지원, 구현된 연산이 장점이지만 멀티버저닝과 제네릭 지원이 약합니다. pulp는 선형대수 라이브러리 faer에 맞춰 하드웨어 폭 벡터와 수학 연산에 초점을 둡니다. macerator는 burn의 CPU 백엔드에서 쓰이며 원소 타입 제네릭을 지원하지만 문서와 고정 폭 벡터 지원이 제한적입니다.

인트린식과 컴파일러

Rust 1.87부터 #[target_feature] 함수 안에서는 플랫폼별 인트린식을 안전하게 호출할 수 있습니다. 다만 메모리 로드·저장 인트린식은 여전히 raw pointer를 다뤄 unsafe가 필요하고, 필요한 CPU 기능을 확인한 뒤 해당 함수에 진입하는 절차도 필요합니다. 글은 archmage와 Fearless SIMD의 매크로가 기능 토큰과 안전한 접근을 제공한다고 설명합니다. pulp는 특정 SIMD 수준에 묶이지 않은 CPU 기능을 안전하게 쓰는 데 강점이 있지만, 기능 확인 뒤 개별 인트린식을 별도 함수로 부르면 호출 비용이 커질 수 있어 전체 핫 루프를 문맥 안에 넣어야 합니다.

인트린식도 성능을 보장하지는 않습니다. 컴파일러가 연산을 블랙박스로 취급하면 불필요한 메모리 왕복이 생길 수 있고, 반대로 일반 연산 조합으로 구현하면 최적화 과정에서 원하던 명령어로 다시 합쳐지지 않을 수 있습니다. 작성자는 LLVM이 512비트 셔플을 SSE4.2 방식으로 바꿔 느리게 만든 사례를 들며, 관련 버그를 보고해 LLVM 23에서 수정됐다고 설명합니다. 인라인 어셈블리는 컴파일러 최적화를 막는 블랙박스가 되므로, 한두 개 명령어만 넣는 방식은 이득이 없을 수 있습니다.

하드웨어별 고려 사항

작성자는 x86의 복잡성을 AVX2, SSE4.2, AVX-512가 오랜 기간 함께 남은 결과로 설명합니다. Firefox 하드웨어 조사에서는 2026년에도 x86 CPU의 15%가 AVX2를 지원하지 않았다고 합니다. AVX-512는 초기 Intel CPU에서 사용 시 전체 코어의 주파수를 낮추는 문제가 있었고, Ice Lake 이후에는 실용성이 나아졌습니다. 그래서 Fearless SIMD는 Intel의 Ice Lake 이상과 다운클럭 문제가 없었던 AMD에서 AVX-512를 선택한다고 설명합니다. Steam 조사에서는 AVX-512 지원 시스템이 23.9%로 집계됐습니다. LLVM의 scatter/gather 자동 생성은 Intel에서 1.75배, AMD에서 4배 느려지는 사례도 제시합니다.

반면 64비트 ARM의 NEON은 기본 탑재되고, 여러 연산을 단순한 128비트 모델에서 지원합니다. 작성자는 SVE2가 고급 서버에서도 128비트 폭으로 구현된 사례를 들어 현재는 NEON보다 나은 선택이라고 보기 어렵다고 평가합니다. RISC-V 벡터 명령어는 2026년 기준 성능과 가격 면에서 실용적인 하드웨어가 부족하고, 일부 명령어는 사양 구조 때문에 컴파일러가 활용하기 어렵다고 지적합니다.

Rust 도구체인에서 바라는 점

글은 배열과 하드웨어 폭 SIMD를 함께 다루기 쉽게 만들 min_generic_const_args, 함수마다 #[simd]를 붙이지 않아도 되는 Struct Target Features RFC, 멀티버저닝과 이터레이터의 충돌 해결을 요청합니다. std::simd 안정화와 인트린식 변경을 크레이트 생태계에서 검증하는 절차도 제안합니다.

Reddit 반응

  • @u/ollpu — algebraic_*는 Rust 1.88이 아니라 Rust 1.98에서 안정화됐습니다. 시간의 구조를 잠시 의심하기 시작했습니다.
    • @u/Shnatsel — 앗, 수정했습니다. 알려줘서 감사합니다.
  • @u/exDM69 — Simd<T, N> API는 N을 대상으로 연산할 수만 있다면 우아하게 잘 작동할 것처럼 보이지만, 그 기능은 nightly에서도 매우 미완성입니다. 저는 타입 제네릭 코드와 벡터 폭 제네릭 코드를 둘 다 작성해 봤고, 몇 가지 불편함은 있어도 대체로 잘 작동했습니다. 또 이 글은 하드웨어 폭 벡터에 많이 초점을 둡니다. 제 경험에서는 하드웨어가 지원하는 폭보다 넓은 벡터가 더 빠를 때가 있습니다. AVX2에서 f32x16 같은 512비트 벡터를 쓰면 10~20% 더 빠른 벤치마크가 여러 개 있었고, 제네릭 Simd<T, N>에서는 폭을 바꾸기도 쉽습니다.
    • @u/Shnatsel — 레지스터 공간이 부족해지기 전까지는 잘 작동합니다. 그 순간 성능이 급격히 떨어지므로 주의해서 조정해야 합니다. NEON에서는 대개 좋은 방법이고 AVX-512에서는 메모리 병목이 아니라면 도움이 될 수 있습니다. AVX2와 SSE에서는 오히려 성능을 떨어뜨리는 경우가 많습니다. 두 배 폭을 요청하고 싶어도 먼저 하드웨어 기본 폭을 알아야 하므로 같은 문제가 남습니다.
    • @u/exDM69 — 저는 보통 벤치마크로 대상 하드웨어에서 가장 빠른 단일 폭을 고르고 싶습니다. 깔끔한 방법은 없지만 #[cfg(target_feature = "avx512")] const SIMD_WIDTH: usize = 512; 같은 코드가 출발점입니다. 이 값을 #[multiversion] 함수 안에서 쓸 수 있나요?
    • @u/Shnatsel — 안 됩니다. #[cfg]는 멀티버저닝을 적용하기 전에 처리됩니다. std::simd는 특정 기계를 정해 빌드하는 HPC 환경에서는 훌륭하지만, 여러 하드웨어에서 잘 돌아가는 단일 바이너리를 배포하려면 타입 시스템 한계가 드러납니다.
    • @u/exDM69 — multiversion의 #[target_cfg]가 바로 이 용도 아닌가요? 런타임에 감지한 명령어 세대에 따라 벡터 폭을 달리하는 코드를 작성해 봤습니다.
    • @u/Shnatsel — 네, 그 방식이 문제를 해결하는 것 같습니다. 저도 비슷한 결과를 얻었습니다. 글을 수정하겠습니다. 감사합니다.
  • @u/Psionikus — 추가 복사 작업이 직렬 의존성을 숨기도록 하드웨어 스케줄링을 돕는 걸까요? 피할 수 없는 직렬 의존성이 있는 비벡터 프로그램도 독립적인 명령어 경로로 나누면 CPU가 빈 공간을 더 잘 채워 성능이 좋아지는 사례를 많이 봤습니다.
  • @u/KaMaFour — 이 문제를 피하는 세 번째 음흉한 방법도 있습니다. [Reckless 링크]
    • @u/Shnatsel — 아마 cargo-multivers를 쓰는 편이 나을 것 같습니다.
  • @u/valarauca14 — 처음에는 std::simd로 기계가 해야 할 일을 그대로 재현하려고 했지만, 벤치마크를 거치며 그게 오히려 고생을 부르는 반패턴이라는 걸 알았습니다. 데이터 의미를 그대로 나타내고 LLVM에 맡기려고 Simd<f64, 13>을 썼더니 코드가 더 빨라졌습니다. 컴파일러가 루프와 레지스터 배치를 대상 플랫폼에 맞춰 정리할 수 있습니다.
    • @u/Shnatsel — 그런 기계에서는 256비트 벡터 하나만 고르면 전반적으로 괜찮은 성능을 냅니다. AVX2와 NEON에는 적합하고 AVX-512에서도 나쁘지 않습니다. 다만 고정 폭 벡터는 LLVM이 레지스터 스필을 잘 막지 못해 성능이 크게 떨어질 수 있습니다. 특히 SSE4.2만 지원하는 기계에서 그렇습니다.
  • @u/novacrazy — Thermite 0.3을 크게 개선해 곧 출시할 예정이며, 이 목록의 어떤 라이브러리보다 많은 기능을 제공할 겁니다. 글의 #[simd] 매크로도 부분적으로 Thermite의 #[dispatch]에서 영감을 받았습니다. 버그는 무작위 Reddit 댓글 대신 이슈 트래커에 올렸어야 합니다.
    • @u/R1chterScale — 테스트가 실패한다면 누군가 이슈를 열어야만 하는 건 아닙니다.
    • @u/novacrazy — 올바른 기능 플래그를 넣어 CI를 실행하면 통과했습니다. 기능 플래그를 모른 채 복제해 실행했을 때 좋지 않은 경험을 주긴 하지만, 동작하지 않는 건 아닙니다. std 기능 문제였습니다.

원문: Shnatsel / 번역·요약: Trawling