Takuya71 のぶろぐ

外資系ソフトウェア会社で働いてます、認定スクラムマスター

Scala で 基本的IOのパフォーマンス比較

Scala で IOのやり方の違いによる ファイルコピー パフォーマンスの比較

「Javaプラットフォームパフォーマンス」の 第4章の「基本的なIO」 にあるプログラムをScalaつかって比較してみました。
IOが遅くなる原因と性能を稼ぐ方法についてを Scala のプログラムでのファイルのコピーを題材に比較を行ってみました。

試した方式は

  • FileInputStream/FileOutputStream を使った方式
  • BufferedInputStream/BufferedOutputStream を使った方式
  • 上記 + カスタムバッファを使った方式
  • Scala IO を使った方式

の4つの方式で比較してみました。4つ目はScala 用の有名どころのライブラリを使った場合 Javaとどれくらい差があるのか を比較したいのでおこなってみました。

プログラム自体は 本のサンプルを参考に Scala 用に変更してみました。
ファイルの使用にあたり scala-arm をつかったローンパターンを使っております。

ことの発端

GW中に近所のブックオフ探索中に 「Javaプラットフォームパフォーマンス」という本を500円で購入した。
かなり 古い本ですが、 パフォーマンスチューニングの考え方として 使える内容だと思います。 500円なので とりあえず買いました。
もちろん Sunの時代の本なので、サンプルコードも 当時のURLから辿れません。
Oracleのサイトでもどこにあるのか、、、 わかりませんでした。

本の中で IOのパフォーマンス改良の章を実際に今時のマシンで試して 差がどれくらいあるのか確認したかった。
あと Scala でやってみたかった。

結論

バッファ使うと早い。 これは 当時も今も変わらない ということがわかった。
カスタムバッファ > Scala IO > BufferedInputStream > > > FileInputStream

Scala IO も 早い!!

最速のものは 一番遅いものに比較して 300倍ぐらいの差があった。
benchmark の一回目は どれも時間がかかっているので 2回目以降で比較すると 4000倍以上の差が数字上あったりする。。。
ググって出てくる コードの多くが バッファ使っているのは
この差を見れば 納得のいく結果。

マシンが速くなって 本の頃よりも 差が開いているようです。 DISKのIOがメモリに比べて圧倒的に遅いってことが実感されます。
SSDのマシンだと 違った結果になるのかな 今度 macbook air で試して見たいと思います。

環境

  • mac mini 2012 モデル CPU: Corei7 2.3Ghz Mem:8GB HDD:1TB
  • OS: Mac OSX 10.8.3
  • scala version: 2.10.1
  • scala-arm: 1.3.0
  • scala-io: 0.4.2

計測方法

約1.8MB の mobi ファイルのコピーをおこないます。 時間の計測には scala の benchmark を使って計測をおこないます。 run 10 と実行し 10回計測し その平均値を出します。 これを3回実施

基本IOでの計測

FileInputStream を使った基本パターン

import scala.testing.Benchmark

object JPPFIS extends Benchmark{

  def copy(from:String, to:String):Unit = {
        import resource._
    for {
        in <- managed(new java.io.FileInputStream(from))
        out <- managed(new java.io.FileOutputStream(to))
      }{
        def loop {
          val readBytes = in.read()
            if(readBytes != -1) {
                out.write(readBytes)
              loop
            }
        }
        loop
      }
  }

    def run() = {
        copy("Testing.mobi","t1.mobi")
    }
    
}

実行結果

[info] Running io.github.takuya71.JPPFIS 10
io.github.takuya71.JPPFIS$  5035    4695    4763    4890    4875    4760    4705    4759    4702    4736


[info] Running io.github.takuya71.JPPFIS 10
io.github.takuya71.JPPFIS$  4737    4695    4788    4814    4785    4840    4966    4847    4794    4998


[info] Running io.github.takuya71.JPPFIS 10
io.github.takuya71.JPPFIS$  5350    5002    4702    4737    4723    4774    4766    4795    4795    4729
  • 1回目 平均 4792 ms
  • 2回目 平均 4826 ms
  • 3回目 平均 4837 ms

約5秒ってことで結構かかってます。

BufferedInputStream を使ったパターン

import scala.testing.Benchmark

object JPPBuf extends Benchmark{
    
    def copy(from:String, to:String):Unit = {
    import resource._
        for {
            in <- managed(new java.io.BufferedInputStream(new java.io.FileInputStream(from)))
            out <- managed(new java.io.BufferedOutputStream(new java.io.FileOutputStream(to)))
        }{
            def loop {
              val readBytes = in.read()
                if(readBytes != -1) {
                    out.write(readBytes)
                  loop
                }
            }
            loop    
        }
  }
    
    def run() = {
        copy("Testing.mobi","t1.mobi")
    }
    
}

実行結果

[info] Running io.github.takuya71.JPPBuf 10
io.github.takuya71.JPPBuf$  63  19  19  19  19  19  19  19  19  20


[info] Running io.github.takuya71.JPPBuf 10
io.github.takuya71.JPPBuf$  36  20  19  19  19  19  19  19  19  20


[info] Running io.github.takuya71.JPPBuf 10
io.github.takuya71.JPPBuf$  36  20  20  19  20  22  19  24  19  20
  • 1回目 平均 24 ms
  • 2回目 平均 21 ms
  • 3回目 平均 22 ms

前のより 約250倍はやい BufferedInputStreamの力は大きい!

BufferedInputStream + Custom Buffer を使ったパターン

import scala.testing.Benchmark

object JPPCustBuf extends Benchmark{
    def copy(from:String, to:String):Unit = {
        import resource._
        for{
            in <- managed(new java.io.BufferedInputStream(new java.io.FileInputStream(from)))
            out <- managed(new java.io.BufferedOutputStream(new java.io.FileOutputStream(to)))
        }{
        val buf = new Array[Byte](8192) // 8192 byte のバッファ
        def loop {
          val readBytes = in.read(buf)
            if(readBytes != -1) {
                out.write(readBytes)
              loop
            }
        }
        loop
      }
    }

    def run() = {
        copy("Testing.mobi","t1.mobi")
    }
    
}

実行結果

[info] Running io.github.takuya71.JPPCustBuf 10
io.github.takuya71.JPPCustBuf$  41  1   1   2   1   2   1   1   1   1


[info] Running io.github.takuya71.JPPCustBuf 10
io.github.takuya71.JPPCustBuf$  12  1   1   2   2   1   1   1   1   1


[info] Running io.github.takuya71.JPPCustBuf 10
io.github.takuya71.JPPCustBuf$  12  1   1   1   1   2   1   1   2   1

やっぱ Buffer 使うと早いです。

  • 1回目 平均 5 ms
  • 2回目 平均 2 ms
  • 3回目 平均 2 ms

シビアな処理の時には バッファ使うべきですね。

Scala IO の copyTo を使ったパターン

import scala.testing.Benchmark
import scalax.file.Path

object JPPScalax extends Benchmark{
  def copy(from:String, to:String) = {

        val in = Path.fromString(from)
        val out = Path.fromString(to)
    in.copyTo(out,replaceExisting=true)
    }

    def run() = {
        copy("Testing.mobi","t1.mobi")
    }
    
}

実行結果

[info] Running io.github.takuya71.JPPScalax 10
io.github.takuya71.JPPScalax$   103 5   6   6   5   5   5   6   5   5


[info] Running io.github.takuya71.JPPScalax 10
io.github.takuya71.JPPScalax$   79  4   4   4   5   7   5   14  5   5


[info] Running io.github.takuya71.JPPScalax 10
io.github.takuya71.JPPScalax$   78  5   5   5   5   7   6   6   4   6

おもったよりも 早いので 性能にシビアな使い方じゃなければ、
使いやすいし 早し、いいと思います。

  • 1回目 平均 15 ms
  • 2回目 平均 13 ms
  • 3回目 平均 13 ms

追加テスト

100MBぐらいのファイルでもおこなってみました。

Scala IO のcopyTo の場合

[info] Running io.github.takuya71.JPPScalax 10
io.github.takuya71.JPPScalax$   2350    1295    1112    978 866 1106    937 996 1147    1080

custom buf の場合

[info] Running io.github.takuya71.JPPCustBuf 10
io.github.takuya71.JPPCustBuf$  81  78  70  61  56  34  39  34  34  34

サイズが大きくなると、結構差がつきますね。
Scala IO でもきつい状況には Buffer 使いましょう。

参考

Javaプラットフォームパフォーマンス―コードレベルのチューニングと開発プロセスへの統合 (The Java Series)

Javaプラットフォームパフォーマンス―コードレベルのチューニングと開発プロセスへの統合 (The Java Series)

  • 作者: スティーブウィルソン,ジェフケセルマン,Steve Wilson,Jeff Kesselman,豊福剛
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2002/07
  • メディア: 単行本
  • クリック: 3回
  • この商品を含むブログを見る