[reproduced] how I let Ruby project speed upgrade 10 times?

Recommended for you: Get network issues from WhatsUp Gold. Not end users.
The author describes in detail how he made a Ruby run time project from 20 seconds to 1.5 seconds and optimization. Worth noting is, call in the Ruby method can affect the speed, so the author is modular and reusable code. The following is the author of the original text translation:

This article mainly introduced how I put Ruby gem contracts.ruby speed upgrade 10 times.

Contracts.ruby in my project to add the code contracts (code contracts) to Ruby. Looks like this:

1
2
3
4
Contract Num, Num => Num
def add(a, b)
  a + b
end
As long as the add method is called, the arguments and the return value will be checked.

20 seconds

This weekend, I tested the library, found the performance is very bad:

                                   user     system      total        real
testing add                      0.510000   0.000000   0.510000 (  0.509791)
testing contracts add           20.630000   0.040000  20.670000 ( 20.726758)
This is under random input, run 1000 times after the result.
So, when given a function into the contract function, running speed decreased significantly (about 40 times that, in this regard, I) was studied.

8 seconds

I have made great progress, when the transfer contract, I call the success_callback function, this function is an empty function, here is the function of the whole definition:

1
2
def self.success_callback(data)
end
The original function calls are very expensive in Ruby, just delete it, saved 8 seconds:

                                  user     system      total        real
testing add                     0.520000   0.000000   0.520000 (  0.517302)
testing contracts add           12.120000   0.010000  12.130000 ( 12.140564)
Delete some other accessory function call, time from the beginning of 9.84-> 9.59-> 8.01 seconds, the base rate immediately raised to two times before.
Now, it becomes a bit complicated.

5.93 seconds



There are many years for the definition of a contract of way: Anonymous (Lambdas), (classes), simple old data (plain ol 'values) etc.. I have a very long case statements, types are used to test the contract. On the basis of this contract type foundation, I can do different things. By putting it into the if statement, I save some time, but each call to this function, I still spend unnecessary time in examining the decision tree.:

1
2
3
4
5
if contract.is_a?(Class)
 
# check arg
elsif contract.is_a?(Hash)
 
# check arg
...
When defining the contract and construction of lambda, only a check on the tree:

1
2
3
4
if contract.is_a?(Class)
  lambda { |arg|
# check arg }
elsif contract.is_a?(Hash)
  lambda { |arg|
# check arg }
Then, I will completely bypass the branch of logic, by passing parameters to the pre computed lambda to verify, thus saving 1.2 seconds.


                                  user     system      total        real
testing add                      0.510000   0.000000   0.510000 (  0.516848)
testing contracts add            6.780000   0.000000   6.780000 (  6.785446)
Pre computing and some other If statements, and save the second time almost:
                                   user     system      total        real
testing add                      0.510000   0.000000   0.510000 (  0.516527)
testing contracts add            5.930000   0.000000   5.930000 (  5.933225)
5.09 seconds
Convert.Zip to.Times and saved me a second time:

                                   user     system      total        real
testing add                      0.510000   0.000000   0.510000 (  0.507554)
testing contracts add            5.090000   0.010000   5.100000 (  5.099530)
Results show that:
1
args.zip(contracts).each do |arg, contract|
The code above than below the slow:
1
args.each_with_index do |arg, i|
Than the more slowly:
1
args.size.times do |i|
.zip to spend unnecessary time to copy and create a new array. And I think, the reason.Each_with_index is slow, because it is subject to the.Each behind, so it involves two restriction rather than a.

4.23 seconds

Below to see a little details, the contracts library at work, it will add class_eval for each method (class_eval faster than define_method) of the new method, the new method has a reference to the old method, when the new method is called, it will examine the parameter, then according to the old method call parameters, and then check the return value, and the return value. All of these will be called Contract class check_args and check_result two method. I cancelled the two method calls, and proper inspection of new methods, results and saved 0.9 seconds.:

                                  user     system        total        real

testing                         0.530000   0.000000   0.530000 (0.523503)
testing contracts add           4.230000   0.000000   4.230000 (  4.244071)
2.94 seconds

In the above, I have already explained how to create a lambda based on the Contract type, and then use these to test parameters. Now, I changed my ways, by generating code to replace, when I use class_eval to create a new method, it will get the results from eval. A terrible vulnerability, but it avoids a lot of method calls, and saved 1.25 seconds.:

                            user    system     total   real
    testing add            0.520000 0.000000 0.520000 ( 0.519425)
    testing contracts add  2.940000 0.000000 2.940000 ( 2.942372)
1.57 seconds

Finally, I changed invokes overridden methods, I was using a reference:

1
2
3
4
5
6
7
8
# 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 was modified, and using the method of alias_method:
1
2
3
4
5
6
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
}}
Surprise, and saved 1.4 seconds. I don't know why aliaa_method would be so fast, my guess is it skipped a method call and binding to.Bindbind.

                                 user     system      total        real
testing add                      0.520000   0.000000   0.520000 (  0.518431)
testing contracts add            1.570000   0.000000   1.570000 (  1.568863)
Result

The success of our time from 20 seconds to 1.5 seconds to optimize, I can't think of anything better results. The test script that I write, a package of the add method is better than that of conventional add method 3 times slower, so these numbers have been good enough.

To verify the above conclusion is simple, the amount of time spent calling method is only 3 times slower, here's a more realistic example: a function that reads a file 100000:

                                  user     system      total        real
testing read                     1.200000   1.330000   2.530000 (  2.521314)
testing contracts read           1.530000   1.370000   2.900000 (  2.903721)
Slightly slower! The add function is an exception, I decided not to use the alias_method method, because it polluted the namespace alias, and these functions will emerge everywhere (automatic document, IDE etc.).

Other reasons:

Call the Ruby method is very slow, I like the code modularity and reuse, but maybe it is time to more code inline.
Test your code! A simple method to delete unused time decreased from 20 seconds to 12 seconds.
Other attempts

The 1 method selector

Ruby 2 is a lack of the characteristic method selector, or you can also write this:

1
2
3
4
5
6
7
8
9
10
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
It may be easier to write decorator, and running speed.
The 2 key old

Another characteristic of Ruby 2 is the lack of reference rewriting method:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 using redef to redefine the way:
Matz said:

In order to eliminate the alias_method_chain, we introduce Module#prepend, prepend in the # number, so that no opportunity to add redundancy in language.
So if redef is redundant features, maybe prepend can be used to write decorator?

4 other

So far, these have been done in the YARV test.

Via adit.io

The original
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download

Posted by Baldwin at November 27, 2013 - 9:27 PM