coding()ハンドラをより高速にする

今回の一連のAppleScriptの話題は、coding()ハンドラの発見がすべての始まり。ところが、そのcoding()ハンドラ自体は、最初にダメ出しを修正した以降は、ほとんど見直していなかった。json_from()、for_key()、set_key_value()はより効率的な処理を目指して修正したのに、そのベースとなるcoding()ハンドラの効率が悪いと、すべてのハンドラに悪い影響が及んでしまう。

  • for_key()、set_key_value()は、見直しによってもはやcoding()ハンドラに依存しなくなってしまったが...。

今一度、coding()ハンドラを徹底的に見直してみた。

現状のコード


on coding1(obj) try
if obj's class = text then
"\"" & obj & "\""
else if obj's class = list or obj's class = record then
obj as number
else
obj as text
end if
on error msg
try
--match_str("/\\{.*\\}/=~" & quoted form of msg) --依存するので以下のコードに回避した
"require \"jcode\";$KCODE=\"u\";/\\{.*\\}/=~" & quoted form of msg & ";puts($&);"
do shell script "ruby -e " & quoted form of result
--do shell script "echo " & quoted form of msg & "|sed -e \"s/^[^{]*//g\"|sed -e \"s/[^}]*$//g\""--シェルコマンドのみ
on error
--do shell script262,144バイト制限エラーでも、日本語環境なら救われる(それ以外はNG
--http://developer.apple.com/jp/technotes/tn2002/tn2065.html
msg's items 1 thru -24 as text --3
end try
end try
end coding1

現状の問題点

  • do shell scriptによって、シェルスクリプトRubyに依存している。
  • そのため、シェルの制限である262144バイトを超える情報を渡そうとすると、エラーになってしまう。
error "0 以外の状況でコマンドが終了しました。" number 255
  • そのエラーが発生しても日本語環境なら救われるようになっているが、それ以外の言語環境ではどうしようもない。

パフォーマンス計測

  • 実際に利用しているauto_loginのログイン情報のレコードを引数に、coding()ハンドラを1000回繰り返す時間を計測してみた。
    • 3回計測して、31秒、32秒、33秒だった。


set obj to result

display dialog "計測開始"
set t to current date
repeat 1000 times
set res to coding1(obj) end repeat
(current date) - t
display dialog result
res

AppleScriptのみで文字列処理

  • 現状では、エラーメッセージに含まれる、余分な文字列を取り除くために、Ruby正規表現を利用している。
    • 余分な文字列 = 日本語環境では" のタイプを number に変換できません。"
  • これをAppleScriptだけで何とかしてみる。


on coding2(obj) try
if obj's class = text then
"\"" & obj & "\""
else if obj's class = list or obj's class = record then
obj as number
else
obj as text
end if
on error msg
set head to offset of "{" in msg
set tail to -(offset of "}" in (msg's items's reverse as text)) msg's items head thru tail as text
end try
end coding2

  • 変更したのはon error以下のブロック内。動作は正常。

しかし、遅い...

  • 1000回ループさせて62〜64秒だった。
  • coding1()では31秒前後だったので、do shell script方式よりも2倍も時間がかかってしまった。
  • 何がそんなに遅いのか?試しに、同じ仕組みだけど、自前でループ作ってやってみた。


on coding2(obj) try
if obj's class = text then
"\"" & obj & "\""
else if obj's class = list or obj's class = record then
obj as number
else
obj as text
end if
on error msg
repeat with i from 1 to msg's number
if msg's item i = "{" then exit repeat
end repeat
set head to i
repeat with i from 1 to msg's number
if msg's item -i = "}" then exit repeat
end repeat
set tail to -i
msg's items head thru tail as text
end try
end coding2

  • すると、予想外の好成績。
  • 1000回ループさせて、37秒だった。
  • なんと、offset inとreverse使うよりは、自前のループで検索した方が速いのである。

文字列置き換えを駆使する

  • AppleScriptの文字列操作はイケてない。このスピードにはガックリである。
  • 但し、区切り文字を活用した文字列置き換えだけは、超高速なのだけど...。
  • 試しに、文字列置き換え方式で計測してみることにする。


on coding3(obj) try
if obj's class = text then
"\"" & obj & "\""
else if obj's class = list or obj's class = record then
obj as number
else
obj as text
end if
on error msg
replace(msg, " のタイプを number に変換できません。", "") end try
end coding3

on replace(src_text, text1, text2) join(split(src_text, text1), text2) end replace

on split(src_text, delimiter) set last_delimiter to AppleScript's text item delimiters
set AppleScript's text item delimiters to delimiter
set res to src_text's text items
set AppleScript's text item delimiters to last_delimiter
res
end split

on join(src_list, delimiter) set last_delimiter to AppleScript's text item delimiters
set AppleScript's text item delimiters to delimiter
set res to src_list as text
set AppleScript's text item delimiters to last_delimiter
res
end join

  • すると、激速である!
  • 1000回ループさせて、たったの9秒。
  • 今までと次元の違う速さである。
  • しかし、この方法では日本語環境しか対応できない。
  • この超高速を活かして、文字列置き換えだけで、言語環境に依存せず、{ }内だけを取得する方法はないものか...。
  • そう考えてやってみたのが以下のコード。


on coding(obj) try
if obj's class = text then
"\"" & obj & "\""
else if obj's class = list or obj's class = record then
obj as number
else
obj as text
end if
on error msg
set a_list to split(msg, "{") set a_list's item 1 to ""
set a_list to split(join(a_list, "{"), "}") set a_list's item -1 to ""
join(a_list, "}") end try
end coding

on split(src_text, delimiter) set last_delimiter to AppleScript's text item delimiters
set AppleScript's text item delimiters to delimiter
set res to src_text's text items
set AppleScript's text item delimiters to last_delimiter
res
end split

on join(src_list, delimiter) set last_delimiter to AppleScript's text item delimiters
set AppleScript's text item delimiters to delimiter
set res to src_list as text
set AppleScript's text item delimiters to last_delimiter
res
end join

  • 計測もしてみた。

すると...

  • 1000回ループさせて10秒!
  • 当初30秒だったので、3倍速くなった。
  • 素晴らしい速さ。

AppleScriptの置き換え処理だけは超高速なのであった。

  • そして、do shell scriptに依存しないので、もはや262144バイトの制限も受けない。
  • コード自体は何をやっているか分かりにくくなったが、二重のtryブロックが不要になって構造はシンプルになった。
  • split()、join()ハンドラに依存してしまうが、このハンドラは何をやるにも外せないほど必須となる基本ハンドラだ。
    • スピードの改善、
    • 262144バイトの制限も受けない、
  • この二つの恩恵を受けられるのなら、split()、join()ハンドラへの依存くらいなら許せる。


以上で、より高速かつ、バイト制限のないcoding()ハンドラが完成した!