レガシーなJavaで書かれたシステムのコードを見ていると、以下のようにInputStreamでファイルを開いて、OutputStreamでコピー先のファイルに書き込むみたいなものがあったりします。
try(InputStream input = new FileInputStream(srcFile);
OutputStream output = new FileOutputStream(dstFile)) {
byte[] buffer = new byte[BUFFER_SIZE];
int size = -1;
while ((size = input.read(buffer)) > 0) {
output.write(buffer, 0, size);
}
}
他にはどういう方法があるのでしょうか。ファイルコピーの歴史が詰まっている、commons-ioの実装の変遷をふりかえり、それぞれの手法のベンチマークをとってみます。
1. メモリ上に全部バッファリングする
2002年にcommons-ioのFileUtilsに、初めてファイルコピーメソッドが誕生したときは、今となってはおぞましい以下のようなコードでした。
public static void fileCopy(String inFileName, String outFileName) throws
Exception
{
String content = FileUtils.fileRead(inFileName);
FileUtils.fileWrite(outFileName, content);
}
今、こんなコードを書こうものならはっ倒されますね…
2. InputStream / OutputStream
前述の方法はあんまりなので、2003年にFileInputStreamでファイルの中身を読み、FileOutputStreamで書き込む、当時のオーソドックス実装に変わったようです。といっても、J2SE1.4は2002年リリースなので、後述のFileChannelが使えた時代ではあるのですが。
https://github.com/apache/commons-io/blob/d9d353082503a217fa6c6510622973d018db0e26/src/java/org/apache/commons/io/FileUtils.java#L604
https://github.com/apache/commons-io/blob/d9d353082503a217fa6c6510622973d018db0e26/src/java/org/apache/commons/io/CopyUtils.java
final byte[] buffer = new byte[bufferSize];
int count = 0;
int n = 0;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
3. FileChannel (NIO)
2008年になってようやくFileChannelが使われるようになったようです。
private static void doCopyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException {
if (destFile.exists() && destFile.isDirectory()) {
throw new IOException("Destination '" + destFile + "' exists but is a directory");
}
FileChannel input = new FileInputStream(srcFile).getChannel();
try {
FileChannel output = new FileOutputStream(destFile).getChannel();
try {
output.transferFrom(input, 0, input.size());
} finally {
IOUtils.closeQuietly(output);
}
} finally {
IOUtils.closeQuietly(input);
}
if (srcFile.length() != destFile.length()) {
throw new IOException("Failed to copy full contents from '" +
srcFile + "' to '" + destFile + "'");
}
if (preserveFileDate) {
destFile.setLastModified(srcFile.lastModified());
}
}
commons-ioの現在のtrunk実装もこれですが、Windowsでラージファイルがコピーできない(IO-175)とのことから、30MBごとにtransferFromをループするようになっています。
4. Files (NIO2)
Java7からは、NIO2になってjava.nio.file.Filesにcopyメソッドができました。なので、(ファイルコピーに関しては)commons-ioとおさらばして、Filesのcopyを直接使うことで同じ効果が期待できます。
Files.copy(source.toPath(), dest, StandardCopyOption.REPLACE_EXISTING);
ベンチマーク
さて、これらのベンチマークをとってみます。ベンチマークのコードは以下にあります。20MBのファイルをコピーを繰り返したときのスループットを比較します。
Linux
Linuxの以下のようなディスク性能のマシンで、
% sync; time bash -c "(dd if=/dev/zero of=bf bs=8k count=500000; sync)"
4096000000 バイト (4.1 GB) コピーされました、 7.00345 秒、 585 MB/秒
次のような結果となりました。
Benchmark Mode Cnt Score Error Units
InputStream2OutputStream.benchmark thrpt 5 1.669 ± 0.055 ops/s
NIOFileChannel.benchmark thrpt 5 1.860 ± 0.113 ops/s
NIO2Files.benchmark thrpt 5 7.110 ± 1.115 ops/s
Filesのコピーメソッドを使うのが、圧倒的に速い結果となります。
Filesが速い理由
Filesのコピーメソッドを辿っていくと、sun.nio.fs.UnixCopyFileで実際コピーしているようです。
private static void copyFile(UnixPath source,
UnixFileAttributes attrs,
UnixPath target,
Flags flags,
long addressToPollForCancel)
throws IOException
{
int fi = -1;
try {
fi = open(source, O_RDONLY, 0);
} catch (UnixException x) {
x.rethrowAsIOException(source);
}
try {
// open new file
int fo = -1;
try {
fo = open(target,
(O_WRONLY |
O_CREAT |
O_EXCL),
attrs.mode());
} catch (UnixException x) {
x.rethrowAsIOException(target);
}
// set to true when file and attributes copied
boolean complete = false;
try {
// transfer bytes to target file
try {
transfer(fo, fi, addressToPollForCancel);
NIOパターンと同じく、ふつうにファイル開いてバッファコピーしてそうですが… openのところをたどると、sun.misc.Unsafeを使って、ネイティブのバッファが使われています。それでこれだけ差がついちゃうようです。
Windows
同じベンチマークをWindowsで流してみました。
Benchmark Mode Cnt Score Error Units
InputStream2OutputStream.benchmark thrpt 5 1.032 ± 0.421 ops/s
NIOFileChannel.benchmark thrpt 5 5.466 ± 9.434 ops/s
NIO2Files.benchmark thrpt 5 3.153 ± 2.775 ops/s
FileChannelとFiles.copyの結果が逆転します。Windowsには疎いので、ちょっと理由までは分かりませんでした…
(Files.copyでは、CopyFileExW
APIが使われており、これがオーバーヘッドかかるのでしょうか?)
まとめ
java.nio.fileのクラスは、java.ioとの互換性が弱いですが、Nativeの機能をふんだんに呼んでくれるので、性能面でメリットが大きいことが多いです。
特に巨大なファイルや多くのファイルを扱う場合は、積極的に使っていくとよいのではないでしょうか。
- Java1.4以上は、FileChannelを使おう。
- Java7以上は、Files.copyを使おう。