2020-04-12T10:12:43+09:00

AppleScript ライブラリ XList のループ処理について

先日、AppleScript のリストの高速化についてまとめた記事を更新した。その高速化手法を全面的に採用したライブラリが XList です。

リストの高速化手法をザックリと言ってしまえば、script object の property を経由してリストの要素にアクセスすると、速いよ!ということです。しかし、property にアクセスというのはライブラリ化の際にはご法度です。ライブラリのインターフェースはメソッドで統一するのが常道です。つまり、property に直接アクセスせず、ハンドラの呼び出しで全てを行うようにします。

このため、XList を使ったループ処理は property に直接アクセするよりは、ちょっと遅くなります。それでも、十二分に高速です。

XList はいくつかの方法でのループを提供していますが、一番のオススメは block script および each を使った方法です。柔軟性と速度のバランスが良いと思っています。

以下、XList および、一般的な方法、リストを property 経由でアクセスする方法の速度の比較です。詳細は、コード中のコメントに頑張って書いてあります。

use LapTime : script "LapTime"
use XList : script "XList"

on main()
set a_list to {}
repeat with n from 1 to 10000
set end of a_list to n
end repeat

(*== Simple Loop ==

もっともオーソドックスな AppleScript のループです。
とても遅いです、たった 10000 程度を回すのに、2 秒以上かかります。
しかし、高速化する魔法があります。
*)
set tm to LapTime's start_timer()
repeat with an_item in a_list
get contents of an_item
end repeat
tm's duration() -- 2184.5 [ms]

(*== Fastest Loop ==

リストを script object property に設定して、その property 経由で access すると、
312 倍も高速化されます。
*)
set tm to LapTime's start_timer()
script list_wrapper
property cnts : a_list
end script

repeat with an_item in cnts of list_wrapper
get contents of an_item
end repeat
tm's duration() -- 7.0 [ms]

(*== XList を使う ==

XList は、リストを script object property 経由でアクセスするテクニックをベースに、
Queue, Stack, Iterator のリスト操作をまとめたライブラリです。
XList を使えば、上記の高速化のテクニックが自動的に適用されます。
*)
set x_list to XList's make_with(a_list)

(*== each を使ったループ ==

XList は、様々なループ処理をサポートしています。
一番使えそうなオススメの方法は block script each を使った方法です。
block script は一つだけの do ハンドラをもった script object です。
each メソッドに block script を渡すと、リストの各要素を引数にして do ハンドラを実行します。
do ハンドラが flase を返すと、ループは停止します。
つまり、exit repeat したことと同じになります。

最速の方法の 7 [ms] から、比べると、約 12 [ms] と少し遅くなりました。
遅くなった原因は、次に述べるようにハンドラの呼び出しです。
しかし、ほんのちょっとのオーバヘッドがあったからといって、オーソドックスなループ処理に比べれば、
180 倍は早いんです。
XList が提供する利便性を考えれば、許容すべきオーバヘッドだと思います。
*)
script block
on do(x)
return x
end do
end script

set tm to LapTime's start_timer()
x_list's each(block)
tm's duration() -- 12.3 [ms]

(*== each_rush ==

XList each を使ったループは、約 12 [ms] かかりました。
最速の方法の 7 [ms] から、遅くなった要因として、
* ハンドラの呼び出し
* ループを抜けるかどうかの条件判定
が考えられます。

すこしでも高速化すべく、もしくは途中でループを停止する必要がない場合のために、
each_rush というメソッドを用意しています。

each との違いは、ループを抜けるかどうかの条件判定をしないこと。
しかし、実行時間は 1ms 程度しか変わらず、ほどんど高速化には寄与しません。
each のオーバーヘッド はもっぱらハンドラの呼び出しだと考えられます。
*)
set tm to LapTime's start_timer()
x_list's each_rush(block)
tm's duration() -- 11.2 [ms]

(*== has_next() next() を使った方法 ==

XList でも、has_next() next() を使えば repeat 文を使って、
コンサバティブなループ処理の見た目になります。

ハンドラの呼び出しが増えるので、ちょっと遅くなりますが、
インデックスで要素にアクセスしているので、
decrement_index() increment_index() を使って、ループを
巻き戻したりスキップしたりといった柔軟性があります。
*)
set tm to LapTime's start_timer()
tell x_list
repeat while its has_next()
its next()
end repeat
end tell
tm's duration() -- 32.4 [ms]

(* == map でリストを生成 ==

map メソッドを使うと、リストの要素を操作した結果から、別の新しいリストを生成できます。
XList を使うと新しいリストを高速に構築できます。
*)
set tm to LapTime's start_timer()
script block2
on do(x)
return x * 2
end do
end script
set new_list to x_list's map(block2)
tm's duration() -- 59.2 [ms]

(*
XList を使わないで、素のリストに要素を追加していくと、とても遅いです。
*)
set tm to LapTime's start_timer()
set new_list to {}
repeat with an_item in cnts of list_wrapper
set end of new_list to (an_item * 2)
end repeat
tm's duration() -- 1247.6 [ms]
end main

main()