GN⁺: FFI 속도 향상을 위한 Tiny JITs
(railsatscale.com)CRuby의 FFI 속도를 향상시킬 방법이 있을까?
- Ruby에서 네이티브 코드를 호출해야 할 때, 가능한 한 많은 Ruby 코드를 작성하는 것이 좋음. YJIT는 Ruby 코드를 최적화할 수 있지만 C 코드는 최적화할 수 없기 때문임.
- 네이티브 라이브러리를 호출할 때는 Ruby에서 대부분의 작업을 수행하고, 네이티브 함수 호출을 위한 간단한 API를 제공하는 네이티브 확장을 작성하는 것이 좋음.
- FFI는 네이티브 확장만큼의 성능을 제공하지 않음. 예를 들어,
strlen
C 함수를 FFI로 래핑한 경우, C 확장과 비교하여 성능이 떨어짐.
벤치마크 결과
-
String#bytesize
를 직접 호출하는 것이 가장 빠르며, 이는 기준점으로 생각할 수 있음. - C 확장을 통한
strlen
호출이 두 번째로 빠르고, 간접적으로String#bytesize
를 호출하는 것이 그 다음임. - FFI 구현은 가장 느림. 이는 FFI를 통한 네이티브 함수 호출 시 상당한 오버헤드가 발생함을 보여줌.
현실을 바꿀 수 있을까?
- Chris Seaton의 아이디어로, 외부 함수를 호출하기 위해 JIT 코드를 생성할 수 있는 가능성을 탐색 중임.
- FFI 래퍼 예제에서
attach_function
호출 시, 래퍼 함수 정의 시점에 필요한 기계 코드를 생성할 수 있음.
RJIT 활용
- RJIT는 Ruby로 작성된 JIT 컴파일러로, Ruby와 함께 제공됨.
- RJIT를 gem으로 추출하여 3rd 파티 JIT 컴파일러가 Ruby 데이터 구조를 쉽게 매핑할 수 있도록 함.
- JIT 엔트리 함수 포인터를 항상 실행하여 3rd 파티 JIT가 기계 코드에 등록할 수 있도록 함.
개념 증명
- "FJIT"라는 작은 개념 증명을 통해, 런타임에 기계 코드를 생성하여 외부 함수를 호출할 수 있음.
- 벤치마크 결과, FJIT가 생성한 기계 코드는 C 확장보다 빠르며, FFI 호출보다 2배 이상 빠름.
결론
- C 확장과 동일한 속도(또는 더 빠른 속도)를 유지하면서 가능한 한 많은 Ruby 코드를 작성할 수 있는 가능성을 보여줌.
- Ruby가 FFI 없이 네이티브 코드를 호출할 수 있는 장점을 가질 수 있음.
주의사항
- 현재 ARM64 플랫폼에만 제한됨. x86_64 백엔드를 추가해야 함.
- 모든 매개변수 유형과 반환 유형을 처리하지 않음. 단일 매개변수와 반환만 처리 가능.
- Ruby를
--rjit --rjit-disable
플래그로 실행해야 함. Kokubun의 기능이 적용되면 해결될 것임. - 현재 Ruby 헤드에서만 실행 가능.
Hacker News 의견
-
Java Constraint Solver (Timefold)와 CPython 간의 함수 호출을 위해 FFI를 많이 다루어야 했음
- FFI의 성능 문제는 주로 호스트 언어와 외국어 간의 통신을 위한 프록시 사용에서 발생함
- JNI나 새로운 외국 인터페이스를 사용한 직접적인 FFI 호출은 빠르며, Java 메서드를 직접 호출하는 것과 비슷한 속도임
- 그러나 CPython과 Java의 가비지 컬렉터는 잘 맞지 않아 동기화를 위해 특별한 기술이 필요함
- JPype나 GraalPy와 같은 프록시를 사용하면 성능 오버헤드가 발생하며, 매개변수와 반환값을 변환해야 하고 추가적인 FFI 호출이 발생할 수 있음
- CPython 객체를 Java로 전달하면 Java는 CPython 객체에 대한 프록시를 가짐
- 그 프록시를 다시 CPython으로 전달하면 프록시의 프록시가 생성됨
- 결과적으로 JPype 프록시는 CPython을 직접 FFI로 호출하는 것보다 1402% 느리고, GraalPy 프록시는 453% 느림
- 최종적으로 CPython 바이트코드를 Java 바이트코드로 변환하고, 사용된 CPython 클래스에 해당하는 Java 데이터 구조를 생성함
- 그 결과 프록시를 사용하는 것보다 100배 빠른 성능 향상을 얻음
- CPython 바이트코드를 변환하거나 읽는 것은 매우 불안정하고 문서화가 부족하며, VM의 여러 특이점 때문에 다른 바이트코드로 직접 매핑하기 어려움
- 자세한 내용은 블로그 게시물을 참조할 수 있음: 링크
-
Rails At Scale과 byroot의 블로그 덕분에 현재 Ruby 내부와 성능에 대한 심도 있는 논의에 관심을 가지기에 좋은 시기임
- 최근 Ruby와 Rails의 개선 덕분에 Rubyist로서 좋은 시기임
-
외부 함수 호출을 위해 3rd party 라이브러리를 호출하는 대신 코드를 JIT 컴파일할 수 있는지에 대한 질문
- LuaJIT FFI의 기본 원리라고 확신함: 링크
- LuaJIT의 FFI가 매우 빠른 이유라고 생각함
-
JVMCI를 사용하여 arm64/amd64 코드를 즉석에서 생성하여 JNI 없이 네이티브 라이브러리를 호출하는 라이브러리 관련 정보: 링크
-
"가능한 한 많은 Ruby를 작성하라, 특히 YJIT가 Ruby 코드는 최적화할 수 있지만 C 코드는 그렇지 않기 때문"이라는 의견
- Ruby가 꽤 느린 언어가 아닌가 하는 의문
- 네이티브로 들어간다면 가능한 한 많은 작업을 네이티브에서 하고 싶음
-
10년 이상 Ruby를 사용해왔고, 최근의 발전을 보는 것이 매우 흥미로움
- 기대됨
-
왜 JIT 컴파일이 필요한지에 대한 의문
- C로 작성할 수 있다면 로드 시점에 컴파일할 수 있지 않을까 하는 생각
-
FFI - Foreign Function Interface, 즉 Ruby에서 C를 호출하는 방법
-
이것이 바로 libffi가 하는 일 아닌가 하는 질문
-
tenderlovemaking.com으로 가지 않은 이유를 알 것 같음