Javaのファイルコピー、特に高速化の話でよく見かける、こんなコード。
try (FileChannel srcChannel = new FileInputStream("...").getChannel(); FileChannel destChannel = new FileOutputStream("...").getChannel()) { srcChannel.transferTo(0, srcChannel.size(), destChannel); } catch (IOException e) { e.printStackTrace(); }
これ、実は2Gバイト以上のファイルを1回でコピーできないという罠があるんだそうな。
自分の環境でも試してみましたが、うちでは「2,147,479,552」バイトで壁に当たりましたね。
Bugデータベースにも載っている模様。
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6253145
はい。
というわけで、2Gバイト以上のファイルをコピーする処理を、いくつかのバリエーションを踏まえて書いてみました。
あ、使うのはJDKの標準ライブラリの範囲で、ですよ。
準備
コピー元として、3Gバイトのファイルを作成。
$ dd if=/dev/zero of=large_file_3g bs=1M count=3000 3000+0 レコード入力 3000+0 レコード出力 3145728000 バイト (3.1 GB) コピーされました、 470.924 秒、 6.7 MB/秒 $ ll -h 合計 3.0G drwxrwxr-x 2 xxxxx xxxxx 4.0K Jun 22 23:44 ./ drwxr-xr-x 34 xxxxx xxxxx 4.0K Jun 22 23:39 ../ -rw-rw-r-- 1 xxxxx xxxxx 3.0G Jun 22 23:56 large_file_3g
FileChannel#transferTo(失敗版)
まずは、先の例で出した2Gバイトほどで失敗する版。
public static void badLargeFileCopy() { long startTime = System.currentTimeMillis(); try (FileChannel srcChannel = new FileInputStream("large_file_3g").getChannel(); FileChannel destChannel = new FileOutputStream("copied_file_3g_bad").getChannel()) { srcChannel.transferTo(0, srcChannel.size(), destChannel); } catch (IOException e) { e.printStackTrace(); } }
2Gバイト以下のファイルなら普通にコピーできますが、それ以上になるとうまくコピーできないでしょう。少なくとも、JDK 7ではダメでした。
FileChannel#transferToを繰り返す
1度のFileChannel#transferToで書き込めきれていないのであれば、繰り返せばいいんでしょうか?
public static void betterLargeFileCopy() { try (FileChannel srcChannel = new FileInputStream("large_file_3g").getChannel(); FileChannel destChannel = new FileOutputStream("copied_file_3g_better").getChannel()) { long current = 0L; long size = srcChannel.size(); while (current < size) { long writed = srcChannel.transferTo(current, size, destChannel); current += writed; } } catch (IOException e) { e.printStackTrace(); } }
FileChannel#transferToの戻り値をちゃんと見ているところが、ポイントです。これなら、2Gバイト以上でもうまくコピーできます。
OIOを使う
昔ながらのInputStream/OutputStreamを使った実装。
public static void oioLargeFileCopy() { try (FileInputStream fis = new FileInputStream("large_file_3g"); BufferedInputStream bis = new BufferedInputStream(fis); FileOutputStream fos = new FileOutputStream("copied_file_3g_oio"); BufferedOutputStream bos = new BufferedOutputStream(fos)) { byte[] bytes = new byte[8192]; long writed = 0L; int n = 0; while ((n = bis.read(bytes)) != -1) { bos.write(bytes, 0, n); writed += n; } } catch (IOException e) { e.printStackTrace(); } }
もしくは
try (FileInputStream fis = new FileInputStream("large_file_3g"); BufferedInputStream bis = new BufferedInputStream(fis); FileOutputStream fos = new FileOutputStream("copied_file_3g_oio"); BufferedOutputStream bos = new BufferedOutputStream(fos)) { int b = 0; while ((b = bis.read()) != -1) { bos.write(b); } } catch (IOException e) { e.printStackTrace(); }
2Gバイトの壁はありません。
1つ目のパターンは、BufferedInputStream/OutputStream要る?って気もしますけど…。
あと、2つ目のパターンはかなり遅いです…。
NIO2を使う
最後、JDK 7ならFiles.copyを使えばよいでしょう。2Gバイトを越えても大丈夫です。
public static void nio2LargeFileCopy() { try { Files.copy(Paths.get("large_file_3g"), Paths.get("copied_file_3g_nio2"), StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { e.printStackTrace(); } }
コード量だけなら、今まで出したどれよりも短いです。
なお、Files.copyの実体は
public static Path copy(Path source, Path target, CopyOption... options) throws IOException { FileSystemProvider provider = provider(source); if (provider(target) == provider) { // same provider provider.copy(source, target, options); } else { // different providers CopyMoveHelper.copyToForeignTarget(source, target, options); } return target; }
というように、FileSystemProviderに投げられているので正体不明。
分岐は、コピー先と元で異なるファイルシステムやパーティションだったりした場合かな?
CopyMoveHelper.copyToForeignTargetに、それっぽいところありましたし。
// create directory or copy file if (attrs.isDirectory()) { Files.createDirectory(target); } else { try (InputStream in = Files.newInputStream(source)) { Files.copy(in, target); } }
そして、この場合はStreamを使用したコピーになるんですね。
とりあえず、同じファイルシステム・パーテション上だと、先ほどのFileChannel#transferToを繰り返し適用した時と、同じくらいのパフォーマンスでした。
それにしても、やたらディスクを消費する実験でした…。
今回書いたコード。
FileCopies.java
import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; public class FileCopies { public static void main(String[] args) { oioLargeFileCopy(); badLargeFileCopy(); betterLargeFileCopy(); nio2LargeFileCopy(); } public static void oioLargeFileCopy() { long startTime = System.currentTimeMillis(); try (FileInputStream fis = new FileInputStream("large_file_3g"); BufferedInputStream bis = new BufferedInputStream(fis); FileOutputStream fos = new FileOutputStream("copied_file_3g_oio"); BufferedOutputStream bos = new BufferedOutputStream(fos)) { byte[] bytes = new byte[8192]; long writed = 0L; int n = 0; while ((n = bis.read(bytes)) != -1) { bos.write(bytes, 0, n); writed += n; } } catch (IOException e) { e.printStackTrace(); } /* try (FileInputStream fis = new FileInputStream("large_file_3g"); BufferedInputStream bis = new BufferedInputStream(fis); FileOutputStream fos = new FileOutputStream("copied_file_3g_oio"); BufferedOutputStream bos = new BufferedOutputStream(fos)) { int b = 0; while ((b = bis.read()) != -1) { bos.write(b); } } catch (IOException e) { e.printStackTrace(); } */ long elapsedTime = System.currentTimeMillis() - startTime; try { System.out.println("oioLargeFileCopy, input size => " + String.format("%1$,3d", Files.size(Paths.get("large_file_3g")))); System.out.println("oioLargeFileCopy, output size => " + String.format("%1$,3d", Files.size(Paths.get("copied_file_3g_oio")))); System.out.println("oioLargeFileCopy, elapsed Time => " + String.format("%1$,3d", elapsedTime) + "msec"); } catch (IOException e) { e.printStackTrace(); } } public static void badLargeFileCopy() { long startTime = System.currentTimeMillis(); try (FileChannel srcChannel = new FileInputStream("large_file_3g").getChannel(); FileChannel destChannel = new FileOutputStream("copied_file_3g_bad").getChannel()) { srcChannel.transferTo(0, srcChannel.size(), destChannel); } catch (IOException e) { e.printStackTrace(); } long elapsedTime = System.currentTimeMillis() - startTime; try { System.out.println("badLargeFileCopy, input size => " + String.format("%1$,3d", Files.size(Paths.get("large_file_3g")))); System.out.println("badLargeFileCopy, output size => " + String.format("%1$,3d", Files.size(Paths.get("copied_file_3g_bad")))); System.out.println("badLargeFileCopy, elapsed Time => " + String.format("%1$,3d", elapsedTime) + "msec"); } catch (IOException e) { e.printStackTrace(); } } public static void betterLargeFileCopy() { long startTime = System.currentTimeMillis(); try (FileChannel srcChannel = new FileInputStream("large_file_3g").getChannel(); FileChannel destChannel = new FileOutputStream("copied_file_3g_better").getChannel()) { long current = 0L; long size = srcChannel.size(); while (current < size) { long writed = srcChannel.transferTo(current, size, destChannel); current += writed; } } catch (IOException e) { e.printStackTrace(); } long elapsedTime = System.currentTimeMillis() - startTime; try { System.out.println("betterLargeFileCopy, input size => " + String.format("%1$,3d", Files.size(Paths.get("large_file_3g")))); System.out.println("betterLargeFileCopy, output size => " + String.format("%1$,3d", Files.size(Paths.get("copied_file_3g_better")))); System.out.println("betterLargeFileCopy, elapsed Time => " + String.format("%1$,3d", elapsedTime) + "msec"); } catch (IOException e) { e.printStackTrace(); } } public static void nio2LargeFileCopy() { long startTime = System.currentTimeMillis(); try { Files.copy(Paths.get("large_file_3g"), Paths.get("copied_file_3g_nio2"), StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { e.printStackTrace(); } long elapsedTime = System.currentTimeMillis() - startTime; try { System.out.println("nio2LargeFileCopy, input size => " + String.format("%1$,3d", Files.size(Paths.get("large_file_3g")))); System.out.println("nio2LargeFileCopy, output size => " + String.format("%1$,3d", Files.size(Paths.get("copied_file_3g_nio2")))); System.out.println("nio2LargeFileCopy, elapsed Time => " + String.format("%1$,3d", elapsedTime) + "msec"); } catch (IOException e) { e.printStackTrace(); } } }