Optimization can take many different forms, but programmers can be strategic about where and how to optimize during the process of project development. In this talk from RubyConf India 2015, Berlin-based SoundCloud developer Erik Michaels-Ober (Twitter and Github) talks about how to write faster Ruby by boosting performance optimization at the source code level.
Michaels-Ober begins by noting that most developers have a bias against doing performance optimization too early. He references Donald Knuth, a Stanford University professor and the grandfather of algorithm design and analysis, who said in 1974 that “premature optimization was the root of all evil.” Michaels-Ober points out that well-intentioned but ill-timed optimization does have a tendency to make code “uglier, harder to read and more complex,” so it is best to optimize code only when there is a need, or when the time is right.
So when should one do optimization? Here, Michaels-Ober references Knuth again, who set down these two criteria: first, make optimizations when they can be easily obtained; second, make optimizations when there is a significant benefit (Knuth gives a figure of 12 percent as the threshold of significance). Michaels-Ober adds a third criterion, borrowed from Ruby chief designer Matz (Yukihiro Matsumoto): Ruby was created to make programmers happy, so optimization should also be done to make programmers happy — whether it means making code “more beautiful” or perform better.
Levels of Optimization
Michaels-Ober then outlines the various levels of optimization that are possible with any language and project: at the levels of design, source, build, compile, and runtime. Michaels-Ober’s focus here is on source code optimization. Once again, he brings in a bit of wisdom from Knuth, who recognized that a programmer’s human intuition can fail when it comes to anticipating how a computer will actually interpret code, whether it’s optimized or not. “We don’t have very good intuition about what will be fast or slow,” says Michaels-Ober. “We often make mistakes, so if you do optimization too early, you might actually think you are optimizing it, but you might really be writing a less optimal version.”
So how do you know which version is the optimal one? “You have to benchmark it,” says Michaels-Ober. Ruby’s built-in benchmark library allows you to run methods a predetermined number of times. However, Michaels-Ober notes that the problem with the built-in benchmark library is that you have to guess how many times it has to run it to make it significant.
Instead, Michaels-Ober recommends using benchmark-ips (iterations per second), which extends Ruby’s library to show iterations-per-second instead of seconds-per-iteration. When each version of code is run for only five seconds, a report will be generated indicating how many times each version was able to run. “The idea is that five seconds is a long enough time, and it gets rid of any statistical variance; the variance will be relatively low once you run it that many times, so you don’t have to worry about the noise.”
Writing Faster Ruby at the Source Code Level
Michaels-Ober then shows some examples of how to simplify code to make it run faster and easier to read (you can refer to the lecture slides on Speaker Deck).
Block vs. Symbol#to_proc
Symbol#to_proc is 20 times faster compared to block as the optimization is made inside the Ruby interpreter. Symbol#to_proc was initially invented as shorthand within RAILS and added to Ruby later. Ruby knows how to run Symbol#to_proc efficiently and optimizes it internally.
Enumerable#map and Array#flatten vs. Enumerable#flat_map
A new map will return an array of arrays and if you want one array call map and then flatten(1). You can replace map and flatten(1) with a more declarative flat_map and it will work the same way, plus it is 4.5 times faster because it iterates once through the code instead of twice.
Enumerable#reverse and Enumerable#each vs. Enumerable#reverse_each
Reverse_each will not make a copy of the array and will just iterate the array in reverse making it 17 percent faster by just replacing the former with reverse_each.
Hash#keys and Enumerable#each vs. Hash#each_key
Ruby has a method built-in to create an array with only keys of that hash. The quicker method will not create an intermediate array and instead just return the keys, making it 33 percent faster.
Array#shuffle and Array#first vs. Array#sample
When you have an array and you want to pick some random value out of the array, one way to do that is to shuffle the array and to take off the first element using array.shuffle.first — or you can use array.sample, making it 15 times faster.
Hash#merge vs. Hash#merge!
The mutable version that modifies the hash versus the immutable version that creates a copy of that hash and then does a merge and copy. Because you are modifying the hash within a tighter scope, it is 3 times faster. The same concept applies with hash#, with speed compounded twice over.
Hash#fetch vs. Hash#fetch with block
Fetch taking a second argument for a block is two times faster than directly passing the block’s result.
String#gsub vs. String#sub
The gsub will globally substitute every instance in the first string with the second string. If you want to make just one substitution and not scan for additional instances then use sub which is 50% faster.
String#gsub vs. String#tr
You can always replace gsub with tr but in cases where you want to replace something everywhere in the code, use tr which is 5 times faster.
Parallel vs. sequential assignment
Parallel assignment works by allocating the array but it makes code harder to read. Parallel assignment is useful when swapping values, otherwise, use sequential assignment which is faster to read and is 40 percent faster.
Using throw/catch for control flow is useful to jump around the code when compared to using exceptions, making it five times faster.
Though other optimizations can be done at the next level of the compiler, these source code optimization techniques mentioned above can be implemented painlessly and quickly today, says Michaels-Ober, making your Ruby code faster, perform better and be more beautiful to read.
Feature image via Flickr Creative Commons.