Comparing iRule Control Statements

A ways back, we put up an article titled “Ten Steps to iRules Optimization - DevCentral” in which we illustrated a few simple steps to making your iRules faster.  Item #3 in that article was “Understanding Control Statements”.  I decided to put the findings to the test and build myself an iRule that performs built-in timing diagnostics for the various control statements and hopefully shed some more light on what command to choose in your situation of choice.

The Commands

The control statements that we seen used primarily in iRules are the following:

  • switch – The switch command allows you to compare a value to a list of matching values and execute a script when a match is found.
    switch {
      “val1” { # do something }
      “val2” { # do something else }
      …
    }​
  • switch –glob – An extenstion to the switch command that allows for wildcard matching
    switch –glob {
      “val1” { # do something }
      “val2” { # do something else }
      …
    }​
  • if/elseif – A control statement that allows you to match arbitrary values together and execute a script when a match is found.
    if { $val equals “foo” } { # do something }
    elseif { $val equals “bar” } # do something else }
    …​
  • matchclass – Perform comparisons with the contents of a datagroup or TCL list.
    set match [matchclass “val” equals $::list];​
  • class match – A datagroup-only command to perform extended comparisons on group values (LTM v10.0 and above).
    set match [class match “val” equals listname]​

The Variables

Since the execution of a command is very very fast and the lowest resolution timer we can get is in milliseconds, we are required to execute the commands many times sequentially to get a meaningful number.  Thus we need to allow for multiple iterations of the operations for each of the tests.

The total number of comparisons we are doing also is a factor in how the command performs.  For a single comparison (or a data group with one value), the commands will likely be equivalent and it will be a matter of choice.  So, we will allow for multiple listsize values to determine how many comparisons will occur.

Where the comparison occurs in the logic flow is also important.  For a 1000 if/elseif command structure, comparing on the first match will be much faster than the last match.  For this reason, I’ve used a modulus over the total listsize to generate the tests on.  For a list size of 100, we will test item 1, 19, 39, 59, and 79 which should give a good sampling across various locations in the lists where a match occurs. 

Initialization

In my attempt to test as many possible combinations as possible, as well as the pain of hard-coding a 10000 line long “if/elfeif” statement, I’ve taken some shortcuts (as illustrated in the various test snippets).  In this code below, we check for query string parameters and then set default values if they are not specified.  A comparison list is then generated with the matchlist variable.

  1: #--------------------------------------------------------------------------
  2: # read in parameters
  3: #--------------------------------------------------------------------------
  4: set listsize    [URI::query [HTTP::uri] "ls"];
  5: set iterations  [URI::query [HTTP::uri] "i"];
  6: set graphwidth  [URI::query [HTTP::uri] "gw"];
  7: set graphheight [URI::query [HTTP::uri] "gh"];
  8: set ymax        [URI::query [HTTP::uri] "ym"];
  9:  
  10: #--------------------------------------------------------------------------
  11: # set defaults
  12: #--------------------------------------------------------------------------
  13: if { ("" == $iterations) || ($iterations > 10000) } { set iterations 500; }
  14: if { "" == $listsize } { set listsize 5000; }
  15: if { "" == $graphwidth } { set graphwidth 300; }
  16: if { "" == $graphheight } { set graphheight 200; }
  17: if { "" == $ymax } { set ymax 500; }
  18:  
  19: set modulus [expr $listsize / 5];
  20: set autosize 0;
  21:  
  22: #--------------------------------------------------------------------------
  23: # build lookup list
  24: #--------------------------------------------------------------------------
  25: set matchlist "0";
  26: for {set i 1} {$i < $listsize} {incr i} {
  27:   lappend matchlist "$i";
  28: }
  29:  
  30: set luri [string tolower [HTTP::path]]

The Main iRule Logic

The iRule has two main components.  The first checks for the main page request of /calccommands.  When that is given it will generate an HTML page that embeds all the report graphs.  The graph images are passed back into the second switch condition where the specific test is performed and the a redirect is given for the Google Bar chart.

 1: switch -glob $luri {
 2:   
 3:   "/calccommands" {
 4:     #----------------------------------------------------------------------
 5:     # check for existence of class file.  If it doesn't exist
 6:     # print out a nice error message.  Otherwise, generate a page of
 7:     # embedded graphs that route back to this iRule for processing
 8:     #----------------------------------------------------------------------
 9:   }
 10:   "/calccommands/*" {
 11:     #----------------------------------------------------------------------
 12:     # Time various commands (switch, switch -glob, if/elseif, matchclass, 
 13:     # class match) and generate redirect to a Google Bar Chart
 14:     #----------------------------------------------------------------------
 15:   }
 16: }

Defining the Graphs

The first request in to “/calccommands” will first test for the existence of the class file for the test.  A class must be defined named “calc_nnn” where nnn is the listsize.  Values in the list should be 0 to nnn-1.  I used a simple perl script to generate class files of size 100, 1000, 5000, and 10000.

The catch command is used to test for the existence of the class file.  If it does not exist, then an error message is presented to the user.

If the class file exists, then a HTML page is generated with 5 charts, breaking up the list index we are testing into between 1 and listsize.  When the HTML response is build, it is sent back to the client with the HTTP::respond command.

1: "/calccommands/*" {
2:   
3:   #----------------------------------------------------------------------
4:   # Time various commands (switch, switch -glob, if/elseif, matchclass, 
5:   # class match) and generate redirect to a Google Bar Chart
6:   #----------------------------------------------------------------------
7:   
8:   set item [getfield $luri "/" 3]
9:   set labels "|"
10:   set values ""
11:   
12:   #----------------------------------------------------------------------
13:   # Switch
14:   #----------------------------------------------------------------------
15:   
16:   set expression "set t1 \[clock clicks -milliseconds\]; \n"
17:   append expression "for { set y 0 } { \$y < $iterations } { incr y } { "
18:   append expression "switch $item {"
19:   foreach i $matchlist {
20:     append expression "\"$i\" { } ";
21:   }
22:   append expression " } "
23:   append expression " } \n"
24:   append expression "set t2 \[clock clicks -milliseconds\]";
25:   
26:   eval $expression;
27:   
28:   set duration [expr {$t2 - $t1}]
29:   if { [expr {$duration < 0}] } { log local0. "NEGATIVE TIME ($item, matchclass: $t1 -> $t2"; }
30:   append labels "s|";
31:   if { $values ne "" } { append values ","; }
32:   append values "$duration";
33:   
34:   if { $autosize && ($duration > $ymax) } { set ymax $duration }

The Tests

switch

As I mentioned above, I cheated a bit with the generation of the test code.  I found it too tedious to build a 10000 line iRule to test a 10000 switch statement, so I made use of the TCL eval command that allows you to execute a code block you define in a variable.  The expression variable holds the section of code I am to execute for the given test.

For the switch command, the first thing I do is take down the time before they start.  Then a loop occurs for iterations times.  This allows the clock counters to go high enough to build a useful report.  Inside this look, I created a switch statement looking for the specified index item we are looking for in the list.  Making the list content be strings of numbers made this very easy to do.  The foreach item in the generated matchlist a switch comparison was added to the expression.  Finally closing braces were added as was a final time.

Then the switch expression was passed to the eval command which processed the code.

Finally the duration was calculated by taking a difference in the two times and the labels and values variables were appended to with the results of the test.

1: "/calccommands/*" {
2:   
3:   #----------------------------------------------------------------------
4:   # Time various commands (switch, switch -glob, if/elseif, matchclass, 
5:   # class match) and generate redirect to a Google Bar Chart
6:   #----------------------------------------------------------------------
7:   
8:   set item [getfield $luri "/" 3]
9:   set labels "|"
10:   set values ""
11:   
12:   #----------------------------------------------------------------------
13:   # Switch
14:   #----------------------------------------------------------------------
15:   
16:   set expression "set t1 \[clock clicks -milliseconds\]; \n"
17:   append expression "for { set y 0 } { \$y < $iterations } { incr y } { "
18:   append expression "switch $item {"
19:   foreach i $matchlist {
20:     append expression "\"$i\" { } ";
21:   }
22:   append expression " } "
23:   append expression " } \n"
24:   append expression "set t2 \[clock clicks -milliseconds\]";
25:   
26:   eval $expression;
27:   
28:   set duration [expr {$t2 - $t1}]
29:   if { [expr {$duration < 0}] } { log local0. "NEGATIVE TIME ($item, matchclass: $t1 -> $t2"; }
30:   append labels "s|";
31:   if { $values ne "" } { append values ","; }
32:   append values "$duration";
33:   
34:   if { $autosize && ($duration > $ymax) } { set ymax $duration }

switch –glob

Just for kicks, I wanted to see what adding “-glob” to the switch command would do.  I didn’t make use of any wildcards in the comparisons.  In fact, they were identical to the previous test.  The only difference is the inclusion of the wildcard matching functionality.

  1: #----------------------------------------------------------------------
  2: # Switch -glob
  3: #----------------------------------------------------------------------
  4:  
  5: set expression "set t1 \[clock clicks -milliseconds\]; \n"
  6: append expression "for { set y 0 } { \$y < $iterations } { incr y } { "
  7: append expression "switch -glob $item {"
  8: foreach i $matchlist {
  9:   append expression "\"$i\" { } ";
  10: }
  11: append expression " } "
  12: append expression " } \n"
  13: append expression "set t2 \[clock clicks -milliseconds\]";
  14:  
  15: eval $expression;
  16:  
  17: set duration [expr {$t2 - $t1}]
  18: if { [expr {$duration < 0}] } { log local0. "NEGATIVE TIME ($item, matchclass: $t1 -> $t2"; }
  19: append labels "s-g|";
  20: if { $values ne "" } { append values ","; }
  21: append values "$duration";
  22:  
  23: if { $autosize && ($duration > $ymax) } { set ymax $duration }

If/elseif

The if/elseif test was very similar to the switch command above.  Timings were taken, but the only difference was the formation of the control statement.  In this case, the first line used ‘if { $item eq \”$i\” } {}’  Subsequent entries prepended “else” to make the rest of the lines ‘elseif { $item eq \”$i\”} {}’.  The evaluation of the expression was the same and the graph values were stored.

  1: #----------------------------------------------------------------------
  2: # If/Elseif
  3: #----------------------------------------------------------------------
  4: set z 0;
  5: set y 0;
  6:  
  7: set expression "set t1 \[clock clicks -milliseconds\]; \n"
  8: append expression "for { set y 0 } { \$y < $iterations } { incr y } { "
  9: foreach i $matchlist {
  10:   if { $z > 0 } { append expression "else"; }
  11:   append expression "if { $item eq \"$i\" } { } ";
  12:   incr z;
  13: }
  14: append expression " } \n";
  15: append expression "set t2 \[clock clicks -milliseconds\]";
  16:  
  17: eval $expression;
  18:  
  19: set duration [expr {$t2 - $t1}]
  20: if { [expr {$duration < 0}] } { log local0. "NEGATIVE TIME ($item, matchclass: $t1 -> $t2"; }
  21: append labels "If|";
  22: if { $values ne "" } { append values ","; }
  23: append values "$duration";
  24:  
  25: if { $autosize && ($duration > $ymax) } { set ymax $duration }

matchclass

My first attempt at this iRule, was to use matchclass against the generated matchlist variable.  The results weren’t that good and we realized the matchclass’s benefits come when working with native classes, not TCL lists.  I decided to keep this code the same, working on the auto-generated matchlist.  The next test will illustrate the power of using native data groups (classes).

  1: #----------------------------------------------------------------------
  2: # Matchclass on list
  3: #----------------------------------------------------------------------
  4:  
  5: set expression "set t1 \[clock clicks -milliseconds\]; \n"
  6: append expression "for { set y 0 } { \$y < $iterations } { incr y } { "
  7: append expression "if { \[matchclass $item equals \$matchlist \] } { }"
  8: append expression " } \n";
  9: append expression "set t2 \[clock clicks -milliseconds\]";
  10:  
  11: eval $expression;
  12:  
  13: set duration [expr {$t2 - $t1}]
  14: if { [expr {$duration < 0}] } { log local0. "NEGATIVE TIME ($item, matchclass: $t1 -> $t2"; }
  15: append labels "mc|";
  16: if { $values ne "" } { append values ","; }
  17: append values "$duration";
  18:  
  19: if { $autosize && ($duration > $ymax) } { set ymax $duration }

class match

In BIG-IP, version 10.0, we introduced the new “class” command that gives high-performance searches into data groups.  I decided to include the pre-configured classes for this test.  Data groups named “calc_nnn” must exist where nnn equls the listsize and it must contain that many elements for the test to be valid.

  1: #----------------------------------------------------------------------
  2: # class match (with class)
  3: #----------------------------------------------------------------------
  4:  
  5: set expression "set t1 \[clock clicks -milliseconds\]; \n"
  6: append expression "for { set y 0 } { \$y < $iterations } { incr y } { "
  7: append expression "if { \[class match $item equals calc_$listsize \] } { }"
  8: append expression " } \n";
  9: append expression "set t2 \[clock clicks -milliseconds\]";
  10:  
  11: log local0. $expression;
  12:  
  13: eval $expression;
  14:  
  15: set duration [expr {$t2 - $t1}]
  16: if { [expr {$duration < 0}] } { log local0. "NEGATIVE TIME ($item, matchclass: $t1 -> $t2"; }
  17: append labels "c|";
  18: if { $values ne "" } { append values ","; }
  19: append values "$duration";
  20:  
  21: if { $autosize && ($duration > $ymax) } { set ymax $duration }

Chart Generation

Once all of the tests have been run and the labels and values variables have all the data for the reports.  The image is served up with a simple HTTP::redirect to the appropriate google chart server.  I’ve made optimizations here to use the 0-9 prefix chart servers so that the browser could render the images quicker.

  1: #----------------------------------------------------------------------
  2: # build redirect for the google chart and issue a redirect
  3: #----------------------------------------------------------------------
  4:  
  5: set mod [expr $item % 10]
  6: set newuri "http://${mod}.chart.apis.google.com/chart?chxl=0:${labels}&chxr=1,0,${ymax}&chxt=x,y"
  7: append newuri "&chbh=a&chs=${graphwidth}x${graphheight}&cht=bvg&chco=A2C180&chds=0,${ymax}&chd=t:${values}"
  8: append newuri "&chdl=(in+ms)&chtt=Perf+(${iterations}-${item}/${listsize})&chg=0,2&chm=D,0000FF,0,0,3,1"
  9:  
  10: HTTP::redirect $newuri;

The Results

Several runs of the tests are shown below.  In the first, the tests are run on a list of size 100 for 10000 iterations for each test.  As you see for the first element in the matchlist, the if/elseif command is the winner, slightly edging out switch and then matchclass and class.  But when we start searching deeper into the list for comparisons, the if/elseif takes longer and longer depending on how far down in the list you are checking.  The other commands seem to grow in a linear fashion with the only exception being the class command.

http://virtualserver/calccommands?ls=100&i=10000&ym=100

Next we move on to a slightly larger list with 1000 elements.  For the first entry, we see that if if/elseif command takes the longest.  The others are fairly equal.  Again as we move deeper into the list, the other commands grow somewhat linearly in their time with the exception of class which again stays consistent regardless of where in the list it looks.

 

http://virtualserver/calccommands?ls=1000&i=1000&ym=100

Finally, on a 5000 sized list, If is the clear loser regardless of where you are matching.  switch and matchclass (on a list) are somewhat on par with eachother, but again the class command takes the lead.

http://virtualserver/calccommands?ls=5000&i=500&ym=100

Observations

Let’s take the first few bullets in our optimization article and see if they matched our observations here.

  • Always think: "switch", "data group", and then "if/elseif" in that order.
    If you think in this order, then in most cases you will be better off.
  • Use switch for < 100 comparisons.
    Switches are fastest for fewer than 100 comparisons but if you are making changes to the list frequently, then a data group might be more manageable.
  • Use data groups for > 100 comparisons.
    Not only will your iRule be easier to read by externalizing the comparisons, but it will be easier to maintain and update.
  • Order your if/elseif's with most frequent hits at the top.
    If/elseif's must perform the full comparison for each if and elseif.  If you can move the most frequently hit match to the top, you'll have much less processing overhead.  If you don't know which hits are the highest, think about creating a Statistics profile to dynamically store your hit counts and then modify you iRule accordingly.

I think this list should be changed to “always use class match” but that’s not the best option for usability in some places. In situations where you are working with smaller lists of data, managing the comparisons inline will be more practical than having them in lots of little class files.    Aside from that, I think based on the graphs above, all of the techniques are on target.

Get The Code

You can view the entire iRule for this article in the CodeShare - DevCentral under CommandPerformance.

Related Articles on DevCentral

 
Updated Jun 23, 2023
Version 2.0

Was this article helpful?

5 Comments

  • This was unbelievably awesome! Reading the best practices is one thing but actually seeing the data is another. Thanks for going through all the effort to do this! Even though my "lists" are always smaller than 10 items, I'm a big fan of datagroups and class match just to keep my iRules cleaner.

     

     

    Again, thanks a ton! -Chris Miller
  • Thanks Chris and Jason. I think I'll need to write another article just on the lessons learned when trying to build self profiling iRules. One example was including the "clock click" commands within the eval statement. At first I had them outside, but the data was all wonky as the overhead on eval parsing the expression for the very large lists threw things off. Hat tip to Colin for recommending moving them inside the eval.

     

     

    I also found another fun issue with certain cases having "clock clicks" shifting the clock backwards between calls. I'm still trying to figure that one out...

     

     

    Anyway, it was a fun article and I'll hat tip Jason on the idea of the Google Charts. They may look familiar to his previous article...

     

     

    -Joe
  • That was an excellent article. I have made it a habit of trying to use the Switch command when building iRules where I can, but I will definitely have to look at what class match can do for me now.

     

     

    Thanks Joe!

     

     

    -Nathan