Ruby
Turbo Rails with Rust
Summarized using AI

Turbo Rails with Rust

by Godfrey Chan

In the talk titled 'Turbo Rails with Rust' given by Godfrey Chan at RailsConf 2016, the speaker explores how incorporating Rust can enhance the performance of Ruby on Rails applications. While Ruby is appreciated for its usability and rich ecosystem, it is known to have performance limitations, especially in resource-intensive scenarios. Chan addresses building native Ruby extensions in Rust as a solution to optimize Rails performance, highlighting key features that Rust offers over traditional C programming.

Key Points Discussed:

  • Performance Limitations of Ruby: Chan acknowledges that Ruby is slower compared to lower-level languages like C, but it provides significant benefits in developer experience and community support.

  • Native Extensions: The concept of native extensions is introduced, using the example of the 'json' gem, which uses both pure Ruby and C implementations to provide speed while maintaining a user-friendly API.

  • FastBlank Example: Chan discusses how the FastBlank gem reimplemented the string.blank? method in C, achieving significant performance improvements—demonstrating that developer experience can be preserved while enhancing performance through native extensions.

  • Rust Advantages: Chan presents Rust as a safer, modern alternative to C, discussing its memory safety without a garbage collector, compile-time error checks, and zero-cost abstractions. This reduces the risks associated with native extensions while ultimately achieving high performance.

  • Helix Project: Chan introduces a project called Helix, aimed at reducing boilerplate code needed for writing Rust extensions, making it easier for Ruby developers to use Rust without extensive knowledge of the language.

  • Case Study – Meal Matching Algorithm: The talk showcases a real-world use case from a catering company facing performance issues with a Ruby implementation of a meal matching algorithm. By re-implementing the algorithm in Rust, substantial performance improvements were observed, highlighting the practical benefits of using Rust for intensive computations.

Conclusions and Takeaways:
- Rust enables Rails developers to optimize performance-critical components without leaving the Ruby ecosystem, integrating seamlessly with existing Rails applications.
- The safety and performance of Rust presents an appealing option for Ruby developers, particularly those facing computationally heavy tasks, while leveraging the community support from both languages.
- The Helix project further aims to facilitate this integration, ensuring developers can continue to focus on Ruby while tapping into Rust's capabilities as needed.

00:00:11.240 Hello! Should we get started? Are we ready for this? Okay, let's do it.
00:00:16.480 Um, welcome to Kansas. I have always been very excited about this RailsConf because I always wanted to come to Kansas.
00:00:23.599 I've always heard about this movie called 'Wizard of Oz,' and as you can tell, it's a little bit ahead of my time. But I watched it before I came here in preparation for the conference.
00:00:31.359 But as we learned in Jeremy's keynote the other day, it turns out these lines on the maps are state lines. Kansas is on the left, and the other half is Missouri.
00:00:38.200 And Kansas City is actually not in Kansas, so I guess we're not in Kansas anymore. Thank you for that.
00:00:45.000 I had to redo all my slides to add colors to them, so if they don't look very good, that's probably why. I am Godfrey. You can find me on the internet as SheninCode, and I'm very excited to welcome you to your new employee orientation.
00:01:09.479 I hope you're at the right place. If you're looking for RailsCon, I'm afraid that you've moved... just kidding, this is RailsConf.
00:01:15.400 Welcome to RailsConf, and thank you for coming. I always say there is a very personal connection for me regarding RailsConf, and I love coming back.
00:01:31.040 This is actually my fifth RailsConf. Five years ago in Austin was my first RailsConf that I attended on a student scholarship. So if you were there, thank you for chipping in for my ticket.
00:01:53.240 Thank you for giving me the opportunity to be part of this community. The next year, I went back to RailsConf Portland, and for some reason, I suddenly had the courage to go up and say, 'Hey, I have a pull request. Can you merge it?' And for some reason, they ended up merging it.
00:02:13.080 So, I had my first commit in Rails, and soon after, at the next RailsConf in Chicago, I joined the Rails core team. The following year, I had the pleasure to speak at RailsConf for the first time, which was last year.
00:02:30.879 This year, I am very honored to be part of the program committee for RailsConf and helped create the 'Behind the Magic' and 'New in Rails 5' tracks.
00:02:49.319 So, if you went to those talks and liked those tracks, well, that's where they came from. And, as Jeremy said the other day, Rails exists because of its community, so thank you for being part of this community.
00:03:08.560 Following Aaron's lead, I would like to announce some new Rails 5 features. As you learned today, Rails 5 will come with PHP support, and I would like to announce JavaScript support for Rails 5.
00:03:27.200 In fact, you don't even have to wait for Rails 5; you can get it today by running `gem install javascripts`. Once you have installed the gem, all you need to do is require JavaScript at the top of your Ruby file.
00:03:44.400 You can wrap anything in a JavaScript block and write your JavaScript code there. It even supports things like functions, which is very handy because what is everyone's favorite JavaScript feature? Of course, that's callbacks. And what's everyone's favorite Rails feature? Of course, that's also callbacks.
00:04:25.880 The JavaScript gem lets you combine the best of both worlds. For example, here's the Active Record model; you can have your favorite `before_create` callback and write that in JavaScript. Just like the Ruby gem, this is a real thing you can use.
00:04:34.360 It requires an insane amount of engineering, but if you want to learn more about that, I suggest you watch my talk at Guko called 'Dropping Down C', which you can find on YouTube.
00:05:04.800 I have another thing to plug: if you went to the lightning talk from yesterday, you would already know this, but I helped coordinate a newsletter called 'This Week in Rails', where you can find the latest commits, pull requests, and other updates from Rails each week.
00:05:36.960 Here is a sample from last week, including sensational headlines like 'Local Scientists Discover New Method to Manipulate Time' and 'Faster Code Found to Perform Better on Load'. If you haven't already, go to bitly.com/railsweekly to subscribe to this newsletter. The next issue will be coming out in a few hours, and you'll probably learn something new.
00:06:02.560 Speaking of newsletters, I'm also part of another newsletter, which is a product newsletter. If you are not already signed up for Skylight, you should do so by visiting skylight.io and selecting the 'Almost Daily Insider' email preference.
00:06:28.680 We usually write about our experiences writing and building Skylight, often discussing problems we've encountered and solutions offered by our customers. So, if you're interested, you should go sign up, and if you have pen and paper, you might want to write down that secret URL that is actually my personal referral link.
00:06:50.320 I heard we have plans to distribute bonuses this year based on referral credits in our account, so please help me out. Anyway, let's talk about Ruby. Ruby is great. We all love Ruby, and that's why we're here.
00:07:12.240 Ruby has many nice features; its metaprogramming capabilities are pretty awesome. However, there's a problem with Ruby—it's pretty slow. Most of the time, this doesn't really matter, but occasionally you might hit a wall when trying to accomplish something in Ruby, and it's just too slow for your use case.
00:07:35.840 On the other end of the spectrum is C, which is a low-level, super-fast language—it's as close to the metal as you can get without writing assembly. That speed is great, but it's also dangerous. You can easily write code that crashes your program at runtime in an unexpected way.
00:07:50.840 There are a lot of concepts that are a little bit hard to grasp. In Ruby, we have a feature called native extensions that give you the best of both worlds. For example, when you run `gem install json`, what you're getting is actually two different things for the price of one.
00:08:14.440 By default, you will get something called 'json pure', which is a pure Ruby implementation of the JSON encoder. But if you're on a supported platform, you'll also get a thing called 'json_ext', which is a native extension.
00:08:29.640 That's the same JSON encoder API written in C, and it's super fast. As a user, you don't even notice the difference; you just call the regular Ruby class and method, and under the hood, it transparently calls a C method to do the work for you.
00:08:56.240 Chances are you're already using the native version without knowing it. So, native extensions are great, but why don't we write more of them? Well, there is a catch.
00:09:05.760 While it's indeed the best of both worlds, it's only the best of both worlds if you're the end user using the gem. This is fine. David, who created Rails, once said something like this to me: 'We will jump through whatever hoops on the implementation side to make the user-facing API nicer.'
00:09:30.640 I think I would personally extrapolate that user-facing API to developer experience in general, and I think that's a good goal to have: make the experience for your developer users as nice as possible—with a beautiful interface on the outside and whatever you need on the inside to make it work.
00:09:50.360 A good example of this is Sam Saffron, who you might have heard of. He did a lot of Ruby performance work and was a Ruby Hero from last year. He noticed something in Rails, particularly in ActiveSupport. It turns out that there is a method called `string.blank?`, which is called a lot both inside the Rails framework and also in user code.
00:10:15.040 This is also the implementation of the flip side called `present?`. You might have seen things like `User.present?` or `params[:user].present?`. This is actually the method we're talking about. As of a few weeks ago, the implementation in ActiveSupport looked like this.
00:10:35.200 It's pretty short—it basically reopens the string class and checks if the string consists of all whitespace characters. It's a one-liner that reads beautifully in your Ruby code, and it's evidently a very useful method because we use it enough in our application and Rails to make it a performance hotspot.
00:10:56.440 According to Sam, he made a gem called 'FastBlank' that reimplemented the same blank method in C. This C implementation is up to 20 times faster than the Ruby version we saw on the other slide.
00:11:17.080 In some applications, you can achieve up to 5% performance improvement on average. As I said, there have been some recent improvements to the Rails version, but for the purpose of this presentation, that doesn't matter much because the optimization in Rails is about the edge cases or common cases when the string is empty.
00:11:37.119 But as a user, you don't really need to know the difference. You just get the FastBlank gem in your app, and all of your code works seamlessly because it provides the same `string.blank?` method, just with a different implementation under the hood.
00:12:10.560 You can get the performance you want and the user experience you want. So, that seems great—are we done here? Well, there is a problem, though: the problem is me. As a developer, I know just enough C to be dangerous. If you give me a C program with a variable called PTR, the first thing I will try is probably to add a star to it, and if that doesn't work, I will try adding more stars.
00:12:30.640 If that still doesn't fix it, I will try the ampersand. The problem is, as I mentioned before, if you're like me and your C code looks like this, your program can crash at runtime. That's a significant risk because if you're embedding your C code inside a Ruby process, it will crash the entire Ruby process.
00:13:02.760 When this happens, there's not a regular exception that you can handle. At Scala, we have a similar problem—by the way, this is where I work. We are a performance analyzer for Rails apps.
00:13:14.640 To do that, we have to have an agent that we put inside your app to collect performance data in production, and we want to make sure that our agent is as lightweight as possible. We don't want the thing that's supposed to measure your performance to become a bottleneck.
00:13:36.560 So, we could write that agent in C or C++, and fortunately, most of our engineers are smarter than me; they don’t randomly add stars and ampersands to variables in C. Even then, we don’t feel confident enough that we can write and maintain our own C code that goes inside all of our customers’ apps.
00:13:55.960 There are a lot of native extensions in the Ruby community—like Nokogiri or the JSON gem. In their early days, they all had various runtime issues. Those people are way smarter than me, and if even after careful writing they still occasionally crash, then we definitely won't want to recommend our customers put something like that in their app.
00:14:17.560 So, what is the alternative? At the time when we started this project, Rust announced that they had made very good improvements. We thought, 'Hey, we'll perhaps give that a try.' What makes Rust different from writing your native extension in C or C++? Let's look at the Rust website.
00:14:51.279 Rust is a system programming language that runs blazingly fast, prevents segfaults, and guarantees memory safety. Those are a lot of words that don't mean much yet, but they also have another slogan that puts the same thing in different words: 'Hack Without Fear.'
00:15:07.440 The goal of the Rust project is to make system programming more accessible to more programmers, and it achieves this by having a compiler that can find most of these errors that could cause your program to crash at runtime and flag them at compile time.
00:15:26.640 If you don't satisfy the Rust compiler that your program is sound, it simply won't compile. Therefore, you don’t have a thing to run at runtime, and it cannot crash at runtime.
00:15:44.360 There are features in Rust that make that possible, but I can't go into a lot of details today. This talk isn't about teaching you Rust, but I'll describe them at a very high level so you can understand them without seeing actual code.
00:16:10.560 First, Rust manages the safety of your memory without using a garbage collector. Ruby also offers memory safety guarantees. In Ruby, you cannot write code that accesses random locations of memory, causing your program to crash at runtime.
00:16:34.320 This may sound crazy, but your Ruby program cannot crash at runtime if you write it correctly. The difference is that Ruby manages this using a garbage collector, while Rust tracks the lifetime of your variables at compile time.
00:16:50.400 This means Rust knows when and where to locate things and when it needs to clean them up, so it doesn't have to pause the program to clean up with a garbage collector periodically.
00:17:09.120 It also allows you to do concurrency without data races or race conditions, but I don't have time to get into that right now. The final feature that's particularly relevant to us is that it has this concept of zero-cost abstractions.
00:17:23.840 In Ruby, or in most other languages, you always have to make a trade-off between abstracting your code and performance. You might notice that you're repeating steps and want to extract that into a method. That's fine, but it incurs the cost of an extra method invocation.
00:17:39.440 When you're discussing really performance-sensitive code, you have to choose how much to abstract your code versus how performant you want your code to be. In Rails, for example, we have a lot of modules, and we cost super often and that's how we chose to abstract our code ideally.
00:18:05.040 But there might be cases where we realize this is a really hot path, and we prefer not to incur that cost. In Rust, this is one of the biggest features; you don't have to make that trade-off because the Rust compiler is smart enough to notice that this method can be inlined into another.
00:18:22.960 In fact, often if you use higher-level constructs in Rust like iterators, you're giving the compiler more information about how it can optimize your code.
00:18:38.200 So it actually makes your program run faster. For example, in Ruby, you might want to write things in an `each` loop, and we often do, and that's fine, but that incurs the extra cost of calling the `each` method.
00:18:55.440 On the other hand, in Rust, if you use an iterator, it actually makes it faster than writing a hand-rolled loop because the compiler knows that this is going to be a safe iteration, so it can remove some of the bounds checks for each iteration.
00:19:09.560 I can't get into a lot more details, but Yahuda gave a talk on Rust at RailsConf last year. If you are new to Rust and are curious about why you might want to look into this language, you can look up his talk from last year.
00:19:31.440 Now, let's get back to FastBlank. I guess we'll look at the FastBlank implementation quickly. This is the FastBlank body; you probably cannot read it, but it's fine—we'll walk through it step by step.
00:19:57.320 At the top, you have the method signature and some boilerplate to extract some pointers. If the string is empty, then return right away so you can avoid doing a bunch of extra work. The main part of the method loops through all the characters inside the string.
00:20:21.760 If you encounter a whitespace character, you keep looping, and if you encounter a non-whitespace character, you know that this string is not blank and can return false immediately. If you get to the end of the loop, you know that all characters are whitespace.
00:20:37.640 This probably looks a little bit scarier than it is, only because it's on a slide, but this is about 50 lines of code, and it’s not particularly difficult to reason about. So if we can get up to 20 times faster performance writing 50 lines of C code, it seems worth it.
00:21:01.760 Next, let's look at the equivalent FastBlank implementation in Rust. Here it is—this is literally a one-liner function in Rust that does exactly the same thing and handles all the unique edge cases correctly.
00:21:22.040 Let’s walk through it—basically, you define an ‘extern C’ function; this tells Rust, 'Please, I know this code is going to be called from a C program, so keep the C function calling convention when compiling my program.'
00:21:37.800 That part is not particularly important, except to illustrate that Rust is a pretty low-level system programming language designed to interact with other C programs.
00:21:53.360 The Ruby implementation you're probably using, MRI or C implementation, is a C program, so they work nicely together. The Rust compiler cares a lot about safety, so you have to do a bit of extra work to annotate your code and give it information it needs.
00:22:10.320 Here, we’re telling the compiler what the type of the input to this function will be. The specific type we use here isn't particularly important, but you need to tell the compiler which type each variable is going to be.
00:22:28.520 That also helps the Rust compiler figure out how to allocate memory since Rust tries to allocate things on the stack to make cleaning up faster, which is another key to Rust's performance.
00:22:45.280 Here, we’re annotating that this method will return a Boolean value because the `blank?` method is expected to return either true or false, depending on whether the string is blank.
00:23:04.080 As for the body of the method, it resembles the Ruby code you're used to writing. Here we get all the characters from the string as an iterator and use high-level combinators like `all`, which is equivalent to the `array.all?` method.
00:23:20.720 Inside, you're checking whether each character is a whitespace character. The Rust library has a method for that; it knows how to perform the necessary checks correctly.
00:23:34.680 This ultimately illustrates how Rust can accomplish the same tasks with fewer lines of code compared to C. It's quite surprising how much the code has similar readability to Ruby.
00:23:50.440 Given Rust's high-level features, is this going to sacrifice performance compared to the C version? We ran some benchmarks, and although I won't present them in detail, the Rust version is actually slightly faster than the C version.
00:24:10.960 I’m not being scientific here; I'm just trying to illustrate that you can write high-level code without sacrificing performance. It puts your code in the same ballpark as the C equivalent.
00:24:32.840 However, I must mention that there's a catch I haven't told you about. There is a lot of glue code that I didn't show you here. If you look at the full Rust versus C extension, you'll notice that the red part is the one-liner I showed you, while the C function body is much larger.
00:24:54.520 However, if you consider all the boilerplate code around it, in terms of line count, they're roughly the same size. Not all is lost! I should point out that on the Rust side, a lot of this is common shared abstractions, while on the C side, that's literally your business logic.
00:25:14.720 So, you must write that amount of code every time you create an extension like this. In Rust, the oneliner is the only thing specific to your extension.
00:25:35.120 Yehuda and I are working on a project called Helix, and our goal is to eliminate much of that boilerplate code and leverage Rust features like zero-cost abstractions, allowing you to focus on writing your code without all the boilerplate.
00:26:05.040 Here's the entire FastBlank extension written in Helix. We are using Rust's macro feature, which lets you write things similar to Ruby DSLs. At the top, we import a library and declare our Ruby types.
00:26:23.280 The first thing we do is try to reopen the Ruby string class and add a method called `blank?`. You might find this syntax familiar. Finally, in the code, we have the one-liner we saw earlier, which is all the code you need to write to create a FastBlank extension in Rust.
00:26:44.960 My personal goal going forward is to experiment with implementing some Rails features using Helix. We already have an extensive test suite in Rails, and there are many modular parts like `string.blank?` that can be swapped out for a Rust extension without affecting the user-facing API.
00:27:03.680 This enables us to iterate quickly on the pure Ruby code while taking advantage of the existing Rails test suite to experiment with those lower-level implementations.
00:27:26.160 If we keep running against the Rails test suite, we can be fairly confident that everything is in parity, and it would be an optional feature that you can install, just like FastBlank.
00:27:48.960 If your platform supports it and you trust it, it might offer substantial performance gains. There are many low-hanging fruits in smaller modules, like the ActiveSupport Duration class, which you can optimize.
00:28:06.560 For example, when you call `1.day`, that's the ActiveSupport Duration class that's created. There are likely many smaller pieces that we can experiment with using Helix. Eventually, we can work on bigger components, perhaps even the core routing library in Rust someday.
00:28:28.720 While we’re working on this project, we have a friend who works at Cesti, a catering company in San Francisco. They have a Ruby stack and are trying to deliver organized meals for companies in the Bay Area.
00:29:01.600 They have a meal matching algorithm with constraints, such as allergies that restrict certain ingredients or preferences that require meals to be vegan or gluten-free. The problem boils down to checking if the tags in the meal fully satisfy all requirements in the preferences array.
00:29:27.680 This is the set containment problem. The reason they care is that this algorithm is currently implemented in Ruby and has long execution times—sometimes ranging up to 30 minutes.
00:29:53.040 After measuring performance, they found that a lot of the time is spent in this set containment algorithm. It’s interesting to see what we can achieve by implementing this algorithm in Rust.
00:30:15.440 There’s a known trick for testing set containment. On platforms like Stack Overflow, you might find a solution. In essence, to check whether one array fully contains another, you can perform an intersection between the two.
00:30:41.920 Create a new array that only contains the items found in both original arrays, and then check if that new array equals the preferences array. This is extremely readable and expresses the logic well.
00:31:12.240 They had implemented this method and run it against their existing test suite, confirming that it indeed worked as expected. But we can do better.
00:31:29.280 After thinking very hard, we realized that all the numbers in the arrays are unique and sorted, which allows us to write a more efficient algorithm in pure Ruby that will check these conditions.
00:31:42.880 First, check for edge cases; if either array is empty, you can quickly determine one way or the other. Track the position within the other array while looping through all elements in the longer array, trying to advance the pointer.
00:32:06.760 If you get to the end of the loop and find items in the second array that aren’t present in the original, you’ll know that previous items did not meet the requirements. We benchmarked this and found it runs up to 7 times faster.
00:32:16.400 This algorithm behaves well under most circumstances, achieving about a 2X speedup on average. The fastest scenarios can yield up to 7X better performance.
00:32:36.960 Next, we wanted to see what would happen if we implemented that in Rust. Again, we’re using Helix, with the declarative types macro. We're reopening the array class and defining the 'fully contains' method with type annotations.
00:33:00.120 Here, we have similar edge-case checks and the same boilerplate for tracking the index. Interestingly, in Rust, as I previously mentioned, if you use iterators, the compiler can perform optimizations.
00:33:20.760 Here, we use an iterator instead of writing a hand-rolled loop. Because we're using an iterator, the loop body looks slightly better than the Ruby version in my opinion.
00:33:42.080 After implementing it this way and running benchmarks, we found it performs up to 173 times faster than the Ruby version, depending on the workload. I plotted this data, so you can show the performance differences visually.
00:34:05.680 You may not see all the details, but the performance of the pure Ruby implementation is near the bottom, while the fast algorithm in Ruby and the Rust implementation are performing significantly better.
00:34:31.120 Here is the summary of what we have observed: while the Rust implementation behaves much better, you should see it's usually 10 to 173 times faster than the Ruby implementation on average.
00:34:55.120 With time almost out, I want to present where you can find the code for the Helix project. It's still a work in progress, but we're on the right track.
00:35:13.920 If you're interested in helping out with this project, please come talk to us afterward.
00:35:29.960 Finally, I'll close with this: historically, scripting languages were slow, and they are still relatively slow today.
00:35:45.040 Because of this slowness, they were mainly used as a coordination layer, delegating heavy tasks to system-level languages like C.
00:36:05.240 However, it turns out many operations we are doing are IO-bound, which makes the performance difference less critical than expected.
00:36:24.760 Since web applications are so IO-heavy, this has worked out wonderfully for frameworks like Rails.
00:36:43.840 The ability to run multiple Unicorn processes on a powerful machine serves as good empirical evidence.
00:37:04.960 Despite this, we still encounter computationally heavy operations in our applications. Business logic defines the uniqueness of your application.
00:37:25.680 I think we're entering a new era where we have taken advantages from scripting languages, especially in terms of ergonomics, and moved that into system languages like Rust.
00:37:47.240 Helix allows you to move computationally heavy parts of your applications back to Rust. You don’t need to entirely switch your stack to something like Go to achieve good performance.
00:38:10.720 In my opinion, Rust is particularly suited for this task in Rails teams because of the safety guarantees offered by the Rust compiler.
00:38:30.640 More of your team may be able to tinker and experiment with the Rust code without worrying about causing runtime issues. If it doesn't compile, the worst outcome is just failing to run.
00:38:53.920 In our team, everyone wound up picking up enough Rust to fix bugs or make minor tweaks to our Rust agent. We believe that will continue to be the case.
00:39:15.840 So, the goal of the Helix project is to make this even more accessible to Ruby teams so they can continue writing most of their code in their favorite language without fearing it will be too slow.
00:39:39.040 You can always drop down to Rust if and when you need to. One more time, that's all I have today.
00:39:51.280 You can find me on the internet as SheninCode. Thank you for your time. Let’s make Ruby great again! Thank you.
Explore all talks recorded at RailsConf 2016
+102