左のフレームがないっていう人はこちら


1/20  【java】MemoryMappedFileの薦め【バイナリ全走査】

新年明けましておめでとうございます<(_ _)>

せっかく(?)年も明けたので、すごく久しぶりにプログラム的な話題を。

実は趣味で作ってるプログラムがあるんですよ。
ごくたま〜〜〜に自分で使ったりして。

概要的には100MB程度のファイルを中身を全走査するようなプログラムなんですね。
で、これが作りが悪いせいでものっそい遅い!
100MB程度のファイルを調べ終わるのに5分10分かかっちゃう!

今までは、まぁ別の作業の片手間にやっておくかぁ(´ー`) なんて思って、不便さを感じながらもそのままにしておいたんですが、つい先日、せっかく(?)年も明けたのだからと一念発起してプログラム高速化プロジェクトを立ち上げました。


そうして紆余曲折の果てにたどり着いた情報がこれ!
ランダムアクセスファイルを使ってバイナリサーチ
そんなときには MemoryMappedFile を使うのが常套手段です。

java.nio.channels.FileChannel#map メソッドでファイルをマップすることができます。
マップする領域もヒープではなく、JNI で直接メモリを確保するので、ヒープ領域のサイズを気にすることもありません。

また、マップした後は java.nio.ByteBuffer として扱えるので、RandomAccessFileに比べたら検索のスピードは段違いです。 
		

キタ━━━━━(゚∀゚)━━━━━!!


ちなみにメモリマップドファイルの詳細はここを見るとわかりやすいかも。
項目(メモリマップトファイルとは)のあたり。
複数プロセス間でのファイル操作にも便利そうですが、今回は『高速化』に焦点を置きます。

javaAPI(FileChannel)より
・ファイルの領域はメモリーに直接マッピングされる。ファイルのサイズが大きい場合は、通常の read メソッドや write メソッドを呼び出すより、この方法のほうが効率的


以上を踏まえて、早速検証したいと思います。

バイナリ全走査
マシン
  Celeron1.7GHz
  メモリ:512MB

以下、計測用ソース。
try{
	File file = new File("C:/40M.file");
	//File file = new File("C:/1M.file");
	
	//FileChannelの作成
	RandomAccessFile raf = new RandomAccessFile(file,"r");
	FileInputStream fis = new FileInputStream(file);
	FileChannel fch = raf.getChannel();
	
	//memoryMappingの作成 マッピングモード,position,size
	MappedByteBuffer mapbuf = fch.map(MapMode.READ_ONLY, 0, raf.length());
	
	//読み込みテスト
	long start,end;
	
	//RandomAccessFile
	//1byteずつ読み込み。
	System.out.println("***** RandomAccess");
	System.out.println("\t0 → "+raf.length());
	start = System.currentTimeMillis();
	while(raf.getFilePointer() < raf.length())
		raf.read();
	end = System.currentTimeMillis();
	System.out.println("\t 掛かった時間:"+(end - start)+"ms.");
	System.out.println("*****");
	
	//あらかじめ読み込みファイルサイズ分のbyte配列に値を格納してから走査
	System.out.println("***** Bytes");
	System.out.println("\t0 → "+mapbuf.capacity());
	start = System.currentTimeMillis();
	byte bytes[] = new byte[fis.available()];
	fis.read(bytes);
	for(int cnt=0, i=0;cnt < bytes.length;cnt++)
		i = bytes[cnt];
	end = System.currentTimeMillis();
	System.out.println("\t 掛かった時間:"+(end - start)+"ms.");
	System.out.println("*****");
	
	//MappedByteBuffer
	//メモリ領域を直接走査
	System.out.println("***** MappedByteBuffer");
	System.out.println("\t0 → "+mapbuf.capacity());
	start = System.currentTimeMillis();
	while(mapbuf.position() < mapbuf.capacity())
		mapbuf.get();
	end = System.currentTimeMillis();
	System.out.println("\t 掛かった時間:"+(end - start)+"ms.");
	System.out.println("*****");		
	
	//close
	fch.close();
	fis.close();
	raf.close();
	
	}catch(Exception e){
		e.printStackTrace();
	}

以下、結果。

約1MBのファイル
***** RandomAccess
  0 → 1059328
  掛かった時間:38812ms.
*****
***** Bytes
  0 → 1059328
  掛かった時間:31ms.
*****
***** MappedByteBuffer
  0 → 1059328
  掛かった時間:63ms.
*****

約40MBのファイル
***** RandomAccess
  0 → 41874944
  掛かった時間:1289328ms.
*****
***** Bytes
  0 → 41874944
  掛かった時間:8609ms.
*****
***** MappedByteBuffer
  0 → 41874944
  掛かった時間:1391ms.
*****

以上の結果から言えることは、
 ・小さいサイズのファイルならば、直接バイト配列でも作って読み込んだほうが早い。
 ・サイズが大きくなればなるほど、マッピングのほうが優位になる。
 ・RandomAccessFileは論外だった。
ということですかね。

まぁマシンスペックに激しく左右されるので計測時間自体は気にしないで欲しいのですが、やたら遅いこのマシンでこの結果なのでMemoryMappedFileの価値は揺らがないかと思われます。

あと、上記のソースのように単純に読み込むように作ると読み込み部分でファイルサイズ分のメモリ消費は避けられないので、巨大ファイルを扱いたいときは何か一工夫必要だと思います。


まぁ何はともあれ、無事目的を達成出来たのでよかったです。(´∀`)

たまーにがんばると楽しいなぁ……。