우리가 진정으로 원했던 러스트(Rust) 호출 규약 (2024)

Rust는 현재 복합 타입을 처리할 때 LLVM의 보수적인 C 호출 규약(ABI)을 따르고 있어, 12바이트 크기의 [i32; 3] 배열조차 레지스터가 아닌 포인터로 전달하는 비효율성이 발생하고 있습니다. 이를 해결하기 위해 저자는 Go 언어의 레지스터 ABI를 벤치마킹한 '-Zcallconv=fast' 플래그 도입을 제안하며, x86, ARM, RISC-V 등 다양한 아키텍처에서의 코드 생성 최적화를 강조합니다.

AI 요약

2024년 4월 17일 공개된 이 아티클에서 저자는 현재 Rust의 'extern "Rust"' 호출 규약이 지나치게 보수적이며 비효율적이라고 비판합니다. Rust는 현재 LLVM의 C 호출 규약을 주로 활용하며 Clang이 생성할 법한 시그니처를 유지하려 하는데, 이는 디버거 호환성과 LLVM 버그 회피를 위한 선택이지만 실제로는 '형편없는 코드 생성(terrible codegen)'을 초래하고 있습니다. 구체적인 사례로 12바이트 크기의 배열([i32; 3])이 레지스터(rdi, rsi 등)를 통해 효율적으로 전달될 수 있음에도 불구하고, 현재 Rust는 이를 메모리 포인터로 전달하고 있습니다. 저자는 이러한 간극을 메우기 위해 Go의 레지스터 ABI를 참고한 새로운 호출 규약 설계를 제안하며, -Zcallconv 플래그를 통해 기존 방식(legacy)과 최적화 방식(fast)을 선택할 수 있는 구조를 제시합니다.

핵심 인사이트

  • 데이터 처리 비효율성: 12바이트 너비의 [i32; 3] 배열이 레지스터에 충분히 들어갈 수 있음에도 불구하고, 현재 Rust는 이를 포인터 방식으로 전달하여 성능 저하를 유발합니다.
  • Go 레지스터 ABI의 대안 제시: C ABI의 한계를 극복하기 위해 이미 성공적으로 레지스터 기반 인자 전달을 수행하고 있는 Go 언어의 방식을 롤모델로 지목했습니다.
  • 플래그 기반 개선안: 사용자가 -Zcallconv=fast와 같은 컴파일 플래그를 통해 더 공격적이고 효율적인 호출 규약을 선택할 수 있도록 하는 아키텍처를 제안합니다.
  • 디버깅 환경의 유연성: ELF 기반 시스템에서 DWARF 포맷은 특정 ABI에 종속되지 않으므로, 호출 규약을 변경하더라도 디버깅 이슈가 크지 않음을 강조합니다.

주요 디테일

  • 레지스터 활용 현황: extern "C" 요청 시 [i32; 3] 배열은 rdirsi 레지스터에 패킹되어 전달되지만, 기본 Rust 호출에서는 이 최적화가 제대로 이루어지지 않습니다.
  • LLVM과의 관계: Rust는 LLVM 버그를 자극하지 않기 위해 Clang과 유사한 코드 생성을 지향하지만, 저자는 오히려 Rust가 LLVM 버그를 유도하고 이를 수정해 나가는 것이 올바른 방향이라고 주장합니다.
  • 범용 아키텍처 대응: 본문은 x86 어셈블리를 예시로 들고 있으나, 제안된 원칙은 ARM과 RISC-V 등 현대적인 CPU 아키텍처 전반에 적용 가능하도록 설계되었습니다.
  • ABI 구성 요소: 호출 규약이 단순히 인자 전달뿐만 아니라 리턴 값 처리, 함수 프롤로그/에필로그, 언와인딩(unwinding) 방식을 모두 포함하는 개념임을 명시합니다.

향후 전망

  • Rust 성능 최적화: 새로운 호출 규약이 도입될 경우, 복잡한 구조체를 빈번하게 주고받는 Rust 프로그램의 런타임 성능이 유의미하게 향상될 것으로 기대됩니다.
  • 컴파일러 고도화: rustc가 LLVM의 기본 설정에 의존하는 것을 넘어, 언어 특성에 최적화된 독자적인 ABI 생성 전략을 강화할 가능성이 높습니다.
Share

댓글

이 소식에 대한 의견을 자유롭게 남겨주세요.

댓글 (0)

불러오는 중...