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 + JRuby な Geotools 利用モジュールを作ってみたいと思いました。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 に追々機能を加えていきたいと思っているところです。