CLOVER🍀

That was when it all began.

Javaで2Gバイト以上のサイズのファイルをコピーする

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();
        }
    }
}