
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 reveal—relax—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)) |
“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?”).
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.

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!
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 |
1 comment for “A Contribution to Danielle Sucher’s Ruby Case vs. Conditional-If via FizzBuzz”