I would appreciate any feedback concerning my analysis of the most efficient FizzBuzz solution programmed in Ruby. I submit that a Case-statement solution is more efficient than utilizing a ConditionalIf-statement in my recent blog-post within my conclusion provided thusly:
Consequently, the predictability pattern is the reason why a "Case" statement, or branchIf, is optimal and less expensive than a "Conditional If" statement, as clarified by Igor Ostrovsky's Blog Post: Fast and Slow If-Statements: Branch Prediction in Modern Processors
"If the condition is always true or always false, the branch prediction logic in the processor will pick up the pattern. On the other hand, if the pattern is unpredictable, the if-statement will be much more expensive."
Back to my optimized FizzBuzz solution- when the "Case" statement processes a number initializing the method, the constraint or case stops calculating when the condition is satisfied, and it will not continue to verify the constraint by branching unless divisible as it were for an IF/ELSIF construction, which saves time, performing faster, as the best solution possible, ultimately proving Danielle Sucher's point:
"I'd expect if/elsif to be faster in situations where one of the first few possibilities is a match, and for case to be faster in situations where a match is found only way further down the list (when if/elsif would have to make more jumps along the way on account of all those branchUnlesses)."
Furthermore, a real programmer can impress an interviewer by asking if the range invoked will be consecutive or random. Although a "Case" statement solution for FizzBuzz is generally more efficient, it will certainly be faster for a random range of numbers called.
I've approached several academics and professionals alike but nobody has challenged, so I would like to broaden the forum for additional contribution:
require 'benchmark'
def fizzbuzz(array)
array.map!{ |number|
divisibleBy3 = (number % 3 == 0)
divisibleBy5 = (number % 5 == 0)
case
when divisibleBy3 && divisibleBy5
puts "FizzBuzz"
when divisibleBy3
puts "Fizz"
when divisibleBy5
puts "Buzz"
else
puts number
end
}
end
puts Benchmark.measure{fizzbuzz(Array(1..10000))}
puts "fizzbuzz"
# puts RubyVM::InstructionSequence.disasm(method(:fizzbuzz))
def super_fizzbuzz(array)
array.map! { |number|
if(number % 3 == 0 && number % 5 == 0)
puts "FizzBuzz"
elsif(number % 3 == 0)
puts "Fizz"
elsif (number % 5 == 0)
puts "Buzz"
else
puts number
end
}
end
puts Benchmark.measure{super_fizzbuzz(Array(1..10000))}
puts "super_fizzbuzz"
# puts RubyVM::InstructionSequence.disasm(method(:super_fizzbuzz))
# Additional Documentation
# https://en.wikipedia.org/wiki/Assembly_language
# underTheHood => http://ruby-doc.org/core-2.0.0/RubyVM/InstructionSequence.html
# http://ruby-doc.org/stdlib-2.0.0/libdoc/benchmark/rdoc/Benchmark.html
Upon executing the above, terminal-output correspondingly revealed the following when scraped with grep
:
Desktop ruby fizzbuzz.rb | grep 0.0 0.010000 0.000000 0.010000 ( 0.010018) 0.010000 0.000000 0.010000 ( 0.011353)
2 Answers 2
The test setup seems unfair. Your case
implementation precomputes the divisibleBy3
and divisibleBy5
booleans, while the if..elsif
implementation does the modulo operation(s) and comparison(s) for each attempted branch. I haven't checked, but putting the implementations on equal footing might reduce them to identical instruction sets, since they really become functionally and structurally equivalent.
But I can't really speak to performance with much confidence. I get practically identical performance for 10 million numbers*. So this is really deep in "why bother?" territory, if you ask me.
I don't mean to be harsh; it's an interesting idea to ponder. And I can see that the case can be made for one version being theoretically more efficient than the other at a very, very low level. But Ruby is very, very high level. In practical terms, I highly, highly doubt it's something you'll ever actually need for anything - at least while coding Ruby. Again, I can appreciate the thought that went into it, but the phrase "overthinking things" also comes to mind.
My tests used plain Ruby by the way; have you tested on JRuby, which uses the JVM under the hood? No idea if it'll make a difference either way, but the point is that Ruby is so far removed from the metal of the CPU that lots of things might impact performance.
In short: If your main concern is the rawest of raw performance optimization, don't use Ruby to begin with.
Other than that, Caridorc's answer covers my main concern about this code (the use of map!
, and {..}
instead of do..end
for multiline blocks).
I'd add that indentation is inconsistent: when
s should be indented the same as their case
, not more, and general indentation should be 2 spaces.
*) I changed the puts
calls to call an empty method since the point here is testing the branching performance, not stream IO. I also precomputed the array of numbers so it wouldn't factor into the benchmarking, and used each
instead of mapping. For style, I fixed indentation and removed trailing whitespace. Code below.
require 'benchmark'
N = Array(1..10_000_000)
def no_op(*a); end
def fizzbuzz(array)
array.each do |number|
divisibleBy3 = (number % 3 == 0)
divisibleBy5 = (number % 5 == 0)
case
when divisibleBy3 && divisibleBy5
no_op "FizzBuzz"
when divisibleBy3
no_op "Fizz"
when divisibleBy5
no_op "Buzz"
else
no_op number
end
end
end
puts Benchmark.measure{fizzbuzz(N)}
puts "fizzbuzz"
def super_fizzbuzz(array)
array.each do |number|
divisibleBy3 = (number % 3 == 0)
divisibleBy5 = (number % 5 == 0)
if divisibleBy3 && divisibleBy5
no_op "FizzBuzz"
elsif divisibleBy3
no_op "Fizz"
elsif divisibleBy5
no_op "Buzz"
else
no_op number
end
end
end
puts Benchmark.measure{super_fizzbuzz(N)}
puts "super_fizzbuzz"
Example result:
3.370000 0.000000 3.370000 ( 3.386060) fizzbuzz 3.350000 0.010000 3.360000 ( 3.375098) super_fizzbuzz
(Yes, the if..elsif
implementation was actually faster on this run. I had another run where it ended up 0.2 seconds faster, and others where it lost out to the case
implementation. I'd average that out to "no appreciable difference whatsoever".)
-
1\$\begingroup\$ Thank you @flambino! Addressing a few of your questions: 1.) Though mundane, most shared the same curiosity about the most efficient Ruby solution re: speed/efficiency but never thought about it until I asked. 2.) I'm unfamiliar with JRuby/JVM but I'll definitely explore. On other points, thank you for the styling recos; I should've known better! Lastly, yes, I clinched my blog-post thesis with the final point "Although a "Case" statement solution for FizzBuzz is generally more efficient, it will certainly be faster for a random range of numbers called." Thanks again for your guidance! \$\endgroup\$alexanderjsingleton– alexanderjsingleton2015年10月24日 22:04:06 +00:00Commented Oct 24, 2015 at 22:04
-
\$\begingroup\$ Solution above available at the following Gist \$\endgroup\$alexanderjsingleton– alexanderjsingleton2015年10月27日 15:37:19 +00:00Commented Oct 27, 2015 at 15:37
Using a destructive update like map!
is weird, the user does not expect his array to be mutated. I suggest one of two possibilities:
- Replace
map!
witheach
and leave the rest of the code as is. - Replace
map!
withmap
, removeputs
and handle printing out of the function. Adding ajoin("\n")
may be needed.
The first is more obvious, the second one only has one responsability.
As a minor note, it is common style to use do end
for multiline blocks, not {}
.
-
1\$\begingroup\$ According to my rationale, and in my experience, destruction is generally appropriate as the FizzBuzz challenge implicitly suggests replacing the invoked elements corresponding to the condition with strings "FizzBuzz", "Fizz", "Buzz", necessitating mutation. However, your suggestion is intriguing- the modification accelerates the calculation; I appreciate the new perspective. Thank you @Caridorc \$\endgroup\$alexanderjsingleton– alexanderjsingleton2015年10月24日 20:49:26 +00:00Commented Oct 24, 2015 at 20:49
-
3\$\begingroup\$ @alexanderjsingleton Ruby favours Functional Programming, anyhow one thing is destructevly updating an array with sensible results, another is filling it with nil how you do. (puts returns nil) \$\endgroup\$Caridorc– Caridorc2015年10月24日 20:53:11 +00:00Commented Oct 24, 2015 at 20:53
map!
? Doesn't make sense to me \$\endgroup\$map!
instead of plaineach
, when a)map!
has side-effects, and b) you're not actually mapping anything (you don't need to, you don't want to, and the result is allnil
). Caridorc seems to have been wondering the same, judging by the answer - which also provides better alternatives \$\endgroup\$.map!
instead of.each
. I've also included a StackOverflow post for any readers prompted to revist enumerables. Thank you. \$\endgroup\$