大きなデータを処理する時のこつ

1998.04.26

by 野本夏俊


 例えば、「あるテキストデータに対して1文字ごとにスペースを挿入する」というスクリプトを組む場合、

set AppleScript's text item delimiters to ""
set R to {}
repeat with i from 1 to length of MyData ------------*
  set end of R to character i of MyData & " "
end repeat ---------------------------------------**
R as text

というようにすると思います。(まぁ、いろいろやり方はあると思いますけど...)
 MyDataが小さなデータ(100文字以内など)ならこれで特に問題はありませんが、大きくなる(数千を超える)と、問題が出てきます。

 例えば、以下のようなスクリプトで実験して見ます。----------テスト1
テスト環境:
 マシン:UMAX Pulsar 2250 メモリ割り当て20MB
 OS  :7.6.1+SpeedDoubler2

property S : 1000
property E : 10000
property Step : 1000

on open FS
  set AppleScript's text item delimiters to return
  repeat with F in FS
    Doit(F)
  end repeat
end open

on Doit(F)
  open for access F
  set MyData to read F
  close access F
  
  set TheTime to {}
  repeat with T from S to E by Step
    set LastTime to current date
    set R to {}
    
    repeat with i from 1 to T ----------------------------*
      set end of R to character i of MyData & " "
    end repeat ---------------------------------------**
    
    set end of TheTime to (T as string) & tab & ((current date) - LastTime) as string
  end repeat
  
  set fref to open for access file ((F as text) & ".time") with write permission
  set eof fref to 0
  write (TheTime as text) to fref
  close access fref
end Doit

 テキストデータを読み込んで、処理する文字数を千〜1万文字まで千ずつ増やしながら、処理時間を記録するスクリプトです。結果は以下のようになります。

文字数 時間(秒)
1000........2
2000........5
3000.......11
4000.......18
5000.......28
6000.......40
7000.......55
8000.......70
9000.......87
10000.....109

 これをグラフ化して見ればわかりますが、文字数が増えるに連れて、処理時間が加速度的に増えていきます。1000と2000の時間差が3秒ですから、1000文字増えるごとに3秒ずつ加算されていくなら、10000では27秒で処理できてもよさそうなもんですが、実際はそうはなりません。
 これはアップルスクリプトでは(他でも?)「文字1と文字nを求める時間が違う」ことが原因です。試しに、以下のようなスクリプトで文字nを求める時間を計測して見ます。

property S : 1
property E : 10001
property Step : 1000

on open FS
  set AppleScript's text item delimiters to return
  repeat with F in FS
    Doit(F)
  end repeat
end open

on Doit(F)
  open for access F
  set MyData to read F
  close access F
  
  set TheTime to {}
  repeat with T from S to E by Step
    set LastTime to current date
    
    repeat 50000 times
      get character T of MyData
    end repeat
    
    set end of TheTime to (T as string) & tab & ((current date) - LastTime) as string
  end repeat
  
  set fref to open for access file ((F as text) & ".R") with write permission
  set eof fref to 0
  write (TheTime as text) to fref
  close access fref
end Doit

 テキストデータを読み込んで、nを1〜10001まで千ずつ増やしながら、文字nを50000回求める時間を記録するスクリプトです。結果は以下のようになります。

n 時間
1..........4
1001......11
2001......18
3001......25
4001......32
5001......38
6001......45
7001......52
8001......57
9001......66
10001.....74

 結果をグラフ化すればわかりますが、ほぼnに比例して処理時間が増加しています。テスト1ではこの違いがもろに処理時間に反映されています。

 では、これを回避するためにテスト1の*〜**を

repeat with Block from 1 to T - 999 by 1000 --------------*
  set R to R & SUB(text Block thru (Block + 999) of MyData)
end repeat ----------------------------------------**

と、書き換え、サブルーチンとして、

on SUB(SubData)
  set SubR to {}
  repeat with i from 1 to 1000
    set end of SubR to character i of SubData & " "
  end repeat
  return SubR
end SUB

を加えます。-------------テスト2

 つまり、データを1000ずつのブロックに分けて処理するわけです。この結果、最終的に得られるRの値は同じですが、処理時間は大きく異なります。

テスト1        テスト2
回数.......時間    回数.......時間
1000..........2    1000..........2
2000..........5    2000..........3
3000.........11    3000..........5
4000.........18    4000..........6
5000.........28    5000..........7
6000.........40    6000.........10
7000.........55    7000.........10
8000.........70    8000.........13
9000.........87    9000.........13
10000.......109    10000........16

 このように大きなデータを処理するときには、データをある程度の大きさにブロック分けして処理するほうが効率がいいことがわかります。

 ある程度経験を積んだスクリプターにとっては目新らしいTipsではないでしょうが、ま、初心者向けということで... ちょっと参考になりました?


 さらに続きです。

 下のようなスクリプトを組んで、ブロックわけするときには、ブロックサイズをどの程度の大きさにするのがベストなのかを調べて見ました。

property BlockStart : 100
property BlockEnd : 1000
property BlockStep : 100

on open FS
  set AppleScript's text item delimiters to return
  repeat with F in FS
    Doit(F)
  end repeat
end open

on Doit(F)
  open for access F
  set MyData to read F
  close access F
  
  set TheTime to {}
  repeat with BlockSize from BlockStart to BlockEnd by BlockStep
    set LastTime to current date
    set R to {}
    
    repeat with Block from 1 to 10001 - BlockSize by BlockSize
      set R to R & SUB(text Block thru (Block + BlockSize - 1) of MyData, BlockSize)
    end repeat
    
    set end of TheTime to (BlockSize as string) & tab & ((current date) - LastTime) as string
  end repeat
  
  set fref to open for access file ((F as text) & ".T5") with write permission
  set eof fref to 0
  write (TheTime as text) to fref
  close access fref
end Doit

on SUB(MyData, BlockSize)
  set R to {}
  repeat with i from 1 to BlockSize
    set end of R to character i of MyData & " "
  end repeat
  return R
end SUB

 結果的に、1ブロックを200文字前後にするのが最も速いことがわかりました。
 以外に細かく分けたほうがいいんですね。
 で、テスト2のスクリプトのブロックサイズを200にして試した結果...

文字数   ブロック ブロック ブロック
       なし  1000  200
----------------------------------
1000     2    2   0
2000     5    3   2
3000    11    5   1
4000    18    6   3
5000    28    7   3
6000    40   10   4
7000    55   10   5
8000    70   13   5
9000    87   13   6
10000  109   16   8

 ブロック分けするだけで、109秒かかっていたものが8秒まで短縮できてしまうんですから、なかなかたいしたものです。
 処理の内容によって最適なブロックサイズは変わってくると思いますから、いろいろ試行錯誤して見ましょう。チューニング次第でずいぶんスピードが改善されるかもしれませんよ。


Tipsに戻る
AppleScriptのページに戻る
このホームページに関するお問い合わせは、karino@drycarbon.comまで。