初めてのTclコマンド: array startsearch

[Tclには色々なコマンドがあります](https://www.tcl.tk/man/tcl8.6/TclCmd/contents.html)が、私は普段、その半分程度のコマンドしか使っていない気がしていました。

これにはいくつか理由があって

* 普段同じようなアプリばかり作っている (主にシリアル通信アプリ)
* バージョン8.6のTcl/Tkのヘルプは英語だけで日本語訳は無いため理解がおぼつかない
* ヘルプにはサンプルコード(実行例)が少ない
* 非公式(個人ブログ等)の解説記事も少ない

などで、要するに**使ったことが無いコマンドを学習するのがひと苦労**だと感じているからです。

今回はそういった今まで使ったことが無かったコマンドの中から

* array startsearch
* array anymore
* array nextelement
* array donesearch

を試してみたいと思います。

# どんな機能か?
今回調べてみるまでは、**search**と付くので`array names arrayName ?mode? ?pattern?`みたいな、連想配列の添え字を検索するコマンドだろうと、ぼんやり思っていました。

しかしたまたま運よく見つけた日本語の解説ページを見ると、オプション無しの`array names arrayName`、つまり配列の添え字を列挙するだけの機能でした。

https://blog.it-see.net/it-dokata/tcl-tk/array-4/

# え？だったら何のためにあるの？
ぶっちゃけ、array namesやarray getをforeachとともに使えば、startsearch, anymore, nextelement, donesearchの4つのサブコマンドで行う作業はできてしまいます。そのことは、なんと公式のヘルプにすら書いてあります。

ただ、`array startsearch`のヘルプの末尾に「～ but very large arrays」という記述があり、大きな配列で威力を発揮しそうな雰囲気です。

# 試してガッ〇ン!
「大きな配列」の定義がよく分らないのですが、取り敢えず単純に配列要素数の大小で比較してみることにしました。

記事末尾のスクリプト(benchmark_array.tcl)を書き、

```
> %LOCALAPPDATA%\Apps\Tcl86\bin\tclsh benchmark_array.tcl 1 1 1 > C1_I1_N1-1.log
```

のように実行し、配列作成方法による違いとして、

* create style1: array setを使用
* create style2: foreach, setを使用

配列列挙方法による違いとして、

* iterate style1: array startsearch, anymore, nextelement, donesearchを使用
* iterate style2: foreachとarray namesを使用
* iterate style3: foreachとarray getを使用

のバリエーションを

* 所要時間 (clock microsecondsを使用)
* メモリ使用(増加)量 (::twapi::get_process_memory_infoで得られるPrivateBytesを使用)

の観点で比較してみました。

実行環境は、

* CPU: Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz   3.40 GHz
* RAM: 16.0 GB
* OS: Windows10 64bit 21H2
* Tcl: Magicsplat Tcl/Tk for Windows, version 1.12.0 (changes), is based on Tcl/Tk 8.6.12

です。

# 比較結果

![compare3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165786/cc8601ec-7be9-0a10-1520-49bf32ace4e5.png)

## 配列作成
* 処理時間: style2 ＞ style1 
* メモリ使用量: style1 ≒ style2

処理時間は1回のコマンドで済むstyle1(array set)の方が断然速い結果となりました。

一方、メモリ使用量はほぼ同じでした。大部分は恐らく生成される配列による消費だからでしょうが、foreachやsetではメモリ使用量は増加しないとも言えそうです。

## 配列列挙
* 処理時間: style1 ＞ style3 ＞ style2
* メモリ使用量: style3 ＞ style2 ＞ style1 (※style1 ＝ 0)

処理時間は、いくら要素数を増やしても順位は変わりませんでしたが、劇的に違うというほどではありませんでした。

一方、メモリ使用量は大きな差があり、style1は要素数がどんなに増加してもメモリ使用量は全く増加しませんでしたが、style2とstyle3は要素数に比例してメモリ使用量が増加しており、style3は断トツでした。

array startsearch(style1)のヘルプの末尾で「～ but very large arrays」と書かれていた正体は、array startsearchによる列挙はメモリを消費しないという点にあったというわけです。

これに対し、style2(array names)は記述のシンプルさと速度を重視したチューニングがされているということのようです。

style3(array get)については配列と同等な内容のリストを作るわけなので、配列生成時と同じくらいメモリを消費するのは致し方ありません。style3はどうしてもリストにして処理したい場面だけに限定した方が良さそうです。

# ソースコード

```Tcl:benchmark_array.tcl
#!/bin/sh
#-*-mode:tcl;coding:utf-8;tab-width:4-*-\
    exec tclsh "$0" ${1+"$@"}

package require twapi

# benchmark method
proc benchmark {title script} {
    puts "\n$title"

    set s0 [::twapi::get_process_memory_info]
    set t0 [clock microseconds]
    if {[catch {uplevel 1 $script}]} {
        puts $::errorInfo
    }
    set t1 [clock microseconds]
    set s1 [::twapi::get_process_memory_info]

    set dt [expr {$t1 - $t0}]
    puts " spent(usec): $dt"

    set ds [expr {[dict get $s1 -privatebytes] - [dict get $s0 -privatebytes]}]
    puts " usage(byte): $ds"
}

# command-line option: number of entries in an array
lassign $argv create_style iterate_style N
if {$create_style eq "" || $create_style <= 0} {
    set create_style 1
}
if {$iterate_style eq "" || $iterate_style <= 0} {
    set iterate_style 1
}
if {$N eq "" || $N <= 0} {
    set N 1
}

# make a list
set list1 [list]
for {set i 0} {$i < $N} {incr i} {
    lappend list1 $i $i
}

switch -- $create_style {
    1 {
        # create1
        benchmark {Create array style1: array set} {

            array set array1 $list1

        }
    }
    2 {
        # create2
        benchmark {Create array style2: foreach, set} {

            foreach {key value} $list1 {
                set array1($key) $value
            }

        }
    }
    default {
        exit
    }
}

switch -- $iterate_style {
    1 {
        # iterate1
        benchmark {Iterate array style1: array startsearch, array anymore, array nextelement, array donesearch} {

            set search_id [array startsearch array1]
            while {[array anymore array1 $search_id]} {
                set value $array1([array nextelement array1 $search_id])
            }
            array donesearch array1 $search_id
            
        }
    }
    2 {
        # iterate2
        benchmark {Iterate array style2: foreach, array names} {

            foreach key [array names array1] {
                set value $array1($key)
            }
            
        }
    }
    3 {
        # iterate3
        benchmark {Iterate array style3: foreach, array get} {

            foreach {key value} [array get array1] {
            }
            
        }
    }
    default {
        exit
    }
}

# check array size
puts "\nArray statistics:"
foreach line [split [array statistics array1] "\n"] {
    puts " $line"
}
```