A Contribution to Danielle Sucher’s Ruby Case vs. Conditional-If via FizzBuzz

The natural, intuitive response is to fight for oxygen, to struggle back into what we know, to try and rip the helmet off.

Deep-Dive Duress: The natural, intuitive response is to fight for oxygen, to struggle back into what we know, to try and rip the helmet off- like Ed Harris prepping for liquid-breathing in The Abyss

Although code bootcamps are hailed for a practical education in programming, best practices for long-term efficiency are compromised for rapid proficiency.  Don’t get me wrong, the bootcamp movement should be celebrated for leveling-up the economy and empowering the disenfranchised(let’s hope the urgency for accreditation and federal loans doesn’t inflate bootcamp tuition prices), but University will endure as a place for eager minds who won’t settle for how things work and must grapple with why things work.  I specifically recall respectfully disagreeing with a bootcamp instructor about the appropriate use of  “Conditional If” statements vs. “Case” statements; in fairness, he was only encouraging Agile methodologies for MVP‘s, however, I was right. Evidently, a “Case” statement is most appropriate for solving FizzBuzz (a common interview question asking candidates to reproduce or white-board a function invoking a range of numbers, but for multiples of 3 prints ‘Fizz’ instead of the number, for multiples of 5 prints ‘Buzz’, and numbers which are multiples of both 3 and 5 prints ‘FizzBuzz’), as opposed to a standard “Conditional If” statement, which only a technical deep-dive can revealrelax—this won’t hurt a bit; here is my solution(extended commentary edition for n00bs):

# *NOTE*, case statements are preferred over standard conditionals-this has been confirmed according to my very
# own academic research and engagements with CS professors-BUT-here is an outstanding explanation by [@DanielleSucher](http://www.daniellesucher.com/2013/07/ruby-case-versus-if/)
# define method 'fizzbuzz' initialized by single-variable argument 'number'
def fizzbuzz(number)
# define local method 'divisibleby3' defining a function checking divisibility of initialized by single-variable 'number' using the modulus and case-equality operators checking for remainder of 0 when divided by 3
divisibleBy3 = (number % 3 == 0)
# define local method 'divisibleby5' defining a function checking divisibility of initialized by single-variable 'number' using the modulus and case-equality operators checking for remainder of 0 when divided by 5
divisibleBy5 = (number % 5 == 0)
# construct case statement according to fizzbuzz specifications for 3 and 5, only 3 and only 5
case
# according to divisibiity by 3 and 5 using the AND logical operator
when divisibleBy3 && divisibleBy5
#print "FizzBuzz"
puts "FizzBuzz"
# according to divisibiity by 3
when divisibleBy3
#print "Fizz"
puts "Fizz"
# according to divisibiity by 5
when divisibleBy5
#print "Buzz"
puts "Buzz"
# use conditional else statement to account for numbers not divisible by 3 or 5
else
# print number that isn't divisible by 3 or 5
puts number
# end case statement
end
# end method
end
# call range of 1-100 utilizing the enumerable .each; invoke the enumerated range by block treating each number to initialize fizzbuzz method
(1..100).each { |n| fizzbuzz(n) }
# Benchmark testing: call the Benchmark module chaining class method 'measure' invoking method call of fizzbuzz using Array class method, effectively passing/testing behavior as an argument.
puts Benchmark.measure{fizzbuzz(Array(1..10000))}
# Under the hood with RVM; The InstructionSequence class represents a compiled sequence of instructions for the Ruby Virtual Machine. [Documentation](http://ruby-doc.org/core-2.2.3/RubyVM/InstructionSequence.html)
puts RubyVM::InstructionSequence.disasm(method(:fizzbuzz))
After consulting my Learning to Program in Java with Alice textbook in addition to other notes, and then verifying my hypothesis with several Google queries, I finally discovered the following within a SERP including a post by Danielle Sucher:

“Personally, I’m pretty intrigued by the fact that if/elsif uses branchUnless, while case uses branchIf. Based on that alone, 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).”

The Gist below contains the terminal-output replicating Danielle’s experimentation with RubyVM::InstructionSequence but scoped for FizzBuzz.  According to RubyDocs, the InstructionSequence class represents a compiled sequence of instructions for the Ruby Virtual Machine (RubyVM)(YARV) handling “instructions comprising a method or proc, compile strings of Ruby code down to VM instructions and disassemble instruction sequences to strings for easy inspection instructions,” or in short, enabling inspection under the hood of Ruby.  Evidently, “Case” and “Conditional If” statement-behaviors are respectively characterized by branchIf and branchUnless.

➜ Desktop ruby fizzbuzz.rb
fizzbuzz
== disasm: <RubyVM::InstructionSequence:fizzbuzz@fizzbuzz.rb>===========
== catch table
| catch type: break st: 0004 ed: 0008 sp: 0000 cont: 0008
|------------------------------------------------------------------------
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1] s1)
[ 2] array<Arg>
0000 trace 8 ( 3)
0002 trace 1 ( 4)
0004 getlocal_OP__WC__0 2
0006 send <callinfo!mid:map!, argc:0, block:block in fizzbuzz>
0008 trace 16 ( 19)
0010 leave ( 4)
== disasm: <RubyVM::InstructionSequence:block in fizzbuzz@fizzbuzz.rb>==
== catch table
| catch type: redo st: 0000 ed: 0086 sp: 0000 cont: 0000
| catch type: next st: 0000 ed: 0086 sp: 0000 cont: 0086
|------------------------------------------------------------------------
local table (size: 4, argc: 1 [opts: 0, rest: -1, post: 0, block: -1] s3)
[ 4] number<Arg>[ 3] divisibleBy3[ 2] divisibleBy5
0000 trace 256 ( 4)
0002 trace 1 ( 5)
0004 trace 1
0006 getlocal_OP__WC__0 4
0008 putobject 3
0010 opt_mod <callinfo!mid:%, argc:1, ARGS_SKIP>
0012 putobject_OP_INT2FIX_O_0_C_
0013 opt_eq <callinfo!mid:==, argc:1, ARGS_SKIP>
0015 setlocal_OP__WC__0 3
0017 trace 1 ( 6)
0019 trace 1
0021 getlocal_OP__WC__0 4
0023 putobject 5
0025 opt_mod <callinfo!mid:%, argc:1, ARGS_SKIP>
0027 putobject_OP_INT2FIX_O_0_C_
0028 opt_eq <callinfo!mid:==, argc:1, ARGS_SKIP>
0030 setlocal_OP__WC__0 2
0032 trace 1 ( 17)
0034 getlocal_OP__WC__0 3 ( 9)
0036 dup
0037 branchunless 42
0039 pop
0040 getlocal_OP__WC__0 2
0042 branchif 61
0044 getlocal_OP__WC__0 3 ( 11)
0046 branchif 70
0048 getlocal_OP__WC__0 2 ( 13)
0050 branchif 79
0052 trace 1 ( 16)
0054 putself
0055 getlocal_OP__WC__0 4
0057 opt_send_simple <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0059 jump 86 ( 17)
0061 trace 1 ( 10)
0063 putself
0064 putstring "FizzBuzz"
0066 opt_send_simple <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0068 jump 86 ( 17)
0070 trace 1 ( 12)
0072 putself
0073 putstring "Fizz"
0075 opt_send_simple <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0077 jump 86 ( 17)
0079 trace 1 ( 14)
0081 putself
0082 putstring "Buzz"
0084 opt_send_simple <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0086 trace 512 ( 18)
0088 leave ( 16)
super_fizzbuzz
== disasm: <RubyVM::InstructionSequence:super_fizzbuzz@fizzbuzz.rb>=====
== catch table
| catch type: break st: 0004 ed: 0008 sp: 0000 cont: 0008
|------------------------------------------------------------------------
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1] s1)
[ 2] array<Arg>
0000 trace 8 ( 25)
0002 trace 1 ( 26)
0004 getlocal_OP__WC__0 2
0006 send <callinfo!mid:map!, argc:0, block:block in super_fizzbuzz>
0008 trace 16 ( 37)
0010 leave ( 26)
== disasm: <RubyVM::InstructionSequence:block in super_fizzbuzz@fizzbuzz.rb>
== catch table
| catch type: redo st: 0000 ed: 0086 sp: 0000 cont: 0000
| catch type: next st: 0000 ed: 0086 sp: 0000 cont: 0086
|------------------------------------------------------------------------
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1] s3)
[ 2] element<Arg>
0000 trace 256 ( 26)
0002 trace 1 ( 27)
0004 getlocal_OP__WC__0 2
0006 putobject 3
0008 opt_mod <callinfo!mid:%, argc:1, ARGS_SKIP>
0010 putobject_OP_INT2FIX_O_0_C_
0011 opt_eq <callinfo!mid:==, argc:1, ARGS_SKIP>
0013 branchunless 35
0015 getlocal_OP__WC__0 2
0017 putobject 5
0019 opt_mod <callinfo!mid:%, argc:1, ARGS_SKIP>
0021 putobject_OP_INT2FIX_O_0_C_
0022 opt_eq <callinfo!mid:==, argc:1, ARGS_SKIP>
0024 branchunless 35
0026 trace 1 ( 28)
0028 putself
0029 putstring "FizzBuzz"
0031 opt_send_simple <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0033 jump 86 ( 27)
0035 trace 1 ( 29)
0037 getlocal_OP__WC__0 2
0039 putobject 3
0041 opt_mod <callinfo!mid:%, argc:1, ARGS_SKIP>
0043 putobject_OP_INT2FIX_O_0_C_
0044 opt_eq <callinfo!mid:==, argc:1, ARGS_SKIP>
0046 branchunless 57
0048 trace 1 ( 30)
0050 putself
0051 putstring "Fizz"
0053 opt_send_simple <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0055 jump 86 ( 29)
0057 trace 1 ( 31)
0059 getlocal_OP__WC__0 2
0061 putobject 5
0063 opt_mod <callinfo!mid:%, argc:1, ARGS_SKIP>
0065 putobject_OP_INT2FIX_O_0_C_
0066 opt_eq <callinfo!mid:==, argc:1, ARGS_SKIP>
0068 branchunless 79
0070 trace 1 ( 32)
0072 putself
0073 putstring "Buzz"
0075 opt_send_simple <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0077 jump 86 ( 31)
0079 trace 1 ( 34)
0081 putself
0082 getlocal_OP__WC__0 2
0084 opt_send_simple <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0086 trace 512 ( 36)
0088 leave ( 34)
➜ Desktop

Academic analysis of algorithms identifies two basic types of sequences, linear sequences and branching routines; think of the former as a set of instructions immediately processed without any consideration for contingencies (e.g. baking a cake; smoking a salmon; calculating Internal Rate of Return (IRR)) because process output only requires input (plug ‘n chug), whereas the latter can account for contingencies through branching (e.g. a computerized water-sprinkler/irrigation controller accounting for weather-if it rains later today, reduce water output by 50%)-and so a branching routine occurs whenever the path or flow of sequential logic in an algorithm splits into two or more paths, or “branches,” AKA selection sequences or selection structures.  If there are only two possible paths, the branching routine is binary branching ( e.g. a yes or no question), but if there are more than two paths, then the routine qualifies as a multiple branching algorithm (e.g. open-answer question/paths, like: “What color would you like for your purchased item?”).

linear-sequence

A Linear Sequence Algorithm

For the scope of this analysis, only binary branching will be further examined, reduced to two types: binary bypass and binary choice.  In a binary bypass, an instruction is either executed or bypassed, effectively skipped, potentially yielding no result or decisions, whereas in a binary choice, one of two sets of instructions will certainly occur, but not both. Hence think of a binary bypass as equivalent to a standard IF/THEN statement and binary choice as a IF/THEN/ELSE statement.

An infographic detailing the branching behavior of a selection sequence algorithm.

A Branching Routine Algorithm

An infographic about depicting a "Binary Choice Branching Algorithm"

A Binary Choice Branching Algorithm

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’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.  For the sake of clarity and convenience, here is a direct link to Danielle Sucher‘s post cited herein, Ruby: Case versus If (and a wee bit about Unless) detailing her performance tests.  For additional reading, here is another outstanding post “How A Ruby Case Statement Works And What You Can Do With It” by Alan Skorkin.  Thank you!

A funny picture of Eric Cartman of South Park in front of a blackboard for comic relief while examining Ruby fizzbuzz

Respect the Maths


UPDATE: As promised, I’ve substantiated my hypothesis with benchmarking. In conclusion, please find the terminal output and corresponding benchmarks included below for review. Please feel free to ask any questions or challenge. Thank you!

Desktop ruby fizzbuzz.rb | grep 0.0
0.010000 0.000000 0.010000 ( 0.010018)
0.010000 0.000000 0.010000 ( 0.011353)
require 'benchmark'
def fizzbuzz(array)
array.map do |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
end
puts Benchmark.measure{fizzbuzz(Array(1..100))}
puts "fizzbuzz"
# puts RubyVM::InstructionSequence.disasm(method(:fizzbuzz))
def slower_fizzbuzz(array)
array.map do |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
end
puts Benchmark.measure{slower_fizzbuzz(Array(1..100))}
puts "slower_fizzbuzz"
# puts RubyVM::InstructionSequence.disasm(method(:slower_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

A meme captured from The Abyss to convery technical Deep-Dive Duress

Decompression? No problem!

  1 comment for “A Contribution to Danielle Sucher’s Ruby Case vs. Conditional-If via FizzBuzz

Leave a Reply

Your email address will not be published. Required fields are marked *