Example of how to optimize Ruby code to speed up the program running, ruby speed
This article mainly introduces how to increase ruby gem contracts. ruby speed by 10 times.
Contracts. ruby is used in my project to add code contracts to Ruby. It looks like this:
Contract Num, Num => Num
def add(a, b)
a + b
end
As long as the add method is called, parameters and return values are checked.
20 seconds
This weekend, I tested the database and found that its performance was very bad:
This is the result after running 1000 times under random input.
Therefore, when a function is added with the contract function, the running speed is significantly reduced (about 40 times). I have studied this in depth.
8 seconds
I have made great progress. When passing the contract, I call the success_callback function, which is an empty function. The following is the entire definition of this function:
def self.success_callback(data)end
Previously, function calls were very expensive in Ruby. Only deleting this call saves 8 seconds:
The call time for deleting some other attachment functions starts from 9.84-> 9.59-> 8.01 seconds, and the database speed is immediately doubled.
Now, things are a little complicated.
5.93 seconds
There are many ways to define a contract: anonymous (lambdas), Class (classes), simple old data (plain ol 'values), and so on. I have a very long case statement to check the contract type. Based on the contract type, I can do different things. By changing it to the if statement, I save some time, but every time I call this function, I still spend unnecessary time checking the decision tree carefully:
if contract.is_a?(Class)
# check arg
elsif contract.is_a?(Hash)
# check arg
...
When defining a contract and building lambda, only one check is performed on the tree:
if contract.is_a?(Class)
lambda { |arg| # check arg }
elsif contract.is_a?(Hash)
lambda { |arg| # check arg }
Then, I will completely bypass the logic branch and pass the parameter to the pre-calculated lambda for verification, thus saving 1.2 seconds.
Pre-Calculation of some other If Statements saves about 1 second:
5.09 seconds
Converting .zip to. times saves me one second:
Proof:
args.zip(contracts).each do |arg, contract|
The above code is slower than the following one:
args.each_with_index do |arg, i|
It is slower than the following one:
args.size.times do |i|
. Zip takes unnecessary time to copy and create an array. In my opinion, the reason why. each_with_index is slow is that it is subject to. each, so it involves two restrictions, not one.
4.23 seconds
Next, let's look at the details. When the contracts Library is working, it will add a new class_eval (class_eval is faster than define_method) method for each method, this new method has a reference to the old method. When a new method is called, it checks the parameters, calls the old method based on the parameters, and then checks the return value and returns the value. All of these methods call the check_args and check_result methods of the Contract class. I canceled the call of the two methods and checked the new method correctly. The result saved 0.9 seconds:
2.94 seconds
As mentioned above, I have explained how to create lambda based on the Contract type and then use these to test the parameters. Now I have changed the method and replaced it with the generated code. When I use class_eval to create a new method, it will get the result from eval. A terrible vulnerability, but it avoids a lot of method calls and saves 1.25 seconds:
1.57 seconds
Finally, I changed the method of calling the rewrite method. I used to use references:
# simplification
old_method = method(name)= method(name)
class_eval %{%{
def #{name}(*args)def #{name}(*args)
old_method.bind(self).call(*args).bind(self).call(*args)
endend
}}
I modified it and used the alias_method method:
alias_method :"original_#{name}", name:"original_#{name}", name
class_eval %{%{
def #{name}(*args)def #{name}(*args)
self.send(:"original_#{name}", *args)self.send(:"original_#{name}", *args)
endend
}}
It saved another 1.4 seconds. I don't know why aliaa_method is so fast. I guess it's because it skips a method call and binds it to. bindbind.
Result
We successfully optimized the time from 20 seconds to 1.5 seconds. I don't think there is any better result. The test script I wrote indicates that a encapsulated add method is three times slower than the conventional add method, so these numbers are enough.
To verify the above conclusion, it is very simple. The reason why a lot of time is spent on calling methods is only three times slower. Here is a more realistic example: a function reads a file 100000 times:
A little slow! The add function is an exception. I decided not to use the alias_method method, because it contaminated the namespace and these alias functions will appear everywhere (documents, IDE Automatic completion, etc ).
Other reasons:
Calling methods in Ruby is slow. I like to modularize and reuse the code, but it may be time to inline more code.
Test your code! The time for deleting a simple unused method is reduced from 20 seconds to 12 seconds.
Other attempts
1. Method Selector
The method selector feature is missing in Ruby 2.0, Or you can write it like this:
class Foo Foo
def bar:beforedef bar:before
# will always run before bar, when bar is called# will always run before bar, when bar is called
endend
def bar:afterdef bar:after
# will always run after bar, when bar is called# will always run after bar, when bar is called
# may or may not be able to access and/or change bar's return value# may or may not be able to access and/or change bar's return value
endend
endend
This may make it easier to write decorator and speed up the operation.
2. Keyword old
Another feature lacking in Ruby 2.0 is to reference the rewrite method:
class Foo Foo
def bardef bar
'Hello''Hello'
endend
end end
class Fooclass Foo
def bardef bar
old + ' World'+ ' World'
endend
endend
Foo. new. bar # => 'Hello world' Foo. new. bar # => 'Hello world'
3. Use redef to redefine the method:
Matz once said:
To eliminate alias_method_chain, we have introduced Module # prepend and prepend with the # sign before it, so we have no chance to add the redundancy feature to the language.
So if redef is redundant, maybe prepend can be used to write decorator?
4. Other implementations
So far, these have been tested in YARV.