Reddit

Fearless SIMD v1.0 is here

Fearless SIMD v1.0 출시 — Rust SIMD에서 unsafe 제거하기

Rust용 SIMD 라이브러리 Fearless SIMD가 v1.0으로 안정화됐습니다. 안전한 SIMD intrinsic 접근, 하드웨어 벡터 폭 활용, 함수 멀티버저닝을 제공하며 직접 의존하는 크레이트 30개와 간접 의존 크레이트 1,000개 이상에서 사용되고 있습니다.

AI 요약

Rust 생태계의 SIMD 라이브러리 Fearless SIMD가 v1.0을 출시했습니다. 프로젝트는 SIMD 코드를 작성할 때 반복적으로 등장하는 unsafe 블록을 별도 코드에 몰아넣고, 나머지 구현에서는 Rust의 타입 시스템으로 메모리 안전성을 유지하는 것을 목표로 합니다. 단순한 자동 벡터화와 멀티버저닝부터 portable SIMD 추상화, 플랫폼 intrinsic 직접 호출까지 한 라이브러리 안에서 다룹니다.

성능과 이식성

portable SIMD 추상화에서 자주 나오는 비판은 성능이 충분하지 않다는 점입니다. Fearless SIMD는 플랫폼마다 결과가 달라질 수 있는 연산에 정밀한 변형과 빠른 변형을 함께 제공합니다. 예를 들어 swizzle이나 부동소수점 최댓값 연산은 경계 조건까지 모든 플랫폼에서 같은 결과를 내는 정밀한 버전과, 해당 조건이 발생하지 않는다고 가정하고 플랫폼에 맞는 빠른 결과를 내는 버전을 나눠 제공합니다.

SIMD 알고리즘을 하드웨어의 기본 벡터 폭에 맞춰 작성하는 기능도 제공합니다. 고정된 벡터 크기가 필요한 알고리즘에는 fixed-size 벡터를 사용할 수 있고, 그렇지 않은 코드는 실행 환경의 하드웨어 폭을 활용합니다. portable SIMD 연산 구현은 Rust와 LLVM에 개선 사항을 upstream할 정도로 최적화했으며, portable abstraction으로 다루지 못하는 명령어나 더 세밀한 제어가 필요한 경우에는 플랫폼 intrinsic으로 내려갈 수 있습니다. 이때 intrinsic 접근도 안전한 방식으로 제공하고, 해당 부분에 별도 성능 오버헤드를 추가하지 않는다고 설명합니다.

unsafe를 격리한 안전성 모델

다른 SIMD 추상화 크레이트의 소스에서는 수천 개의 unsafe 블록이 발견되는 경우가 많지만, Fearless SIMD는 ad-hoc unsafe 코드를 요구하지 않도록 설계했습니다. 첫 번째 구성 요소는 kernel! 매크로입니다. 컴파일러의 target feature v1.1을 활용해 대부분의 SIMD intrinsic을 unsafe 없이 호출합니다.

다만 raw pointer를 사용하는 SIMD load/store까지 이 방식으로 처리할 수는 없습니다. Fearless SIMD는 bytemuckzerocopy에서 영감을 얻은 safe transmute 모듈로 이 부분을 감쌉니다. _mm_loadu_epi32 같은 intrinsic도 내부적으로는 일반적인 load와 store로 변환되므로, 재사용 가능한 단일 래퍼로 동작을 재현할 수 있다는 설명입니다. 결국 감사해야 하는 코드는 kernel!과 safe transmute라는 두 개의 작고 독립적인 구성 요소로 제한됩니다. 두 구성 요소가 메모리 안전성을 지키면 나머지 코드베이스도 타입 시스템의 보장을 받습니다.

멀티버저닝과 사용성

함수 멀티버저닝은 기존에도 까다로운 영역이었습니다. 한 방식은 #[inline(always)]를 직접 붙이고 인라이닝이 성능에 미치는 영향을 이해해야 했습니다. 다른 방식은 모든 함수 호출에 작은 오버헤드를 추가합니다. 함수가 매우 작으면 이 비용이 문제가 될 수 있고, 결국 일부 함수에는 다시 #[inline(always)]를 수동으로 붙여야 합니다.

v1.0과 함께 fearless_simd_macros v0.1도 공개했습니다. SIMD 함수에 #[simd] 매크로를 붙이면 내부에서 어떤 멀티버저닝이 일어나는지 직접 관리하지 않아도 됩니다. 절차적 매크로를 원하지 않는 사용자를 위해 기존 방식도 남겨뒀습니다. 다만 현재는 여전히 약간의 보일러플레이트가 필요하며, 향후 Struct Target Features RFC를 비롯한 컴파일러 지원으로 #[simd] 표기 자체를 없애는 방안을 검토하고 있습니다. 사용성 관련 API는 발전할 수 있지만, fearless_simd 코어 크레이트의 안정성 보장은 유지하며 매크로 사용 여부와 관계없이 현재 작성한 코드를 계속 사용할 수 있다고 밝혔습니다.

안정성 및 표준 라이브러리와의 관계

Fearless SIMD v1.0과 이후 버전에는 3년간 보안 업데이트를 제공합니다. 가까운 시기에 Rust에 추가될 f16 타입과 장기적으로 지원 가능성이 있는 ARM SVE, RISC-V Vector Extension도 API를 깨지 않고 수용할 경로가 있다고 설명합니다.

Rust 표준 라이브러리의 std::simd 안정화를 환영하지만, 그것이 Fearless SIMD를 대체하지는 않는다고 봅니다. 표준 라이브러리는 반드시 포함해야 하는 SIMD 기능을 제공하고, 멀티버저닝이나 하드웨어 벡터 폭 활용 같은 기능은 생태계 크레이트에 남길 가능성이 큽니다. Fearless SIMD는 stable Rust에서 동작하는 std::simd와 동등한 기능을 포함하지만, 그보다 넓은 범위의 기능을 제공하는 것이 차이점입니다. std::simd가 안정화되면 내부 구현을 표준 기능으로 옮겨 자체 코드 일부를 삭제하고 더 많은 특수 플랫폼을 지원할 계획입니다.

현재 Fearless SIMD를 직접 의존하는 크레이트는 30개이며, 간접적으로 의존하는 크레이트는 1,000개가 넘습니다. 프로젝트는 문서와 예제를 통해 도입할 수 있고, 질문은 Zulip에서 받을 예정이라고 안내합니다.

Reddit 반응

  • @u/afl_ext — 좋습니다. 이제 이걸 제 소프트웨어 렌더러에 넣어 보겠습니다.
  • @u/Orjigagd — 멋진 작업입니다. 고맙습니다.
  • @u/XtremeGoose — 정말 좋습니다. 제가 이해한 바로는 #[simd] 예제 함수에서 관련 SIMD 기능이 런타임에 사용 가능하다는 사실을 컴파일러에 확인하지만, 특정 기능을 반드시 사용하도록 강제하지는 않는 것 같습니다. 테스트에서 컴파일러가 일반적으로 사용 가능한 SIMD 방식 중 최선의 방법을 잘 선택했습니까? 같은 명령어를 두 가지 다른 레벨에서 사용하기로 결정하면 컴파일러가 두 구현을 중복 제거합니까? 동일한 구현을 두 개 이상 선택하는 과정에서 런타임 비용을 지불하게 됩니까? 사용자는 이 결과를 어떻게 확인합니까?
    • @u/Shnatsel — 자동 벡터화를 뜻한다면 결과가 다릅니다. Matklad의 글을 참고할 수 있습니다. 명시적 portable SIMD에서 f32x4 같은 타입을 사용하는 경우에는 내부적으로 intrinsic에 매핑되므로 사용 가능한 SIMD 명령어가 거의 확실하게 사용됩니다. 서로 다른 두 레벨에서 같은 명령어를 사용하게 됐을 때 구현을 중복 제거할지는 컴파일러에 달려 있습니다. 가능하지만 보장되지는 않습니다. Cargo.tomllto = true를 설정하면 중복 제거 가능성이 훨씬 커집니다. 레벨을 선택하는 런타임 비용은 여전히 남습니다. 비용은 매우 작지만 0은 아닙니다. 확인하려면 생성된 어셈블리를 직접 살펴보는 편이 좋습니다. rustcemit=asm 인수가 한 방법이지만 LTO 이전에 실행되므로 LTO 결과는 반영하지 않습니다. 최종 바이너리에는 아무 디스어셈블러나 사용할 수 있습니다. 저는 프로파일러인 samply의 어셈블리 뷰를 선호합니다. 현재 SIMD 레벨에 맞춰 선택된 구현과 실제 실행된 코드를 자동으로 보여주기 때문입니다.

원문: Linebender 블로그 / 번역·요약: Trawling