Tcl performance: catch vs. info

Bastien Chevreux wrote in comp.lang.tcl: One thing I discovered is that 'catch' comes with a tremendous cost (of time) if it is triggered. I've always been the lazy kind of programmer, so I frequently used this kind of code:

# initialise 'b' so that we always have valid values (and we don't 
# forget it later on
set b "default"

# do something, eventually creating the variable 'a'
# now, set b to a if it exists
catch { set b $a}

Well ... don't do it (or just use it when you are absolutely sure that it'll trigger only in very rare exceptions). I've attached a small test script which shows that this is approximately 10 times slower than:

if {[info exists a]} {set b $a}

when a does not exist. Only when the catch does not trigger, it is approximately 2 times faster than the if-variant.

#!/bin/sh
# \
        exec tclsh "$0" ${1+"$@"}
 
proc doinfo_fail {} {
    set b "default"
    for {set i 0} {$i < 1000000} {incr i} {
        if {[info exists a]} {set b $a}
    }
    return $b
}
proc doinfo_ok_local {} {
    set a "xxxxxxx"
    set b "default"
    for {set i 0} {$i < 1000000} {incr i} {
        if {[info exists a]} {set b $a}
    }
    return $b
}
proc doinfo_ok_global {} {
    global a
    set b "default"
    
    for {set i 0} {$i < 1000000} {incr i} {
        if {[info exists a]} {set b $a}
    }
    return $b
}
proc docatch_fail {} {
    set b "default"
    for {set i 0} {$i < 1000000} {incr i} {
        catch {set b $a}
    }
    return $b
}
proc docatch_ok_local {} {
    set b "default"
    set a "xxxxxxx"
    for {set i 0} {$i < 1000000} {incr i} {
        catch {set b $a}
    }
    return $b
}
proc docatch_ok_global {} {
    global a
    set b "default"
    for {set i 0} {$i < 1000000} {incr i} {
        catch {set b $a}
    }
    return $b
}
set a "xxxxxxx"

puts "doinfo_fail         [time {doinfo_fail} 1]"
puts "doinfo_ok_local     [time {doinfo_ok_local} 1]"
puts "doinfo_ok_global    [time {doinfo_ok_global} 1]"
puts "docatch_fail        [time {docatch_fail} 1]"
puts "docatch_ok_local    [time {docatch_ok_local} 1]"
puts "docatch_ok_global   [time {docatch_ok_global} 1]"

exit 0

Donal Fellows replied: My performance figures for your script (using the current CVS HEAD version of Tcl under Solaris on an Ultra-5) can be seen below:

  doinfo_fail          7957618 microseconds per iteration
  doinfo_ok_local      8789728 microseconds per iteration
  doinfo_ok_global    11462929 microseconds per iteration
  docatch_fail        67244385 microseconds per iteration
  docatch_ok_local     3634439 microseconds per iteration
  docatch_ok_global    5126791 microseconds per iteration

What these demonstrate is that in performance-sensitive code it is important to code for the "normal" case; where you expect the code to normally succeed and only occasionally fail, there's actually quite a gain to be had from using catch, but you definitely take a performance hit in the failure case for doing this.


1 March 2001: Kevin Kenny adds:

Let's try to quantify the effect a bit more. Let's take one of the procedures from Counting Elements in a List, and code it up with both info exists and catch.

proc count1 { list countArray } {
     upvar 1 $countArray count
     foreach item $list {
         if { [catch { incr count($item) }] } {
             set count($item) 1
         }
     }
     return
}
 
proc count2 { list countArray } {
     upvar 1 $countArray count
     foreach item $list {
         if { [info exists count($item)] } {
             incr count($item)
         } else {
             set count($item) 1
         }
     }
     return
}

Now we can wrap a little benchmark program around the two procedures:

puts {  List  |          | [info }
puts { Length | [catch]  |  exists]}
puts { -------+----------+----------}
 
set list [list apple]
set i 1
 
while { $i <= 20 } {
     set n [expr { 20000 / $i }]

     set c1 [clock clicks -milliseconds]
     for { set j 0 } { $j < $n } { incr j } {
         count1 $list total
         unset total
     }
     set t1 [expr { [clock clicks -milliseconds] - $c1 }]
     set t1 [expr { $t1 / double($n) }]

     set c2 [clock clicks -milliseconds]
     for { set j 0 } { $j < $n } { incr j } {
         count2 $list total
         unset total
     }
     set t2 [expr { [clock clicks -milliseconds] - $c2 }]
     set t2 [expr { $t2 / double($n) }]
 
     puts [format " %6d | %8.3f | %8.3f " $i $t1 $t2]
     flush stdout
 
     incr i 1
     lappend list apple
}

This program tells us the time taken by both methods for various list lengths. THe following numbers are off a 550 MHz PIII running 8.3.2:

  List  |          | [info 
 Length | [catch]  |  exists]
 -------+----------+----------
      1 |    0.077 |    0.045 
      2 |    0.088 |    0.059 
      3 |    0.095 |    0.071 
      4 |    0.104 |    0.082 
      5 |    0.110 |    0.095 
      6 |    0.120 |    0.105 
      7 |    0.126 |    0.119 
      8 |    0.136 |    0.128 
      9 |    0.135 |    0.144 
     10 |    0.145 |    0.150 
     11 |    0.155 |    0.165 
     12 |    0.157 |    0.174 
     13 |    0.176 |    0.183 
     14 |    0.175 |    0.211 
     15 |    0.180 |    0.210 
     16 |    0.193 |    0.224 
     17 |    0.196 |    0.230 
     18 |    0.207 |    0.253 
     19 |    0.219 |    0.247 
     20 |    0.221 |    0.270 

This is a pretty typical example of the behavior of [catch] versus [info exists] as the success-failure ratio goes up.

A good rule of thumb is: If you expect the variable to exist at least 90% of the time, use [catch]. If the variable will fail to exist 10% of the time or more, use [info exists].


Updated Performance Figures

DKF: These figures were all done on a MacBook Pro of around 2009 vintage.

The take-home message from these figures is that it is always not a disadvantage to use info exists now that it is properly bytecoded (a Tcl 8.5 feature), and that a triggered catch is very expensive indeed (indeed, exceptionally so in 8.6).

Tcl 8.4

Tcl 8.4.7, supplied with OSX Leopard

doinfo_fail         402842 microseconds per iteration
doinfo_ok_local     408118 microseconds per iteration
doinfo_ok_global    415020 microseconds per iteration
docatch_fail        2232251 microseconds per iteration
docatch_ok_local    114664 microseconds per iteration
docatch_ok_global   110861 microseconds per iteration

Tcl 8.5

Tcl 8.5.2, built by ActiveState

doinfo_fail         98248 microseconds per iteration
doinfo_ok_local     110740 microseconds per iteration
doinfo_ok_global    113167 microseconds per iteration
docatch_fail        4862640 microseconds per iteration
docatch_ok_local    112386 microseconds per iteration
docatch_ok_global   113112 microseconds per iteration

Tcl 8.6

Development trunk tip from 27 March 2011.

doinfo_fail         101400 microseconds per iteration
doinfo_ok_local     115981 microseconds per iteration
doinfo_ok_global    119663 microseconds per iteration
docatch_fail        7382757 microseconds per iteration
docatch_ok_local    183856 microseconds per iteration
docatch_ok_global   178802 microseconds per iteration

APN presumes this performance hit with catch would also apply to (for example) looping control structures implemented in Tcl where catch is used to handle non-0 return codes such as continue and break