2P by neo 15일전 | ★ favorite | 댓글 1개

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으로 가지 않은 이유를 알 것 같음