Ruby 3.4의 불필요한 암묵적 할당 제거 및 최적화

[EN] Eliminating Unnecessary Implicit Allocations / Jeremy Evans @jeremyevans0

작성자
RubyKaigi
발행일
2025년 05월 27일

핵심 요약

  • 1 Ruby 3.4는 불필요한 암묵적 객체 할당을 제거하기 위한 광범위한 최적화를 단행했습니다.
  • 2 특히, 세 가지 할당 회귀 버그를 수정하고, 향후 회귀 방지를 위해 `Allocation Test Suite`를 도입했습니다.
  • 3 리터럴 배열 및 호출자 측 스플랫 할당 문제를 해결하여 Ruby의 성능을 향상시켰습니다.

도입

본 발표는 Ruby 3.4에서 불필요한 암묵적 할당을 제거하기 위해 적용된 코드 변경 사항들을 심층적으로 다룹니다. 발표자인 Jeremy Evans는 Ruby 커미터로서, 이 과정에서 발견하고 수정했던 세 가지 할당 회귀(Allocation Regressions) 문제와 이를 방지하기 위한 `Allocation Test Suite`의 도입, 그리고 리터럴 배열 및 호출자 측 스플랫(Splat) 할당에 대한 최적화 노력들을 상세히 설명합니다. 이는 Ruby의 성능과 효율성을 크게 향상시키는 데 기여했습니다.

할당 회귀 및 Allocation Test Suite 도입

발표자는 Ruby 3.x에서 발생했던 세 가지 주요 할당 회귀 문제에 대해 설명합니다. 첫 번째는 위치 스플랫, 키워드 스플랫, 블록을 동시에 사용하는 코드에서 불필요한 배열 할당이 발생했던 문제이며, 이는 Ruby 3.4에서 다시 allocationless 상태로 복구되었습니다. 두 번째는 키워드 인수를 받는 메서드를 키워드 스플랫으로 호출할 때 발생한 불필요한 해시 할당 문제였으며, 이 또한 Ruby 3.4에서 해결되었습니다. 세 번째는 위치 스플랫과 정적 리터럴 키워드를 함께 사용할 때 발생한 배열 및 해시 할당 문제로, 이 역시 Ruby 3.4에서 allocationless하게 최적화되었습니다.

이러한 할당 회귀의 반복적인 발생은 근본적인 해결책의 필요성을 제기했습니다. 이에 발표자는 예상되는 동작을 보장하기 위한 테스트의 중요성을 깨닫고 Allocation Test Suite를 개발했습니다. 이 테스트 스위트는 특정 호출 유형이 특정 수의 객체를 할당하는지 검증하며, 코드 변경으로 인한 할당 회귀 발생 시 즉시 문제를 감지할 수 있도록 합니다. Allocation Test Suite는 세 가지 주요 이점을 제공했습니다. 첫째, 도입 이후 추가적인 할당 회귀를 방지하는 데 기여했습니다. 둘째, Prism 컴파일러 팀이 parse_Y 컴파일러보다 더 많은 객체를 할당하는 경우를 찾아 수정하여 파서 전환으로 인한 할당 회귀를 예방할 수 있도록 도왔습니다. 셋째, 각 호출 유형의 암묵적 할당 수를 명확히 보여줌으로써 추가적인 최적화 기회를 식별하는 데 결정적인 역할을 했습니다.

리터럴 배열 최적화

Ruby 3.3에서는 특정 리터럴 배열이 하나 이상의 배열을 할당하는 문제가 있었습니다. 특히, 257개 이상의 요소를 포함하는 대규모 리터럴 배열은 VM 스택 오버플로우를 피하기 위해 256개 단위로 분할되어 여러 배열을 할당했습니다. 또한, 위치 스플랫이나 키워드 스플랫을 포함하는 리터럴 배열도 불필요하게 여러 배열을 할당했습니다.

이러한 문제를 해결하기 위해 Ruby 3.4에서는 기존 VM 명령어를 재활용하거나 새로운 명령어를 도입했습니다. push_to_array 명령어를 사용하여 배열에 요소를 추가함으로써 불필요한 중간 배열 할당을 제거했으며, concat_2_array 명령어를 도입하여 스플랫 부분 구현 시 새로운 배열을 할당하지 않도록 했습니다. 특히, 키워드 스플랫을 포함하는 리터럴 배열을 위해 push_to_array_keyword_splat이라는 새로운 VM 명령어를 추가하여 빈 해시가 아닌 경우에만 객체를 배열에 추가하도록 했습니다. 이러한 최적화를 통해 Ruby 3.4의 모든 리터럴 배열은 단일 배열만 할당하게 되었습니다.

호출자 측 위치 스플랫 할당 최적화

Ruby 3.3에서는 호출자 측 위치 스플랫이 동적 해시를 포함하는 경우 불필요한 배열을 할당하는 문제가 있었습니다. 이는 기존 최적화 프로그램이 정적 키워드나 단일 키워드 스플랫만 처리했기 때문입니다. 이 문제를 해결하기 위해 최적화 로직을 옵티마이저에서 컴파일러(setup_args_core 함수)로 옮겼습니다.

setup_args_core의 API를 수정하여 dup_rest 인수를 정수에서 포인터로 변경함으로써 재귀 호출 전반에 걸쳐 할당 정보를 단일 위치에서 추적할 수 있도록 했습니다. 또한, 배열 할당을 피할 수 있는 네 가지 parse tree 케이스를 식별하고, 안전하지 않은 평가 순서 문제를 방지하기 위해 safe_to_avoid_allocation 함수를 추가하여 해시 노드의 모든 서브 노드를 검사하도록 했습니다. 키워드 인수뿐만 아니라 블록 전달 표현식(block pass expression)도 검사하여 안전하지 않은 경우 배열을 복제하도록 했습니다. 이러한 변경으로 Ruby 3.4에서는 단일 위치 스플랫과 post-splat 위치 인수가 없는 모든 경우에 불필요한 배열 할당이 제거되었으며, Ruby 3.3의 옵티마이저 변경 사항 전체를 제거할 수 있었습니다.

또한, 키워드 스플랫과 블록 전달 표현식에서 변수를 변경할 때 발생하는 평가 순서 문제를 해결하기 위해 dup_rest 포인터가 위치 스플랫 배열 복제 여부와 단일 키워드 스플랫 해시 복제 여부를 모두 추적하도록 확장했습니다. 이를 통해 키워드 스플랫 해시를 복제하여 평가 순서 문제를 방지하는 새로운 컴파일 함수를 도입했습니다.

남은 암묵적 할당

발표자는 여전히 해결하기 어려운 암묵적 할당 사례들도 언급했습니다. 호출자 측에서는 위치 스플랫 뒤에 인수가 있거나 여러 개의 위치 스플랫이 있는 경우, 그리고 리터럴 키워드와 키워드 스플랫이 함께 있거나 여러 개의 키워드 스플랫이 있는 경우 Ruby의 내부 메서드 호출 API의 한계로 인해 배열/해시 할당이 불가피합니다. 호출 받는 측(callee side)에서는 이름 있는 위치 스플랫/키워드 스플랫 매개변수나 리터럴 키워드를 가진 메서드 호출 시 할당이 불가피한 경우가 있습니다. 다만, 위치 스플랫 뒤에 비정적 리터럴 키워드가 오는 경우는 해결 가능성이 있는 것으로 보입니다.

결론

결론적으로, Ruby 3.4는 Jeremy Evans의 헌신적인 노력으로 불필요한 암묵적 객체 할당을 대폭 줄이는 데 성공했습니다. `Allocation Test Suite`의 도입은 향후 할당 회귀를 방지하고 새로운 최적화 기회를 식별하는 데 필수적인 도구가 되었습니다. 리터럴 배열 및 호출자 측 스플랫 할당에 대한 정교한 최적화는 Ruby의 런타임 효율성을 크게 향상시켰습니다. 비록 Ruby의 내부 API 제약으로 인해 일부 암묵적 할당은 여전히 남아있지만, 이번 개선은 Ruby 개발자들에게 더욱 빠르고 효율적인 프로그래밍 환경을 제공하는 중요한 진전입니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!