3
\$\begingroup\$

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)
\$\endgroup\$
  • \$\begingroup\$ Why map!? Doesn't make sense to me \$\endgroup\$ – Flambino Oct 24 '15 at 19:20
  • \$\begingroup\$ @Flambino, I apologize but I don't understand your question; could you please clarify? Are you concerned by the destruction specifically, or utilization of the map enumerator altogether? \$\endgroup\$ – alexanderjsingleton Oct 24 '15 at 19:40
  • 1
    \$\begingroup\$ Both actually. Why map! instead of plain each, 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 all nil). Caridorc seems to have been wondering the same, judging by the answer - which also provides better alternatives \$\endgroup\$ – Flambino Oct 24 '15 at 20:54
  • \$\begingroup\$ @Flambino, please see my rationale included in my comment on @Caridorc's answer; I guess my interpretation of the FizzBuzz challenge influenced my selection of .map! instead of .each. I've also included a StackOverflow post for any readers prompted to revist enumerables. Thank you. \$\endgroup\$ – alexanderjsingleton Oct 24 '15 at 21:09
1
\$\begingroup\$

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: whens 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".)

\$\endgroup\$
  • 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 Oct 24 '15 at 22:04
  • \$\begingroup\$ Solution above available at the following Gist \$\endgroup\$ – alexanderjsingleton Oct 27 '15 at 15:37
2
\$\begingroup\$

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! with each and leave the rest of the code as is.
  • Replace map! with map, remove puts and handle printing out of the function. Adding a join("\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 {}.

\$\endgroup\$
  • 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 Oct 24 '15 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 Oct 24 '15 at 20:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.