107
101

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

システムエンジニアAdvent Calendar 2015

Day 11

Javaでのファイルコピー史

Last updated at Posted at 2015-12-11

レガシーな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. :snail: メモリ上に全部バッファリングする

2002年にcommons-ioのFileUtilsに、初めてファイルコピーメソッドが誕生したときは、今となってはおぞましい以下のようなコードでした。

FileUtils.java
public static void fileCopy(String inFileName, String outFileName) throws
    Exception
{
    String content = FileUtils.fileRead(inFileName);
    FileUtils.fileWrite(outFileName, content);
}

今、こんなコードを書こうものならはっ倒されますね…

2. :turtle: 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

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. :racehorse: FileChannel (NIO)

2008年になってようやくFileChannelが使われるようになったようです。

FileUtils.java
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. :red_car: 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では、CopyFileExWAPIが使われており、これがオーバーヘッドかかるのでしょうか?)

まとめ

java.nio.fileのクラスは、java.ioとの互換性が弱いですが、Nativeの機能をふんだんに呼んでくれるので、性能面でメリットが大きいことが多いです。
特に巨大なファイルや多くのファイルを扱う場合は、積極的に使っていくとよいのではないでしょうか。

  • Java1.4以上は、FileChannelを使おう。
  • Java7以上は、Files.copyを使おう。
107
101
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
107
101

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?