トップ «前の日記(2011-02-06) 最新 次の日記(2011-02-08)» 編集

日々の破片

著作一覧

2011-02-07

_ WIN32OLEの高速化手法

一昨昨日の(「さきおととい」って、「昨」がだぶるになるのか!)東京Ruby会議で、WIN32OLEででっかなXLSファイルをいじくると遅いというので、ActiveScriptRubyを使えば10~100倍速いと言ったけど、実測してみたら悪い方にでたらめだった。すみません。

というわけで、高速化について実測した結果を元に示す。

ある程度遅くなければ意味がないので、まっさらなワークシートに10000行、A~Zの26カラムへWIN32OLEを利用してデータを設定するスクリプトを利用する。

次のスクリプト(xls.rb)を出発点とする。

#coding: cp932
require 'win32ole'
xl = WIN32OLE.new('Excel.Application')
xl.visible = true
book = xl.workbooks.add
start = Time.now
1.upto(10000) do |row|
  ?A.upto(?Z) do |col|
    if row.even?
      book.worksheets(1).range("#{col}#{row}").value = 3
    else
      book.worksheets(1).range("#{col}#{row}").value = 'こんにちは'   
    end  
  end
end
puts "Elapsed time: #{Time.now - start} secs"

実測すると、159.45412秒を得た(Core-i5系Xeon、6GB、Windows7(64)、Excel2010(32))。

最初に行うべき最適化は、ループの中に2行(もっとも実行は都度1回)あるbook.worksheets(1).range("#{col}#{row}").value = の変形だ。

この記述は以下のように動作する。

book - 外部プロセス呼び出し - worksheets(1) - 外部プロセス呼び出し - range(...) - 外部プロセス呼び出し - Value=

この記述のうち、book.worksheet(1)の呼び出しは、指定されたWorkSheetオブジェクトの取得をループの外に出すことで回避できる。

修正後のスクリプト(xls2.rb)を次に示す。

#coding: cp932
require 'win32ole'
xl = WIN32OLE.new('Excel.Application')
xl.visible = true
book = xl.workbooks.add
start = Time.now
sheet = book.worksheets(1)
1.upto(10000) do |row|
  ?A.upto(?Z) do |col|
    if row.even?
      sheet.range("#{col}#{row}").value = 3
    else
      sheet.range("#{col}#{row}").value = 'こんにちは'      
    end  
  end
end
puts "Elapsed time: #{Time.now - start} secs"

sheetへの設定をループの外へ追い出したため、ループ内での呼び出しは、sheet - 外部プロセス呼び出し - range(..) - 外部プロセス呼び出し - Value = と2/3に減少する。

実測すると、122.374999秒と、元の処理時間の3/4となった。

ここで、ActiveScriptRubyを使うことで外部プロセス呼び出しのオーバーヘッドがなくなるので、一気に1/10となるかと思ったが、良く考えてみたら、外部プロセス呼び出しのオーバーヘッド2/3で、処理時間3/4なのだからそんな極端な高速化は期待できないことになる。

以下の例では、Ruby-1.9.2配布パッケージに含めているRScript19を元にする。このバージョンではインストール直後からRubyize機能(OLEオートメーションで処理可能なRubyオブジェクト化)が利用できるからだ。いわゆるASRの場合、インストールディレクトリのsampleディレクトリ内のRubyizeサンプルを自力でRegsvr32を利用して登録する必要がある。

以下のスクリプトを用意する(xlsemb.rb)。

#coding: cp932
class Filler
  def fill_three(book)
    start = Time.now
    1.upto(10000) do |row|
      ?A.upto(?Z) do |col|
        if row.even?
          book.worksheets(1).range("#{col}#{row}").value = 3
        else
          book.worksheets(1).range("#{col}#{row}").value = 'こんにちは'      
        end  
      end
    end
    "Elapsed time: #{Time.now - start} secs"
  end
end
Filler.new

これまでのスクリプトと異なり、与えられたWorkbookオブジェクトを操作するメソッドを持つオブジェクトを返すスクリプトだ。

このスクリプトを新規Excelブックの適当なセル(以下の例では2シート目のA1)の中へ貼り付ける。これはVBAの中では複数行の文字列がヒアドキュメントのようには簡単に記述できないからで、別にVBAのコード内へ文字列として記述しても良い。

次に、Excelに次のマクロを作成する。

Sub fillbyruby()
    Dim ruby As Object
    Dim filler As Object
    Set ruby = CreateObject("ruby.object.1.9")
    Set filler = ruby.erubyize(Sheet2.Cells(1, 1).Value)
    MsgBox filler.fill_three(ThisWorkbook)
End Sub

fillbyrubyマクロは、ruby.object.1.9(これはRScript19のRubyizeクラスのプログラムID)のerubyizeメソッドへスクリプトを流し込み、返されたオブジェクト(ここでは上に示したスクリプトなのでFillerクラスのインスタンスとなる)のfill_threeメソッドをThisWorkbookを引数として呼び出し、結果をメッセージボックスに表示する。つまり、先に示したスクリプトを実行し、経過時間をメッセージボックスに表示する。

実行結果は、元のxls.rbの2/3以下ではあるが、98.273621秒とまったくふるわなかった。

そこで、再度、呼び出し削減(この場合は内部プロセスサーバ呼び出しのはずだが)修正をかけて、次のスクリプトをシート2のA1に張り付けた。

#coding: cp932
class Filler
  def fill_three(book)
    start = Time.now
    sheet = book.worksheets(1)
    1.upto(10000) do |row|
      ?A.upto(?Z) do |col|
        if row.even?
          sheet.range("#{col}#{row}").value = 3
        else
          sheet.range("#{col}#{row}").value = 'こんにちは'      
        end  
      end
    end
    "Elapsed time: #{Time.now - start} secs"
  end
end
Filler.new

結果は、75.0033秒となり、最初のxls.rbの1/2以下となった(が、おれの期待よりは遅い。1/3になるかと思っていたからだ)。

まとめ

外部プロセス呼び出し回数は可能な限り削減する。そのためにはobj.obj.obj.methodのような記述を避ける。

期待外れとはいえ、実行時間が短縮できるのは事実なので、可能な限り生Ruby.exeを利用した外部プロセス実行ではなく、Rubyizeを利用した内部プロセス実行を利用する。この場合、ExcelマクロとしてXLSファイルへ内包できるという利便性も得られる(もちろん、実行環境にASRがインストールされていることが前提となる)。

(蛇足の考察):それにしても遅いのは、Rangeオブジェクトが次々と生成/廃棄されるのでGCのオーバーヘッドが大きいのではないか、と推測している。

(蛇足の追加):RScriptはRubyとクライアントのスレッドを分離しているから、スレッド間通信(プロセス間ほどではないが相当なオーバーヘッドがある)しているというのを忘れてた。なので、こんなものかと納得した。

本日のツッコミ(全2件) [ツッコミを入れる]
_ jun66j5 (2011-02-09 12:25)

セルを1つずつ設定するのではなくて、1行ずつ(もしくは複数行)設定していけばもっと早く出来ると思います。<br>https://gist.github.com/817825

_ arton (2011-02-09 19:01)

おお、そうですね。サンプルコードありがとうございます。<br>でも上のコードの要点は、外部プロセスサーバ呼び出しは遅いということと、それを減らすことが高速化に繋がることを目に見えるほど遅く、かつ簡単な例で示すことです。したがって、最初に「ある程度遅くなければ意味がないので」と書いてあるように、この例が修正後ですら遅いのはわかりきったことだと思います。


2003|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|04|05|06|07|08|09|10|

ジェズイットを見習え