in reply to Simple Switch statement

I like this enough that I will be using it for simple case statements.

You can simplify the syntax of it somewhat and make it read almost like the C version by declaring a sub called switch. It's actually more flexible being able to use (some) strings as well as integers.

Updated: Added check for none label characters in the switch expression in view of Courages concerns, allthough in this version, they just caused the default action without the check, the error reporting is handy.

#! perl -slw use strict; sub switch{ die "Bad switch expression at @{[ caller() ]}\n" unless $_[0] =~ /^\w+$/; eval{ goto "case_$_[0]" } or goto default; } for my $expr ( 1 .. 10, 'fred', "1;system('rm -rf /')" ) { switch( $expr ); { case_1: print '1'; last; case_2: print '2'; last; case_3: print '3'; last; case_4: print '4'; last; case_5: ; case_6: print '5 or 6'; last; case_fred: print 'fred'; last; default: print "default"; } } __END__ P:\test>test 1 2 3 4 5 or 6 5 or 6 default default default default fred Bad switch expression at main P:\test\test.pl8 10

Two things to watch for. The semi-colon after the switch(expr); and the need for a semi-colon after a label without a body (as in case_5: ; above).

I'm actually not quite sure why this latter one is required, but it seems to be. Unless someone can tell me where it is written that you can't have two consequetive labels in perl code, I'd consider this a bug?


Examine what is said, not who speaks.
"Efficiency is intelligent laziness." -David Dunham
"When I'm working on a problem, I never think about beauty. I think only how to solve the problem. But when I have finished, if the solution is not beautiful, I know it is wrong." -Richard Buckminster Fuller
If I understand your problem, I can solve it! Of course, the same can be said for you.

Replies are listed 'Best First'.
Re: Re: Simple Switch statement
by liz (Monsignor) on Sep 13, 2003 at 07:47 UTC
    For scoping reasons, I would change:
    sub switch{ eval{ goto "case_$_[0]" } or goto default; } for my $expr ( 1 .. 10, 'fred' ) { switch( $expr ); {
    to:
    my $switch = sub { eval{ goto "case_$_[0]" } or goto default; }; for my $expr ( 1 .. 10, 'fred' ) { $switch->( $expr ); {
    but other than that, I must say I like this idiom as well. If you're not interested in performance, that is. Because the performance of the switch like this, is abysmal compared to an identical if/elsif/else structure, as this little benchmark shows:
    use strict; use Benchmark; timethese( 50000,{ switch => sub { sub switch{ eval{ goto "case_$_[0]" } or goto default; } for my $expr ( 1 .. 10, 'fred' ) { switch( $expr ); { case_1: print STDERR '1'; last; case_2: print STDERR '2'; last; case_3: print STDERR '3'; last; case_4: print STDERR '4'; last; case_5: ; case_6: print STDERR '5 or 6'; last; case_fred: print STDERR 'fred'; last; default: print STDERR "default"; } } }, if => sub { for my $expr ( 1 .. 10, 'fred' ) { if ($expr eq '1') {print STDERR '1'} elsif ($expr eq '2') {print STDERR '2'} elsif ($expr eq '3') {print STDERR '3'} elsif ($expr eq '4') {print STDERR '4'} elsif ($expr eq '5' or $expr eq '6') {print STDERR '5 or 6'} elsif ($expr eq 'fred') {print STDERR 'fred'} else {print STDERR "default"} } }, } );
    Please note that I changed the print statements to "print STDERR" so that I could send them to the bitbucket. The result of the benchmark:
    Benchmark: timing 50000 iterations of if, switch if: 7 wallclock secs ( 5.41 usr + 0.00 sys = 5.41 CPU) @ 92 +42.14/s (n=50000) switch: 43 wallclock secs (39.85 usr + 0.00 sys = 39.85 CPU) @ 12 +54.71/s (n=50000)

    So the switch structure is at least 7 times as a comparable id/elsif/else structure. So don't put this in very deep and tight loops!

    Liz

    Edit by tye, change PRE to CODE around long lines

      Performance or lack of it in the switch:

      This really caught me by surprise, as to me it just seems counter intuitive that indexed jumps would be slower, especially by such a huge margin, but hey this is perl not 'C', what do I know? ;-)

      So, I took the code liz provided in post, which was so kindly benchmarked. I removed the prints to reduce dilution effect of typically long operations like print and replaced them with just an assignment; bart addressed this as well here. The following is a summay of things I looked at and the results:

        Switch.pm makes code look elegant, but imposes significant performance penalty. I learnt this the hard way. Here is the lesson, hopefully it should help others.

        Using Switch:

        $ cat switch.pl #!/usr/local/bin/perl use Switch; my $letter = "c"; switch($letter) { case "a" {print "1"} case "b" {print "2"} case "c" {print "3"} }

        Profiling this program with Devel::NYTProf, gave some valuable insight:

        ______Source_Code_Files_—_ordered_by_exclusive_time_then_name__ +_____ |Stmts|Exclusive|Reports |Source File + | |_____|Time_____|__________________|__________________________________ +_____| |3508_|324ms____|line • block |Text/Balanced.pm_______________________ +| |542__|147ms____|line • block |Switch.pm______________________________ +| |77___|32.1ms___|line • block |Exporter/Heavy.pm______________________ +| |52___|31.7ms___|line • block |Config_heavy.pl________________________ +| |132__|30.9ms___|line • block |warnings/register.pm___________________ +| |73___|22.2ms___|line • block |DynaLoader.pm__________________________ +| |39___|29.2ms___|line • block |SelfLoader.pm_(including_1_string_eval) +| |27___|15.5ms___|line • block |AutoLoader.pm__________________________ +| |184__|13.3ms___|line • block |overload.pm____________________________ +| |83___|12.9ms___|line • block |vars.pm________________________________ +| |170__|12.2ms___|line • block |Exporter.pm____________________________ +| |40___|10.4ms___|line • block |Config.pm______________________________ +| |12___|9.38ms___|line • block |Carp.pm________________________________ +| |25___|7.80ms___|line • block |Filter/Util/Call.pm____________________ +| |15___|7.77ms___|line • block |switch.pl______________________________ +| |25___|6.78ms___|line • block |version.pm_____________________________ +| |123__|4.36ms___|line • block |strict.pm______________________________ +| |34___|2.22ms___|line • block |warnings.pm____________________________ +| |26___|619µs___|line • block |feature.pm_____________________________| |1____|8µs_____|line • block |Config_git.pl__________________________| |5188_|720ms____|Total_(-4_statements_are_unaccounted_for)____________ +_____| |259__|36.0ms___|Average______________________________________________ +_____| |_____|12.9ms___|Median_______________________________________________ +_____| |_____|0.00852__|Deviation____________________________________________ +_____| Report produced by the NYTProf_4.04 Perl profiler, developed by Tim_Bu +nce and Adam_Kaplan.
        Though my program had 8 lines, a total of 5188 statements were processed!

        Using if-then-else:

        $ cat if-then-else.pl #!/usr/local/bin/perl my $letter = "c"; if ($letter eq "a") {print "1"; }elsif ($letter eq "b") {print "2"; }elsif ($letter eq "c") {print "3"; }
        Profiler output is:
        ____________________then_name_____________________ |Stmts|Exclusive|Reports |Source File | |_____|Time_____|__________________|_______________| |3____|13.0ms___|line • block |if-then-else.pl|

        Much better!

        Using a dispatch-table:

        #!/usr/local/bin/perl my $letter = "c"; my %h = ( "a" => sub{print "1"}, "b" => sub{print "2"}, "c" => sub{print "3"}, ); $h{$letter}->();
        And the profiler output:
        Source Code Files — ordered by exclusive time _____________________then_name_____________________ |Stmts|Exclusive|Reports |Source File | |_____|Time_____|__________________|________________| |5____|4.10ms___|line • block |dispatch-hash.pl|

        The best!!

        Of course, plain if-then-else and dispatch-hash are comparable. Depending on the use case, either may be chosen. However, it is clear that both of them are way better than Switch. For an in-production CGI application that I was tasked to improve performance of, replacing Switch with if-then-else gave a 150% improvement in performance. Many thanks to Tim Bunce and Adam Kaplan for writing that very useful tool - Devel::NYTProf.
Goto out of Sub - Re: Re: Simple Switch statement
by knexus (Hermit) on Sep 14, 2003 at 14:05 UTC
    Thanks for tweeking the code, I like how this looks over my original post. I was going for the 'C'ish look and this is much closer.

    However,I am puzzled/concerned over the technique of calling into  sub switch() and jumping out of it never to return. I have no idea if this is valid/acceptable in perl or not. Although it certainly appears to be OK in this example.

    In other languages I have used, this is a technique I use only very carefully... being sure to manipulate the stack/etc accordingly to avoid stack problems down the road, like overflow and/or returning to oblivion at some point in the future.

    Would/Could this technique bring out nasty side-effects... perhaps in larger applications or those running over extened periods of time?
    Is this of any concern in Perl scripts?

      Perhaps a quote from perlsyn is the best answer.

      Although not for the faint of heart, Perl does support a goto statement. There are three forms: goto-LABEL, goto-EXPR, and goto-&NAME. A loop's LABEL is not actually a valid target for a goto; it's just the name of the loop.

      The goto-LABEL form finds the statement labeled with LABEL and resumes execution there. It may not be used to go into any construct that requires initialization, such as a subroutine or a foreach loop. It also can't be used to go into a construct that is optimized away. It can be used to go almost anywhere else within the dynamic scope, including out of subroutines, but it's usually better to use some other construct such as last or die. The author of Perl has never felt the need to use this form of goto (in Perl, that is--C is another matter).

      I beleive that makes it safe to use this way, though LW's comment is worth reading twice. In this case, I think that the result is as 'structured' as many of the alternatives and much cleaner than most of them.

      It is also at least as efficient as the Switch module which itself uses goto amongst several other fairly esoteric practices, though obviously far less flexible.

      As for the performance, goto is known to be slow, but the benchmarks I've seen offered in this thread so far don't take into account the fact that using if..elsif...else cascades require multiple conditions to be evaluated for those cases near the end of the cascade. Of course this can be mitigated somewhat by careful ordering of the elements if the frequencey distribution of the conditions is predictable, but even then, if all paths are exercised the same number of times, there is no optimal ordering. Using goto bypasses this dilema. Whether this is ever enough to offset the slowness of goto will depend on the application. Used at a high level in the code, with non-trivial conditional code blocks this probably isn't much of a concern. I would definitely favour the hash-based dispatch method for performance critical application though.


      Examine what is said, not who speaks.
      "Efficiency is intelligent laziness." -David Dunham
      "When I'm working on a problem, I never think about beauty. I think only how to solve the problem. But when I have finished, if the solution is not beautiful, I know it is wrong." -Richard Buckminster Fuller
      If I understand your problem, I can solve it! Of course, the same can be said for you.

        Regarding the if/elsif/else comments: I agree. When coding in ASM and 'C', I would always put the highest frequency items first in the chain for performance reasons. It just seems logical that it would apply to any language.

        Also, a long time ago, I found that there was a sort of critical mass, if you will, at which point one method was faster than the other. Below that point if/else or cmp/jne etc. was the way to go, the methods were comparable at or near that point, and switch case/jump table was to prefered method over that critical mass.

        Now, things have changed, compilers and processors, so I don't know how much that holds true today. I don't write drivers any more so I just don't think about it ;-)