Version 7 of fori

Updated 2004-12-31 23:51:43

MC, 31 Dec 2004: In a post on his blog[L1 ] discussing implementation and design issues of his Ramble game (written, naturally in Tcl/Tk), WHD proposes a fori command, whose signature looks like:

fori var from to script

Since any Tcl version is going to be slower than the built-in version of [for], Will writes: "I think something like fori would be a nice addition to Tcl...but it would have to be implemented as a C extension."

I was curious how much slower a Tcl version would be... Here are a couple of different approaches I came up with this afternoon:

 proc fori1 {var m n script} {
     uplevel 1 for [list "set [list $var] $m"] [list "\$[list $var] <= $n"] [list "incr [list $var]"] [list $script]
 }

 proc fori2 {var m n script} {
     uplevel 1 for [list "set [list $var] $m"] [list "\[set [list $var]\] <= $n"] [list "incr [list $var]"] [list $script]
 }

 proc fori3 {var m n script} {
     uplevel 1 "set [list $var] [expr {$m - 1}]
         while {\[incr [list $var]\] <= {$n}} {
             $script
         }"
 }

 proc fori4 {var m n script} {
     uplevel 1 "if {1} {
         for {set [list $var] {$m}} {\$[list $var] <= {$n}} {incr [list $var]} {                      
             $script
         }
     }"
 }

 proc fori5 {var m n script} {
     uplevel 1 "if {1} [list "set [list $var] [expr {$m - 1}]
         while {\[incr [list $var]\] <= {$n}} {
             $script
         }"]"
 }

and now a bit of infrastructure to make testing/timing them easier:

 proc try {name proc limit {n_iterations 50}} {
     set time [time {set result [$proc $limit]} $n_iterations]
     return [format { #%-5s %-14s result == %8d, in %7d microseconds} \
         $name "(1 .. $limit):" $result [lindex $time 0]]
 }

 proc builtin {limit} {
     set sum 0
     for {set i 1} {$i <= $limit} {incr i} {
         incr sum $i
     }
     return $sum
 }

 for {set i 1} {$i <= 5} {incr i} {
     proc method$i {limit} [format {
         set sum 0
         fori%d i 1 $limit {incr sum $i}
         return $sum
     } $i]
 }

 foreach size {10 100 1000 10000} {
     if {$size != 10} then {puts ""} 
     foreach version {builtin method1 method2 method3 method4 method5} \
         name {for fori1 fori2 fori3 fori4 fori5} {
         # run once before we begin timing
         $version $size
         puts [try $name $version $size]
     }
 }

and now to see the timing results (these are from tclsh (8.4.7) on a 1.33GHz PowerBook running MacOS X 10.3 via wiki-reaper | tclsh):

 #for   (1 .. 10):     result ==       55, in      57 microseconds
 #fori1 (1 .. 10):     result ==       55, in     290 microseconds
 #fori2 (1 .. 10):     result ==       55, in     285 microseconds
 #fori3 (1 .. 10):     result ==       55, in     218 microseconds
 #fori4 (1 .. 10):     result ==       55, in     183 microseconds
 #fori5 (1 .. 10):     result ==       55, in     173 microseconds

 #for   (1 .. 100):    result ==     5050, in      86 microseconds
 #fori1 (1 .. 100):    result ==     5050, in    4183 microseconds
 #fori2 (1 .. 100):    result ==     5050, in    1373 microseconds
 #fori3 (1 .. 100):    result ==     5050, in    3808 microseconds
 #fori4 (1 .. 100):    result ==     5050, in     277 microseconds
 #fori5 (1 .. 100):    result ==     5050, in     219 microseconds

 #for   (1 .. 1000):   result ==   500500, in     358 microseconds
 #fori1 (1 .. 1000):   result ==   500500, in    7054 microseconds
 #fori2 (1 .. 1000):   result ==   500500, in    7087 microseconds
 #fori3 (1 .. 1000):   result ==   500500, in    5559 microseconds
 #fori4 (1 .. 1000):   result ==   500500, in    3664 microseconds
 #fori5 (1 .. 1000):   result ==   500500, in    1553 microseconds

 #for   (1 .. 10000):  result == 50005000, in    5034 microseconds
 #fori1 (1 .. 10000):  result == 50005000, in   54393 microseconds
 #fori2 (1 .. 10000):  result == 50005000, in   53805 microseconds
 #fori3 (1 .. 10000):  result == 50005000, in   39142 microseconds
 #fori4 (1 .. 10000):  result == 50005000, in   20296 microseconds
 #fori5 (1 .. 10000):  result == 50005000, in   15092 microseconds

Out of these fori5 looks the best overall, but it is still 2-5 times slower than for is; can we do any better?