AppleScript で高速データ処理

Table of Contents

Introduction

AppleScript の一つの大きな弱点は大きなデータの処理がとにかく遅いということです。例えば、次のようなスクリプトを考えてみましょう。大きなリストデータ(要素の数が 4000)にアクセスするスクリプトです。

なお、実行時間計測のために LapTime.osax を使っています。予めインストールして実行してください。

set a_list to {}
repeat with i from 1 to 4000
set end of a_list to i
end repeat

set tm1 to start timer

(* Case 1 : 3.2 seconds *)
repeat with i from 1 to length of a_list
get item i of a_list
end repeat

lap time tm1

(* Case 2 : 3.2 seconds *)
repeat with an_item in a_list
get contents of an_item
end repeat

time records of tm1

二つのやり方で、すべての要素にアクセスする時間を計測してみましが、僕のマシン(MacPook Pro 2.53GHz)で約 3 秒かかかっています。昔のマシン(PowerBook PowerPC G4 1.67GHz)だと約 6 秒でした。非常に遅いといえるでしょう。

しかし、いくつかのコツを覚えるだけで驚くほど高速化することができます。

リスト処理の高速化

リストの要素へのアクセスは、リストデータそのものにアクセスするのではなく、リストのリファレンスにアクセスすることでかなり高速化できます。

set a_list to {}
repeat with i from 1 to 4000
set end of a_list to i
end repeat

set list_ref to a reference to a_list
set tm1 to start timer

(* Case 1: 67.5 msec *)
repeat with i from 1 to length of list_ref
get item i of list_ref
end repeat

lap time tm1

(* Case 2: 67.4 msec *)
repeat with an_item in list_ref
get contents of an_item
end repeat

time records of tm1

このように、数十倍の高速化ができます。ちなみに 数 msec の違いは誤差とお考えください。その程度は実行の度に変わってきます。

リスト処理の高速化2

さて、a reference to 演算子によってリストのリファレンスを作るというテクニックはいつでも使える訳ではありません。というのも、リファレンスは、

にしか作れないという制限があります。すなわち、任意のハンドラ内のローカル変数に対してはリファレンスを作ることができません。sample2 の a_list はトップレベルの run ハンドラ内の変数だから a reference to 演算子を使うことができたのです。この問題を回避して、任意のローカル変数のリストをリファレンスとしてアクセスするトリックが存在します。次のように、ハンドラ内でスクリプトオブジェクトを定義し、その property に一度格納するのです。

on run
main()
end run

on main()
set a_list to {}
repeat with i from 1 to 4000
set end of a_list to i
end repeat

script listWrapper
property contents : a_list
end script

set tm1 to start timer

(* Case 1: 14.7 msec *)
repeat with i from 1 to length of a_list
get item i of contents of listWrapper
end repeat

lap time tm1

(* Case 2 : 15.3 msec *)
repeat with an_item in contents of listWrapper
get contents of an_item
end repeat

return time records of tm1
end main

おまけとして、さらに数倍、速くなるようです。リストのリファレンスにアクセスするより、スクリプトオブジェクトの property のリストにアクセスするほうが速いようです。sample1 と比べれば、数百倍の差です。なぜ速くなるかは謎ですが(誰か教えてください)、このテクニックを知っていると知らないでは、できるとことできないことに違いが生まれるほどの差であると思います。

テキスト処理への応用

さて、テキストデータの処理で行単位で処理を行うというのは非常によくある状況だと思います。その場合、やり方としては以下の二つが考えられます。

以下のようにリストに変換してから扱った方が高速です。

set a_list to {}
repeat with i from 1 to 4000
set end of a_list to i
end repeat
set AppleScript's text item delimiters to {return}
set a_text to a_list as string


set tm1 to start timer

(* Case 1 : 29.4 sec *)
repeat with i from 1 to count paragraph of a_text
get paragraph i of a_text
end repeat

lap time tm1

(* Case 2 : 3.3 sec *)
set a_list to every paragraph of a_text
repeat with a_paragraph in a_list
get contents of a_paragraph
end repeat

time records of tm1

sample3 のリストデータの高速処理のトリックを使えば、さらに高速化できます。

on run
set a_list to {}
repeat with i from 1 to 4000
set end of a_list to i
end repeat
set AppleScript's text item delimiters to {return}
set a_text to a_list as string

set tm1 to start timer
script listWrapper
property contents : paragraphs of a_text
end script

repeat with an_item in contents of listWrapper
get contents of an_item
end repeat
stop timer tm1 -- 29.6 msec
end run

sample4 と比較して圧倒的に高速です。このように、リスト処理の高速化テクニックを使えば、テキスト処理の高速化も行えます。

リストの高速書き換え

ところで、次のように repeat with ~ in ~ ループの中でリストの要素を書き換えることができることをご存知でしょうか?

repeat with an_item in a_list
set contents of an_item to new_value
end repeat

a_item にはリストの要素へのリファレンスが渡されるので、リストの要素を書き換えることができます。リストの内容を書き換えるだけだと、そんなに遅くないのですが、普通全部のデータを同じ値に書き換えることはしないと思います。ある条件にマッチした値だけを書き換えるのが現実的でしょう。

ですから、a_list の部分はスクリプトオブジェクト内のリストかリストの参照に置き換えないとめちゃくちゃ遅くなります。

しかし、どういう訳か、次のコードは動作しません。

on run
set a_list to {}
repeat with i from 1 to 4000
set end of a_list to i
end repeat

set tm1 to start timer
script listWrapper
property contents : a_list
end script

repeat with an_item in contents of listWrapper
if an_item > 1000 then
set contents of an_item to 5000 -- error of number -10006
end if
end repeat
stop timer tm1
end run

しかし、どういう訳か、a referenc to をつけるとリストの要素の書き換えができます。そして、パフォーマンスも悪くないです。

on run
set a_list to {}
repeat with i from 1 to 4000
set end of a_list to i
end repeat

set tm1 to start timer
script listWrapper
property contents : a_list
end script
repeat with an_item in (a reference to contents of listWrapper)
if an_item > 1000 then
set contents of an_item to 5000
end if
end repeat
stop timer tm1 -- 73.6 msec
end run

なぜこうなるかは激しく謎です。誰か教えてください。

Summary

AppleScript で大きなデータ処理を行う場合の高速化テクニックをご紹介しました。

できれば、このような謎のトリックを使わずとも、常に最適化された処理を実行して欲しいと思いますが、どういうわけか改善される気配はありません。しかし、上記のようなことをいちいち覚えておくのはしんどいですね。そこで、上記の高速化のテクニックを含めてリストを扱うのに便利なライブラリを作って使っています。

XList -- Iterator, Queue, Stack として使えるリストのラッパーオブジェクトを提供する AppleScript のモジュール。AppleScript のリストはリファレンスを経由して要素を参照しないと速度が大きく低下するという性質があります。XList は内部で、常にリファレンスを経由してリストにアクセスする為、常に良好な動作速度が得られます。