RubyKaigi 2015

Turbo Rails with Rust

http://rubykaigi.org/2015/presentations/wycats_chancancode

Ruby is not the fastest language in the world, there is no doubt about it. This doesn't turn out to matter all that much – Ruby and its ecosystem has so much more to offer, making it a worthwhile tradeoff a lot of the times.

However, you might occasionally encounter workloads that are simply not suitable for Ruby. In this talk, we will explore building a native extension with Rust to speed up parts of Rails. (No prior experience with Rust required!) What does Rust has to offer in this scenario over plain-old C? Let's find out!

RubyKaigi 2015

00:00:06.660 Konnichiwa! Watashi o our new home. Unfortunately, that's all the Japanese I managed to learn for this talk, so the rest will be in English. I've translated a few slides into Japanese, so here's the title of my talk in Japanese. The second line is my name in Japanese; for some reason, they look pretty similar to me. If you understand Japanese, maybe you can explain that to me later. Anyway, I'm Godfrey. You can find me on the Internet as Changing Code. You know I'm legit because I have the same shirt as that guy on GitHub. I'm a speaker here at RubyKaigi.
00:00:23.260 I thought that was pretty cool when I saw that yesterday because those Chinese characters actually mean something else in Chinese. If you'd like to be in an author-climber, you should probably come speak next year. Anyway, I came from a land far away called the United States. In case you don't believe me, this is a picture of me enjoying some authentic American cuisine at Portland Airport a few days before I came here.
00:00:35.620 Even though I live in the States, I'm actually Canadian. This is a picture of me representing my country on a TV show in the United States. Given my unique background, I thought I would do a little bit of cultural exchange and introduce you to my country. This is how we report temperature in Canada. You're probably wondering what that 'C' stands for. Turns out, the system was invented in Canada, so it stands for 'Canada.' When you use it in a sentence, you usually say, 'It's 14 degrees in Canada.'
00:00:51.520 Even though it's invented in Canada, it's actually pretty popular. Here are some of the countries that use that system. On the other hand, this is how they do it in the United States. You're probably wondering what that stands for. Obviously, that stands for 'freedom.' The system has degrees of freedom. So, usually, you would say, 'It's 57 degrees of freedom.' This is a list of countries that use this system. If you want to know more about Canada, you can run 'gem install Canada' on your computer, which allows you to program with a Canadian accent.
00:01:06.680 And since this is RubyKaigi, I would like to mention that it's also available for every Ruby version. Anyway, you're probably wondering, 'What is this guy doing? Where's Yehuda?' Well, it turns out that on the web, there's a thing called 'the fold,' so you have to scroll past the fold to see me, but I'm here. The lesson here is to always check the fine print so you don't end up in a bait-and-switch scenario like this. Anyway, let's talk about Ruby.
00:01:31.150 Ruby has many strengths. There are a lot of great things about Ruby; it's high-level, predictable, dynamic, and object-oriented. It's a post-meta programming language. Since you're sitting here, I probably don't need to sell these to you. You already know why it's great. However, Ruby is also pretty slow.
00:01:36.790 In my experience, it gets about three times faster every few years. Unfortunately, the current state of affairs is that it's still pretty slow. This hasn't turned out to matter for the most part. As you can see, there are a lot of companies building big apps, like real samples and that weapon, and for the most part, it's fine. But occasionally, there are things you want to do that Ruby is just not fast enough for. This is not unique to Ruby; you might hear a lot of people are doing machine learning or number-crunching tasks in Python.
00:01:59.590 But it turns out the most intensive parts are actually performed in C. C is low-level and pretty imperative, and it can be dangerous, but on the bright side, it’s pretty fast because it's close to the metal. In Ruby, you can get the best of both worlds by writing native extensions. For example, when you run 'gem install JSON' on your computer, it actually ships with two implementations. First, there's 'json pure,' which is like a pure Ruby implementation of the JSON encoder. But if you are installing this on a platform that supports it, it will usually use the native version which is written in C and is a lot faster.
00:02:31.480 So as a user, it's completely transparent to you, and you just get the benefit of the speed of C when you invoke it like your normal Ruby methods. But under the hood, it's actually completely within C, so it's very fast. By now, you've probably learned to read the fine print. As you can see, there's a star there. What is the fine print? This is obviously great for end users, but as a person writing the native extension, it's not so fun.
00:02:47.490 Writing code in C, like I said, is low-level, and it could be pretty dangerous. That could also mean that it's not great for end users because if you make a mistake, it might crash the entire process at runtime. In my work, I am a member of the Rails Core Team, and the inventor of Rails taught me a thing or two. When we want to implement certain features, we are willing to jump through some hoops under the hood to make things nice to use for the user.
00:03:05.389 I would take the liberty to extrapolate that it's not just to make the API look pretty; we are willing to jump through hoops on the implementation side to make the whole experience faster and nicer. No one likes slow code. So, when we can, we should do whatever is in our power to make things fast so you can benefit from it without knowing what's going on under the hood.
00:03:20.990 You might have heard of Sam Saffron. He did a lot of performance work and ran a pretty big Rails app called Discourse. He spent a lot of time profiling Discourse and one of the things he noticed is that there's a method called 'string blank' that shows up a lot in the profiler output. This is the implementation in ActiveSupport; it just checks if a string consists only of whitespace characters. This is pretty simple and easy to read and, unfortunately, this method is called in a lot of places, and expectedly, it turns out to be somewhat costly across the entire request because it's called multiple times.
00:04:00.590 This is not the fastest implementation. So, what he did was he wrote a C extension for that method and re-implemented it in C. That's great—it's up to 20 times faster! I would hypothesize that in Rails or even in your app, there are probably opportunities like that where you can re-implement a pretty stable part of your program in C and get some performance benefits. But I'm not Sam Saffron; I’m not a C programmer. I know just enough C to be dangerous. When I program in C, and a variable doesn't do what I want, I probably add a star, and if that still doesn't work, maybe add more stars, and if that doesn't work, maybe use ampersand.
00:05:05.569 The tricky thing is I can actually write a C program that compiles, but at runtime, it might blow up on me unexpectedly, and I might not be able to figure out what's wrong. So, personally, I would be pretty nervous about writing a native extension and telling people to use it in production and having to support that. Recently, a developer at a company called Tilter suggested that I should learn Rust. So, let me take some time to tell you why you should perhaps care.
00:05:34.260 I apologize in advance for how fast I talk. Every time I come to RubyKaigi to speak, the only feedback I hear afterwards is, 'That seemed great, but you spoke so fast I couldn't really keep up.' I’m sorry! I was on the Rails Core Team, and I spent a long time trying to make things faster. That was the original pitch I had, and unfortunately, one of the things that was sad about trying to do that in Ruby is the cost of abstracting something. You may want to abstract something for developer experience to make it easier to plug in, but that always has a cost in performance.
00:06:42.220 So it's very difficult to achieve both modularity—which was a big goal at the time—and also performance. It turned out we got modularity, but not as much performance as we would have liked. I worked on the Rails Core Team, and that's that story. More recently, I joined TC39, which is the committee that makes JavaScript. I've worked a lot on that committee and learned a lot about language design. It's pretty fun! You should definitely check out JavaScript; I think it has been improving.
00:07:00.590 If your view of JavaScript is still stuck five or ten years ago and you think it's a terrible language, you should keep an eye on it because it's getting better. Finally, I also recently joined the Rust Core Team. I guess I traded the Rails Core Team for the Rust Core Team, and I’m not sure if that was an upgrade or downgrade. The Rust project's motto is 'hack without fear.' It's basically the same story Godfrey was telling before, which is that a lot of people want to write low-level code to make something faster.
00:08:13.990 But many people writing Ruby programs don't want to take time out of their day to switch from Ruby into writing C. At least for me, I didn't feel confident that I could get away with it without blowing up my app or my users' apps in production. For me, the great thing about Rust is that it allows people to hack at the low level without fear of causing major issues in a production environment. Let me talk a little bit about how Rust works and how it fits into Godfrey's introduction.
00:09:30.270 If you've ever written C code before, you'll be familiar with the basic unit of data in Rust, which is called a 'struct,' just like in C. You can write a struct and include fields in it. Unlike in C, if you wanted to write functions that work with the struct, you would write those functions and the first parameter would be the struct you just wrote. In addition, you can say: 'I want to implement some functions for Circle.' Then you can create implementations like a class method called 'new' that constructs a new Circle, while an instance method called 'diameter' returns the diameter of the circle.
00:10:58.430 Using a struct in Rust is similar to using one in Ruby. The main function in this example is comparable to a main function in many other languages. You can see here how the class method works. In Ruby, the scope resolution operator is '::,' and in Rust, it's optional. You can call 'circle::new(10.0)' which gives you back a circle, and then you can call its diameter method as well. Note that you didn't have to declare any type annotations here because Rust has an excellent type inference system.
00:11:40.000 The key thing about methods in Rust is that the compiler knows exactly what implementations you're talking about. Just like with the C compiler, when you have a function with a specific name, the compiler knows which function you're calling. The same applies in Rust; even if you're using methods, the compiler knows exactly what function you're calling, and it statically dispatches the call. There’s a significant difference between static and dynamic dispatch when it comes to performance. You may initially think that the differences are not considerable, but static dispatch allows for function inlining.
00:12:30.120 Function inlining means that if you know exactly which function you're calling at compile time, not only can you make calling the function fast, but you can actually place the function inline where you’re calling it. This is the first optimization you need for all other optimizations. Without knowing what function you're calling at compile time, you can't do other optimizations. In Rust, the fact that it knows what you’re calling means that when you have a loop with a closure, as Godfrey mentioned, Rust can understand what's happening and optimize it accordingly.
00:13:36.020 Generally, I refer to this as a gatekeeper optimization because it unlocks all the other optimizations that you can perform. Most methods in Rust are statically dispatched. A JavaScript talk mentioned earlier talked about optimizations you could implement in Ruby, and many of them require knowing where you're calling something eventually. Besides knowing what function you're calling, another aspect influencing performance is where you put the values you're working with. Normally, values are stored on the heap in Ruby.
00:14:56.720 In C, you do something slightly differently, which is scary for many reasons. First, when you allocate something on the heap and begin giving out references to it, you're responsible for freeing that memory, but you can't do so if anyone else is still pointing to it. This creates a potentially crashing situation in your code. On the other hand, stack allocation has a nicer syntax. When you run a program, the compiler allocates space for the items it can see, and when you exit the function, you can simply pop that stack frame, and there's no need for explicit freeing.
00:15:29.990 With stack allocation, the stack pointer automatically handles freeing memory, which is an advantage. In Ruby, however, values are heap-allocated, and you can’t rely on guaranteed stack allocation in garbage collected languages. This means that small changes in your program might cause previously stack-allocated values to become heap-allocated, which can introduce significant downsides—like needing to track memory allocation and avoid dangling pointers. So developers often prefer heap allocation despite its overhead.
00:16:55.960 In Matt's keynote, he discussed the possibility of using an ownership model to help eliminate the Global Interpreter Lock (GIL) in Ruby. The exciting part about ownership models is that they can make stack allocation safe. With the typical case of ownership, if you allocate something, you can either have multiple read-only references or exclusive access. In Rust, the ownership model guarantees that if you give someone access to an object, either you can’t access it again, or they must give it back, ensuring safety and the elimination of data races.
00:18:05.529 However, there's no equivalent in Ruby's C API to ensure exclusive access to an object. As a result, there might not be an optimal alignment with the ownership model, making Rust interop a bit tricky. The ownership topic is complex and merits discussions, much like JavaScript's approaches to threading and memory management. It would be great if the Ruby VM supported first-class ways of managing ownership that would allow us to fully leverage Rust’s capabilities and improve the overall efficiency of Ruby applications.
00:19:23.760 In closing, I'd like to go back to the initial idea of performance and how using Rust and optimizing Ruby can lead to better overall experiences. Given my background in both languages, I think there are many ways to strike a balance between safety, speed, and developer experience. Using Rust's sound principles in Ruby could guide future developments and optimizations. It's an exciting time for both Ruby and Rust communities to explore collaborations further and unlock new potentials.