YJIT Makes Rails 1.7x Faster

00:00:10.759

Hi, everyone! Today I'm going to talk about a position called YJIT that makes Rails 1.7 times faster.

안녕하세요, 여러분! 오늘 저는 Rails를 1.7배 더 빠르게 만드는 YJIT 라고 불리는 요소에 관해 이야기 하려고 합니다.

00:00:13.440

I need to apologize; this was a lie. As of this morning, it's actually 1.8 times faster. My name is Takashi Kokubun, and I work for Shield on the Ruby Infrastructure Team.

좀 전에 한 말이 거짓이라고 사과해야겠네요. 오늘 아침에 확인해보니 사실 1.8배 빨랐습니다. 제 이름은 타카시 코쿠분 이고 저는 Shield의 루비 인프라스트럭쳐 팀에서 일하고 있습니다.

00:00:23.160

Shield has a Ruby infrastructure team that was explained in the previous session. It consists of five people. Under that team, we have various subteams such as the Ruby Infra Team and the Ruby DX Team. I'm part of the Ruby Infra Team, which consists of those people.

이전 세션에서 이야기 했듯 실드에는 루비 인프라스트럭쳐 팀이 있습니다. 팀은 다섯명으로 이뤄졌고 팀 안에서도 루비 인프라 팀과 루비 DX 팀 같이 다양한 하위 팀을 이루고 있습니다. 저는 이런 사람들과 함께 루비 인프라 팀의 일원입니다.

00:00:42.520

Now, I'm going to talk about YJIT. YJIT is a Just-In-Time (JIT) compiler. The purpose of this talk is to make you feel that you need to use it in production. You should also consider upgrading your Ruby version to 3.3, the latest version.

이제 YJIT에 대해 이야기 해보겠습니다. YJIT는 Just-In-Time(JIT) 컴파일러입니다. 이 발표의 목적은 여러분들이 실제 프로덕션 환경에서 이 기능을 쓰고 싶도록 하는 것입니다. 거기에 더해 루비 3.3 최신 버전으로 업그레이드 하는 것도 고려하길 바랍니다.

00:01:01.760

What it does is when YJIT is enabled—it's disabled by default—you need to turn it on explicitly. If you enable YJIT and call some methods more than 30 times, it triggers JIT compilation, which converts that method to native code. From that entry point, the method's code will also be compiled.

YJIT 는 기본적으로 꺼져 있기 때문에 여러분 스스로 직접 켜는 것이 필요합니다. YJIT가 활성화 되고 30회 이상 호출되는 메소드들은 JIT 컴파일이 트리거 되어 네이티브 코드로 변환됩니다. 메소드의 진입점부터 메소드의 코드는 컴파일 될 겁니다.

00:01:39.320

In production, we are using YJIT for our most important services. One of those is Shiseido's ST renderer, which is 177% faster in production. This service has the largest traffic in our company—billions of requests—and is battle-tested. I've talked to many people here at RubyKaigi, and they've told me that they are using YJIT and thank us for it.

저희는 프로덕션 환경에서 가장 중요한 서비스에 YJIT를 사용하고 있습니다. 이 중 하나는 시세이도의 ST 렌더러로 177% 빨라졌습니다. 이 서비스는 회사에서 가장 큰 트래픽, 수십억 건의 요청을 처리하며 실전에서 테스트 되었습니다. 저는 여기 루비카이기에서 많은 분들과 이야기를 나눠봤는데 그분들도 YJIT를 쓰고 있고 저희에게 감사하다고 말씀해주셨습니다.

00:02:25.200

Now, I wonder how many of you are currently enabling YJIT in production? Could you raise your hands? I want everyone to raise their hands next year, so please consider enabling it.

이제 저는 얼마나 많은 분들이 현재 프로덕션에서 YJIT를 쓰고 계시는지 궁금해지는데요. 손 들어주실 수 있나요? 내년에는 여기 모든 분들이 손 드셨으면 좋겠습니다. YJIT 를 켜는 것을 고려해보시길 바랍니다.

00:03:07.879

Enabling YJIT can be done in several ways. The most basic method is to add the '-W' option to the Ruby command. In addition, the Ruby opt environment variable is available by default. There's also a special environment variable specific to YJIT: RUBY_YJIT_ENABLE. If you pass this environment variable, YJIT will be turned on by that configuration.

YJIT는 여러 방법으로 활성화 할 수 있습니다. 기본적인 방법은 루비 커맨드를 사용 할 때 -w 옵션을 붙이는 것입니다. 거기다 기본적으로 루비 opt 환경 변수도 가능합니다. YJIT에는 RUBY_YJIT_ENABLE 이라고 쓰는 특별한 환경 변수가 있습니다. 이 환경 변수를 true로 넘길 경우 구성에 의해 YJIT가 켜집니다.

00:03:38.120

Starting from Ruby 3.3, we have a Ruby VM method called 'rb_vm_yjit_enable' that can be called from a Ruby script without needing to set any configuration beforehand. The good news is that Rails 7.2 will include a default initializer that enables YJIT when using Ruby 3.3. As long as you upgrade Ruby and Rails to these versions once they are released, YJIT will effectively become the default JIT compiler in production.

루비 3.3 부터는 rb_vm_yjit_enable 이란 루비 VM 메소드가 생겼습니다. 이 메소드는 어떤 설정 없이 루비 스크립트만으로 YJIT를 켤 수 있습니다. 좋은 소식은 Rails 7.2가 Ruby 3.3을 확인 할 경우 YJIT를 활성화하는 기본 초기화를 포함할 것이라는 것입니다. Ruby와 Rails를 이런 버전으로 업그레이드하기만 하면 YJIT는 프로덕션 환경에서 사실상 기본 JIT 컴파일러가 됩니다.

00:04:21.560

Please use it when you upgrade Rails to 7.2, or you can also use it as long as you upgrade to Ruby 3.2 or 3.3. One of the challenges in enabling YJIT in production is memory usage. There is always a trade-off; gaining performance might mean using more memory.

레일즈를 7.2로 업그레이드 할 때 사용하거나 루비 3.2나 3.3으로 업그레이드 하는 경우에도 사용 할 수 있습니다. 프로덕션 환경에서 YJIT를 활성화 할 때 겪는 어려움 중 하나는 메모리 사용량입니다. 트레이드 오프는 항상 일어납니다. 성능 향상은 더 많은 메모리 사용을 의미할 수 있습니다.

00:05:00.760

One trick we're recommending to the community is to adjust the parameter 'YJIT_EXEC_MSIZE,' which controls the number of bytes allocated for JIT code. By default, this is set to 48MB as of Ruby 3.3. As of Ruby 3.2, it was 64MB. This default is meant to maximize speed, but in many deployments, you may want to reduce the memory usage for your service instead.

커뮤니티에 권장하는 한가지 팁은 JIT 코드에 할당된 바이트 수를 제어하는 'YJIT_EXEC_MSIZE' 파라미터를 조정해보라는 것입니다. 이 파라미터는 기본적으로 루비 3.3에서 48MB, 3.2에서는 64MB로 되어 있습니다. 이 기본값은 속도를 최대화하기 위한 것이지만, 여러분은 서비스의 메모리 사용량을 줄이는 것을 원할 수도 있습니다.

00:05:50.280

In that case, you could try reducing it to 32MB, 24MB, or even 16MB. I have heard that some teams are using a smaller memory configuration, and I think as long as your application is smaller than theirs, you should be able to do that too. The caveat is that 'YJIT_EXEC_MSIZE' only controls the JIT code size, and YJIT also needs to maintain metadata for each JIT code.

이런 경우엔 32MB, 24MB 심지어 16MB로도 줄일 수 있습니다. 어떤 팀은 더 작은 메모리 구성을 사용한다고 듣기도 했는데 여러분의 어플리케이션이 그것보다도 작다면 해볼 수 있다고 생각합니다. 주의할 점은 'YJIT_EXEC_MSIZE'는 JIT 코드 크기만 제어하며, YJIT는 각 JIT 코드에 대한 메타데이터도 유지해야 한다는 것입니다.

00:06:39.680

If you configure 'YJIT_EXEC_MSIZE' to, say, 32MB, it could also require additional metadata memory, such as 64MB or 96MB. Managing this configuration helps control YJIT's memory usage. We provide tips for production deployments in the Ruby documentation for YJIT, so please check it out.

예를 들어 'YJIT_EXEC_MSIZE' 를 32MB로 구성하면 64MB 또는 96MB와 같은 메타데이터 메모리가 더불어 필요할 수 있습니다. 이 구성을 관리하면 YJIT의 메모리 사용량을 제어하는 데 도움이 됩니다. Ruby 문서에서 YJIT의 프로덕션 배포를 위한 팁을 제공하고 있으니 확인해 보세요.

00:07:01.440

The next part is about improvements made in Ruby 3.3 that help explain why YJIT is getting faster with each version upgrade. I want you to understand these upgrades and encourage you to use the latest version in production.

다음 부분은 루비 3.3에서 이뤄진 개선 사항에 관한 것으로 YJIT가 매 업그레이드마다 왜 더 빨라지는지 설명할 수 있을 것 같습니다. 저는 여러분들이 이런 업그레이드를 이해하고 프로덕션 환경에서 최신 버전을 사용하기를 권하고 싶습니다.

00:07:55.360

Among the many features released in 3.3, one significant change improved Rails performance by 7%. Although there was a previous discussion of 5% improvement in string manipulation, 5% is significant—especially if you've worked on performance optimization. In this context, a 7% improvement is even more substantial.

3.3에서 출시된 많은 기능 중에서 한 가지 중요한 변경 사항은 Rails 성능을 7% 향상시켰습니다. 문자열 조작에서 5% 개선에 대해 이전에 논의한 적이 있었습니다. 특히 성능 최적화 작업을 해본 적이 있으시다면 5%는 상당한 수치라는 것을 아실겁니다. 이런 맥락에서 보면 7%의 개선은 훨씬 더 큰 의미가 있습니다.

00:08:39.240

If we look at the list of changes in Ruby 3.3, can you guess which one was the most significant in terms of Rails' performance? Someone from the audience suggested that it was the implementation of the blank or present method. While interesting, that was not the case. The most significant change is actually more complex and can be challenging to explain.

루비 3.3의 변경 사항 목록을 보면 Rails 성능 면에서 가장 중요한 변경 사항이 무엇인것 같습니까? 청중 중 한 분이 blank 또는 present 메서드의 구현이라고 이야기 주셨습니다. 흥미롭지만, 그건 아니었습니다. 가장 중요한 변경 사항은 실제로 더 복잡하며 설명하기 어려울 수 있습니다.

00:09:44.440

Let me explain a method called 'fullbacks'. In the Ruby virtual machine, we compile Ruby source code into instructions called 'instruction sequences'. Although you don't need to understand all the sequences, I wanted to show code to illustrate what is happening behind the scenes. During compilation, we optimize the instructions as long as we can successfully compile each instruction.

'풀백(fullbacks)' 메소드에 대해 설명해보겠습니다. 루비 가상 머신에서 루비 소스 코드를 '명령어 시퀀스(instruction sequences)'라는 명령어들로 컴파일합니다. 이 모든 시퀀스를 이해할 필요는 없지만 뒤에서 어떤 일이 일어나고 있는지 설명하기 위해 코드를 보여드리려고 합니다. 컴파일하는 동안 각 명령어를 성공적으로 컴파일할 수 있을 때까지 최적화를 해나갑니다.

00:10:08.919

In many cases, all instructions are supported by YJIT, yielding a 'ratio in YJIT' of 100%. However, some Ruby code patterns are not optimizable by YJIT. For instance, certain keyword rest patterns do not support optimization. When we encounter unsupported patterns, we need to generate a side exit. A side exit means exiting the JIT code and returning to the interpreter for execution, which is slower compared to executing specialized machine code.

대부분의 경우 YJIT는 모든 명령어을 지원하며, 'YJIT 비율'은 100%가 됩니다. 그러나 일부 루비 코드 패턴은 YJIT가 최적화할 수 없습니다. 예를 들어, 특정 키워드 rest 패턴은 최적화를 지원하지 않습니다. 지원되지 않는 패턴을 만나면 사이드 이그짓(side exit)을 생성해야 합니다. 사이드 이그짓은 실행을 위해 JIT 코드를 종료하고 인터프리터로 돌아가는 것을 의미하며, 이는 특별한 머신 코드를 실행하는 것보다 느립니다.

00:11:00.480

In this context, we might report a 'ratio in YJIT' of only 33%, suggesting that only one-third of the code is optimized. We've been able to compile most instructions, but complications arise when unsupported method codes are encountered.

이런 상황에서 'YJIT 비율'이 33%에 불과하다는 것은 코드의 1/3만 최적화되었다는 말이 됩니다. 대부분의 명령어들을 컴파일할 수 있었지만, 지원되지 않는 메서드 코드가 발견되면 복잡한 문제가 발생합니다.

00:11:54.000

What we did in version 3.3 was to adjust this. By leveraging the functionality of previous JIT implementations, we updated how we handle these scenarios. For example, by calling back to the interpreter correctly, we can optimize the instructions following a method call. This increased the optimization ratio significantly.

3.3 버전에 우리가 한 일은 이러한 것들을 조정하는 것이었습니다. 이전 JIT 구현을 지랫대 삼아 이런 시나리오의 처리 방식을 업데이트했습니다. 예를 들어, 인터프리터로 올바르게 콜백함으로써 메서드 호출에 따라오는 명령어를 최적화할 수 있습니다. 이렇게 하면 최적화 비율이 크게 높아집니다.

00:12:43.440

In Ruby 3.2, the optimization ratio typically dropped to about 90%, meaning 10% of Ruby virtual machine instructions were not compiled. However, upgrading to Ruby 3.3 improved that ratio to an impressive 99%.

루비 3.2에서는 일반적으로 최적화 비율이 90% 가까이 떨어졌습니다. 이것은 루비 가상 머신 명령어의 10%는 컴파일 되지 않았다는 이야기가 됩니다. 그러나 루비 3.3으로 업그레이드 하니 그 비율이 99%까지 올라왔습니다.

00:13:48.480

Please upgrade Ruby to 3.3. This is a main takeaway from this presentation. We've also discussed unsupported codes and the concept of 'Megamorphic' dispatch.

루비를 3.3으로 업그레이드하십시오. 이것이 이 발표의 요점입니다. 또한 지원되지 않는 코드와 '메가모픽' 디스패치 개념에 대해서도 논의했습니다.

00:14:32.960

Megamorphic dispatch is a significant version of polymorphic dispatch. For example, when a method calls an 'X' method through various sub-classes, the dispatching mechanism needs to be efficient. Ruby's JIT can cache multiple entries in a single method call, optimizing performance. However, there is a limit to how many chains we can append before the performance suffers due to overhead.

메가모픽 디스패치는 다형성(polymorphic) 디스패치의 중요한 버전입니다. 예를 들어, 메서드가 다양한 하위 클래스를 통해 'X' 메서드를 호출할 때 디스패치 메커니즘은 효율적이어야 합니다. 루비의 JIT는 단일 메서드 호출에서 여러 항목을 캐시하여 성능을 최적화할 수 있습니다. 그러나 오버헤드로 인해 성능이 저하되기 전에 추가할 수 있는 체인 수에는 제한이 있습니다.

00:15:22.760

In previous versions, when a side exit occurred from this method, the instructions that followed were not compatible due to this limitation. But in Ruby 3.3, we've optimized how we handle this to improve performance drastically. We can reduce exits and enhance the efficiency of method calls.

이전 버전에서는 이 메서드에서 사이드 이그짓이 발생하면 이런 제한으로 인해 다음 명령어가 호환되지 않았습니다. 그러나 루비 3.3에서는 이를 처리하는 방식을 최적화하여 성능을 크게 향상시켰습니다. 이그짓을 줄이고 메서드 호출의 효율성을 높일 수 있습니다.

00:16:34.960

The next topic concerns exception handling. Exceptions in Ruby happen at various points, not only in the 'raise' method. Internally, almost any interruption, like a 'break' in an EACH block, is managed by an exception mechanism in the Ruby VM.

다음 주제는 예외 처리와 관련됩니다. 루비에서 예외는 'raise' 메서드뿐만 아니라 다양한 시점에서 발생합니다. 내부적으로 EACH 블록의 'break'와 같은 거의 모든 인터럽트는 루비 VM의 예외 메커니즘에 의해 관리됩니다.

00:17:25.680

When you reach a 'break' statement, Ruby needs to pop multiple frames off the stack. The mechanism currently allows easy handling of this in Ruby 3.3, which greatly improves performance by providing JIT compilation support for managing exceptions.

'break' 문을 만나면 루비는 스택에서 여러 프레임을 꺼내야 합니다. 현재 메커니즘은 루비 3.3에서 이를 쉽게 처리할 수 있도록 하여 예외 관리를 위한 JIT 컴파일 지원을 제공함으로써 성능을 크게 향상시킵니다.

00:18:17.919

As a result, the exception handling ratio has moved from about 60% in Ruby 3.2 to 100% in Ruby 3.3.

결과적으로 JIT의 예외 처리 비율은 루비 3.2의 약 60%에서 3.3의 100%로 늘었습니다.

00:19:15.440

Though this upgrade does improve the performance, keep in mind that the increase in code complexity can potentially lead to greater memory consumption. However, our experience suggests that fine-tuning the memory settings should not significantly hinder performance.

이 업그레이드가 성능을 향상시키지만, 코드 복잡도가 증가하면 더 많은 메모리 소비가 있을 수 있다는 점을 알고있어야 합니다. 그러나 우리의 경험에 따르면 메모리 설정을 미세 조정하더라도 성능에 큰 영향을 미치지는 않았습니다.

00:20:14.560

The next topic is new behavior for stack values. The Ruby virtual machine is a stack-based VM, and we have some optimizations for accessing stack values, streamlining operations!

다음 주제는 스택 값의 새로운 동작입니다. 루비 가상 머신은 스택 기반 VM이며, 스택 값에 액세스하고 작업을 간소화 하도록 몇 가지 최적화 작업을 했습니다.

00:21:25.440

In version 3.2, operations on Ruby code primarily utilized memory, which is generally slower than accessing registers closer to CPU. Ruby 3.3 will leverage this by optimizing how values are managed during computation.

3.2 버전에선 루비 코드의 연산은 주로 메모리 상에서 이뤄지는데 이는 보통 CPU에 가까운 레지스터에 접근하는 것보다 느립니다. 루비 3.3에선 연산 중에 값을 관리하는 방식을 최적화해 활용합니다.

00:22:11.360

Additionally, the team is working on further enhancements for Ruby 3.4, including optimizations around lower variable access, expected to provide significant performance improvements.

또한 팀은 더 적은 변수 접근에 대한 최적화를 포함한 루비 3.4의 추가 개선 작업을 진행하고 있으며, 이는 상당한 성능 향상을 제공할 것으로 예상됩니다.

00:22:59.360

Improvements in method inlining have also been made, particularly for Ruby on Rails applications. For instance, Rails 7.2 introduces enhancements for present and blank methods, providing faster performance compared to earlier versions.

메소드 인라이닝도 개선되었는데, 특히 레일즈 애플리케이션에서 더욱 그렇습니다. 예를 들어 레일즈 7.2 에서 present 와 blank 메소드에 개선사항을 도입하여 이전 버전에 비해 더욱 빠른 성능을 제공합니다.

00:23:53.520

Ruby 3.3 allows inlining of single-line methods that return immediate values like integers or true/false values. Such optimizations lead to significant reductions in the instruction overhead for common Ruby operations.

루비 3.3은 정수 또는 참, 거짓 값과 같은 즉각적인 값을 반환하는 단일 라인 메소드의 인라이닝을 허용합니다. 이런 최적화를 통해 일반적인 루비 연산에 대한 명령어 오버헤드를 크게 줄일 수 있었습니다.

00:24:54.360

All these changes continuously enhance the core Ruby ecosystem, allowing for cleaner Ruby code and improving overall performance across the board. This includes techniques for optimizing method calls in C code, where we acknowledge existing challenges but strive for effective implementations.

이런 모든 변경들은 핵심 루비 생태계를 지속적으로 향상시켜 더욱 깔끔한 루비 코드를 가능케하고 전박적으로 성능 향상을 이뤄냅니다. 여기엔 C 코드의 메서드 호출을 최적화하는 기술이 포함되며, 기존의 어려운 문제들을 인정하면서도 효과적인 구현을 위해 노력합니다.

00:25:54.560

As we innovate to further improve performance, we encourage contributions to the Ruby C implementation to replace C methods with Ruby, fostering clarity and maintainability.

성능을 더욱 향상시키기 위한 혁신을 거듭하는 가운데, 명확성과 유지보수성을 높이기 위해 C 메서드를 루비 코드로 대체하는 루비 C 구현체에 여러분의 기여를 부탁드립니다.

역자 주: 루비 3.3 에서는 Integer#times, 3.4 에서는 Array#each 가 C 코드에서 루비로 재작성 되었습니다. 루비로 변경 된 코드들은 YJIT가 컴파일 할 수 있게 됩니다.

00:27:04.400

Finally, we must also stress the importance of enabling YJIT in production and upgrading to Ruby 3.3. Many users observe measurable performance gains, so please consider upgrading your applications to the newer versions as they become available.

마지막으로, 프로덕션 환경에서 YJIT를 활성화하고 루비 3.3으로 업그레이드하는 것의 중요성을 강조해야 합니다. 많은 사용자가 측정 할 수 있을 정도의 성능 향상을 경험하고 있으므로, 여러분의 애플리케이션도 업그레이드하는 것을 고려해 보시길 바립니다.

00:28:10.720

That's all! Thank you!

여기까지 입니다. 감사합니다.

관련 링크들

Ruby의 속도를 높이기 위한 C 코드의 Ruby 재작성 | GeekNews
Ruby의 성능 향상: C를 Ruby로 다시 작성하기Ruby의 성능 비교최근 언어 비교 리포지토리에서 Ruby는 R과 Python보다 빠르지만, 세 번째로 느린 언어로 평가됨.벤치마크는 “Loops”와 “Fibonacci” 두 가지로 구성되며, 각각 루프와 조건문, 함수 호출 오버헤드 및 재귀 성능을 강조함.Ruby와 Node.js의 성능 비교M3 MacB