JRuby と CRuby + Rjb の速度の違いにはプリミティブ型クラス (+ String 型) の変換が関係している

geotools.rb を実装しました。

JRuby + Geotools と CRuby + Rjb + Geotools 用に作成した FeatureReader / Writer

http://d.hatena.ne.jp/hfu/20070910/1189455735

Rjbを利用するか、JRubyを利用するかを、環境に依存した特殊な処理と考えることで、差の部分をアプリケーションのメインの処理から分離し、隔離する。

http://arton.no-ip.info/diary/20070911.html#p02

for CRuby + JRubyGeotools 利用モジュールを作ってみたいと思いました。geotools.rb

http://d.hatena.ne.jp/hfu/20070911/1189572966

ということで、geotools.rb を作成してみました。http://d.hatena.ne.jp/hfu/20070910/1189455735#seemore にある FeatureReader, FeatureWriter の機能を、Geo::Reader, Geo::Writer に引き継がせ、CRuby + Rjb と JRuby で同じ挙動をするようにしました(ソースは後述)。

実装詳細

Geo::Tools::String〜Geo::Tools::WKTReader Java からインポートされたクラス。どのクラスがインポートされるかは、Geo::Tools::QUALIFIED_NAMES に書かれています。
Geo::Tools::IMPLEMENTATION Java とのブリッジが何で実現されているかが文字列で示されます。'rjb': Rjb による実現、'java': JRuby による実現
Geo::Reader Shapefile リーダ
Geo::Reader::foreach(filename, sjis_workaround = false) {|geom, attrs| } filename で与えられた Shapefile ファイルを読み、各レコードについて、幾何情報を geom に、属性情報をハッシュ attr にセットして返します。sjis_workaround が true の場合、(現時点では) 属性に含まれる文字列が Shift_JIS である場合に UTF-8 文字列として読めるように工夫します。
Geo::Writer Shapefile ライタ
Geo::Writer::open(filename) {|w| } filename で与えられた Shapefile を作成してブロックに渡します。その後何をすればよいかは w.write を参照。
Geo::Writer::new(filename) 説明省略。通常使う必要はありません。
Geo::Writer::close 説明省略。通常使う必要はありません。
Geo::Writer#write(geom, attrs) JTS Geometry クラスのインスタンスである幾何情報 geom とハッシュ attr の組を、Shapefile ファイルに書き込みます。書き込みはバッファされるようなので、実際には close されるまでは書き込まれないかもしれません。
Geo::import_wkt_geometry WKT (Well-Known Text) 記法で書かれた幾何情報を JTS Geometry クラスのインスタンスに変換します。

CRuby 版については、Shift_JIS コードで書かれた日本語属性も読めるようにしています。JRuby 版については、String が自動的に java.lang.String から ruby::String に変換されてしまので、CRuby 版でできたような工夫ができないため、この機能には未対応です。この工夫の詳細については、別途あとでエントリするかもしれません。
上記の Shift_JIS 対応機能を除けば、ruby geotools.rb でも jruby geotools.rb でも同じ結果が得られるようになりました!

処理速度を確認 - 型変換を合わせることで両者同程度の速度に

ベンチマーク用のデータ(後述)を読んでそのまま別のファイルに出力するという処理を行ったところ、

CRuby + Rjb 版(Shift_JIS 対応処理 on) 220秒
CRuby + Rjb 版(Shift_JIS 対応処理 off) 208秒
JRuby 版(Shift_JIS 対応処理 on) 未実装
JRuby 版(Shift_JIS 対応処理 off) 213秒

となりました。CRuby 版と JRuby 版で、同程度の速度になってしまっています。CRuby + Rjb 版には今回、JRuby 版との互換性を確保するために、属性の型を java.lang.Integer や java.lang.Float などのプリミティブ型クラスから Ruby の Fixnum や Float に変換するようにしたので、これが原因なのではないかと思い調べたところ、

CRuby + Rjb 版(型変換なし) 106秒

となりました。また、http://d.hatena.ne.jp/hfu/20070910/1189455735#seemore にある古い実装に今回のベンチマーク用のデータを入れたところ、

CRuby 版(つまり型変換なし) 109秒
JRuby 版(つまり自動型変換あり) 196秒

となりました。
ベンチマーク用のデータには 99820 ほどレコードがあるので、レコードあたり 2ms、型変換を外して 1ms 程度かかることになります。
私のこのベンチマークに限っては、Rjb の JRuby に対する速度向上は、Java で言うプリミティブ型クラスの自動変換の省略によって得られていると言えそうです。ただし、実用においては CRuby, JRuby 上でもう少しデータ処理を行うはずであり、このデータ処理の速度を見なければどちらが良いとは言えないと考えています。
Rjb では Java で言うプリミティブ型クラスの変換をユーザに任せるという設計にすることで不要な型変換を排除し、それによってパフォーマンスを確保していたのだなあと思いました。

速度を確保するためにどうするべきか?

私の作業では、できればレコードごとの読み書き処理は 1ms 程度以内に完了したいと思っており、速度を確保する工夫を施しておきたいと思っています。
一方で、型変換を省略してしまうと、JRuby と CRuby + Rjb の間で戻り値 attrs の仕様が異なってしまい、互換性確保の観点から不都合です。
そこで、attrs にセットすべき属性の名前のリスト(ホワイトリスト)を Geo::Reader に与えられるようにし、ホワイトリストにない属性の取得と型変換を飛ばせるようにすることにより速度の確保を図りたいと思っています。
考えてみれば、それぞれの処理において使わない属性は多いはずで、使わない属性をデシリアライズする手間を省略するというのは爽快です。

ベンチマーク用のデータ

ベンチマーク用のデータは、今回から「みんなの地球地図プロジェクト」で公開されている「地球地図日本(簡易版)」バージョン1.1を使用することにしました。
http://www.globalmap.org/download/kanni01.html
ここで公開されている、バージョン1.1 の transl.zip にある transl_1_1.shp を使っています。

geotools.rb の現時点のソース

# this code is under development and subject to major change.
require 'iconv'

module Geo
  # Geo::Tools module, to include nesessary classes from Geotools
  module Tools
    QUALIFIED_NAMES = %w{java.lang.String java.lang.Integer java.lang.Double java.lang.Long java.io.File org.geotools.data.shapefile.ShapefileDataStore org.geotools.feature.AttributeTypeFactory org.geotools.feature.FeatureTypeBuilder org.geotools.feature.type.GeometricAttributeType com.vividsolutions.jts.io.WKTReader}
    begin
      require 'rjb'
      QUALIFIED_NAMES.each do |qn|
        sn = qn.split('.').last
        module_eval "#{sn} = Rjb::import('#{qn}')"
      end
      IMPLEMENTATION = 'rjb'
    rescue LoadError
      require 'java'
      QUALIFIED_NAMES.each do |qn|
        include_class qn
      end
      IMPLEMENTATION = 'java'
    end
  end

  # Geo module variables
  @@wkt_reader = Geo::Tools::WKTReader.new

  # Geo module 'good-wrapper' / 'Grossklasstum' classes
  class Reader
    def Reader::foreach(shapefile, sjis_workaround = false)
      if(Tools::IMPLEMENTATION == 'java' && sjis_workaround)
        raise "sjis_workaround for JRuby is not implemented."
      end
      store = Tools::ShapefileDataStore.new(Tools::File.new(shapefile).toURL)
      iter = store.getFeatureSource.getFeatures.features
      feat_type = store.getFeatureSource.getSchema
      attr_names = []
      feat_type.getAttributeCount.times do |i|
        attr_names << feat_type.getAttributeType(i).getName
      end
      while(iter.hasNext)
        feat = iter.next
        attrs = {}
        feat.getNumberOfAttributes.times do |i|
          attr = feat.getAttribute(i)

          if Tools::IMPLEMENTATION == 'rjb'
            if attr.getClass.equals(Tools::Integer)
              attr = attr.intValue
            elsif attr.getClass.equals(Tools::Double)
              attr = attr.doubleValue
            elsif attr.getClass.equals(Tools::String)
              if sjis_workaround
                attr = Iconv.conv('UTF-8', 'Shift_JIS', attr.getBytes('iso-8859-1'))
              else
                attr = attr.toString
              end
            elsif attr.getClass.equals(Tools::Long)
              attr = attr.longValue
            end
          end

          attrs[attr_names[i]] = attr
        end
        attrs.delete('the_geom')
        yield feat.getDefaultGeometry, attrs
      end
      iter.close
    end
  end

  class Writer
    def Writer::open(shapefile)
      w = Writer.new(shapefile)
      yield w
      w.close
    end

    def initialize(shapefile)
      @shapefile = shapefile
      @writer = nil
      @first = true
    end
    
    def setup(geom, attrs)
      attrs.delete('the_geom')
      ftb = Tools::FeatureTypeBuilder.newInstance(@shapefile)
      attrs.each do |key, value|
        if value.methods.include?('_classname')
          attr_class = value.getClass
        elsif value.class == String
          attr_class = Tools::String
        elsif value.class == Fixnum
          attr_class = Tools::Integer
        elsif value.class == Float
          attr_class = Tools::Double
        else
          raise "attribute #{key} has unrecognizable class #{value.class}"
        end
        ftb.addType(Tools::AttributeTypeFactory.newAttributeType(key, attr_class))
      end
      if geom.class == String
        geom = import_wkt_geometry(geom)
      end
      ftb.setDefaultGeometry(Tools::GeometricAttributeType.new('the_geom', geom.getClass, true, nil, nil, nil))
      ft = ftb.getFeatureType
      store = Tools::ShapefileDataStore.new(Tools::File.new(@shapefile).toURL)
      store.createSchema(ft)
      @writer = store.getFeatureWriter(@shapefile, store.getFeatureSource(@shapefile).getTransaction)
      @first = false
    end
    private :setup

    def write(geom, attrs)
      setup(geom, attrs) if @first
      feat = @writer.next
      if geom.class == String
        geom = import_wkt_geometry(geom)
      end
      feat.setDefaultGeometry(geom)
      attrs.each do |key, value|
        feat.setAttribute(key, value)
      end
      @writer.write
    end

    def close
      @writer.close unless @writer == nil
    end
  end

  # Geo module convenient methods
  def Geo::import_wkt_geometry(wkt)
    @@wkt_reader.read(wkt)
  end
end

# ad hoc tests
if __FILE__ == $0
  start_time = Time.now
  Geo::Writer.open('test.shp') do |w|
    Geo::Reader.foreach('transl_1_1.shp', true) do |geom, attrs|
#      p geom.toString
      attrs.each do |key, value|
#        print "#{key} - #{value}\n"
      end
      w.write(geom, attrs)
    end
  end
  print "#{Time.now - start_time} sec."
end

それから

いただいているアクセスの referer にあった Google 検索語から需要が想定される、Geotools による座標変換について、geotools.rb に追々機能を加えていきたいと思っているところです。

ChangeLog

Thu Sep 13 05:47:01 CEST 2007: 誤変換 (書いた気か情報→書いた幾何情報 by ATOK 2007 for Mac) 修正です。すみません。
Thu Sep 13 06:01:15 CEST 2007: タイプミス修正です。すみません。
Fri Sep 14 12:49:44 CEST 2007: プリミティブ型クラスとプリミティブ型が紛らわしい文章になっていたので、そのあたりの表現を修正しました。