rubocop-performance
rubocop-performance copied to clipboard
RedundantBlockCall ignores asymptotic complexity
The asymptotic complexity of yield
and call
is equal (depending on size of the executed code or how often it's executed). Actually call
just adds a constant value to the needed time.
So even when you run call
in a loop the performance of the actual yield
/call
is never worse than by a constant factor. I did some tests and always got just a constant ~30% performance drop for the actual yield
/block
call (nothing like an exponential or higher decrease in performance appeared).
Example: Your software needs overall 10 seconds, every call
wastes 0.000001 or 10^-6 seconds (compared to yield
) and call
comes up 1000 times. Then the software will need just 0.01 seconds more compared to yield
.
Even worse, RedundantBlockCall also fails when there's just a constant value added to the whole method.
I did some performance tests for such a scenario and could get absolutely no difference between yield
and call
. (the difference was always +/- 1 %)
def fun_yield
yield
end
fun_yield { 1000000000.times { print('') } }
def fun_call(&block)
block.call
end
fun_call { 1000000000.times { print('') } }
Thanks @md-work. How does it look for older Ruby versions? If this applies to all Ruby versions, arguably this cop should be removed. If not, it should probably not run for newer rubies.
I tested it with all currently supported Ruby versions (2.3, 2.4 and 2.5) and the results are equal.
require 'benchmark'
def i_will_yield
yield
end
yield_result = Benchmark.realtime do
1_000_000.times do
i_will_yield { 1 + 1 }
end
end
def i_will_call(&b)
b.call
end
call_result = Benchmark.realtime do
1_000_000.times do
i_will_call { 1 + 1 }
end
end
puts "call: %.9f" % call_result
puts "yield: %.9f" % yield_result
# $ ruby call_vs_yield.rb
# call: 0.419539942
# yield: 0.076669322
Your argument is valid. Using yield
over call
does not influence the asymptotic complexity. However, using yield
over call
is five times faster. Especially when calling multiple times, this can make a huge performance difference. Therefore this gem makes sense and should not be removed.
Also rubocop.readthedocs.io referes to a benchmark. [1]
[1] - https://github.com/JuanitoFatas/fast-ruby#proccall-and-block-arguments-vs-yieldcode
@ellcs
*5 (five times) might sound much. But you've got to see this in the context of a realistic software.
No software is just calling yield/call all the time. It's hard to find a realistic value for this. But I think it's OK to say, that not more than 0.1 % of an application instructions should be yield/call (don’t confuse with the number of instructions inside the given block, which isn't affected here). So if you apply *5 to this 0.1%, the overall runtime just increases from 100% to 100.4%.
The big O notation shows, that performance mostly depends on code with exponential or higher complexity. Focusing on stuff which influences the performance simply by a constant factor will probably just make people sacrifice other things like beauty of code. Just think of method calls. Every time you put code into a separate method, you sacrifice some performance for the method call. But everyone would agree, that this isn't a good reason to put a whole program into one big method.
I just randomly stumbled across this by chance and I thought I would point out that 2.6 will remove almost all of this difference (2.6.0-preview2):
call: 0.083332303
yield: 0.064861668
@enebo Confirming! Just tested it and the difference between yield and call has become almost indistinguishable in ruby-2.6.0-preview2 (tested on openSUSE-15.0 x86_64 Linux). Even when I run @ellcs example, which I still consider a non realistic worst case scenario, I get nearly no difference (pretty much the results @enebo wrote before).
Nevertheless, I'd go even further and consider the cop already deprecated for Ruby 2.3 to 2.5. Because if your software isn't just yield'ing/call'ing tiny blocks all the time, which is clearly unrealistic for me, there's already no difference between yield/call in those Ruby versions.
I modified @ellcs example and put just a little more work into the block than just 1 + 1
(still not very realistic software) and the difference also falls below 1 % for ruby-2.4.
require 'benchmark'
def fibonacci( n )
return n if ( 0..1 ).include?(n)
fibonacci(n - 1) + fibonacci(n - 2)
end
def i_will_yield
yield
end
yield_result = Benchmark.realtime do
1_000_000.times do
i_will_yield { fibonacci(10) }
end
end
def i_will_call(&b)
b.call
end
call_result = Benchmark.realtime do
1_000_000.times do
i_will_call { fibonacci(10) }
end
end
puts "call: %.9f" % call_result
puts "yield: %.9f" % yield_result
# ruby-2.4
# $ ruby call_vs_yield.rb
# call: 18.778579076
# yield: 18.691476580
I guess we can simply convert this into a style cop so that people can use consistently call
or yield
.
I guess we can simply convert this into a style cop so that people can use consistently
call
oryield
.
Is that easily possible?
Any progress on this?