覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Java 8 での java.nio.Files#existsは遅いのか?

Java 7で導入されたNew I/O API(NIO2)。その時に新規クラスとして、java.nio.Files などが増えました。

このクラスで、java.io.File のコードを置き換えることなどが可能です。

一般的には、今はjava.io.Fileクラスではなく、NIO2の機能(java.nio.Filesクラス)を使うのが一般的だと思います。

例えばファイルの存在チェックをする場合は、
旧来は、java.io.File#exists と やっていたものを java.nio.Files#exists で置き換え可能です。

が、Java 8 環境だと java.nio.Files#exists は、対象パスが存在しない場合にパフォーマンスが出ない というバグがあるようです。


JMHでベンチマークとって確認してみます。

ベンチマーク取得用のコードは以下の通りです。
java.io.File#existsjava.nio.Files#exists を、対象ファイルが存在する場合と存在しない場合に分けて実行して、スループット計測しています)

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;

@Warmup(iterations=10)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode(Mode.Throughput)
@Fork(1)
public class FileExistsBenchmark {

    /** 存在するファイルのパス */
    private static final Path existsFilePath = Paths.get("sample1.txt");

    /** 存在しないファイルのパス */
    private static final Path notExistsFilePath = Paths.get("sample999.txt");

    @Benchmark
    public void Files_exists_found() {
        // 存在するファイルに対する Files#exists
        Files.exists(existsFilePath);
    }

    @Benchmark
    public void Files_exists_not_found() {
        // 存在しないファイルに対する Files#exists
        Files.exists(notExistsFilePath);
    }

    @Benchmark
    public void File_exists_found() {
        // 存在するファイルに対する File#exists
        existsFilePath.toFile().exists();
    }

    @Benchmark
    public void File_exists_not_found() {
        // 存在しないファイルに対する File#exists
        notExistsFilePath.toFile().exists();
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(FileExistsBenchmark.class.getSimpleName())
                .build();
        new Runner(opt).run();
    }

}


macOS + Java 8 でのベンチマーク

macOS + Java 8 でのベンチマークは以下の通りとなりました

# JMH version: 1.21
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/bin/java
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=55029:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Warmup: 10 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time

(中略)

Benchmark                                    Mode  Cnt    Score     Error   Units
FileExistsBenchmark.File_exists_found       thrpt    5  377.495 ± 163.927  ops/ms
FileExistsBenchmark.File_exists_not_found   thrpt    5  484.295 ±  74.178  ops/ms
FileExistsBenchmark.Files_exists_found      thrpt    5  871.356 ±  46.111  ops/ms
FileExistsBenchmark.Files_exists_not_found  thrpt    5   91.218 ±  56.958  ops/ms

結果は上から順に以下の実行パターンのスループットを表しています(以降の記載でも同様です)

  • java.io.File#exists + 対象のファイルが存在する
  • java.io.File#exists + 対象のファイルが存在しない
  • java.nio.Files#exists + 対象のファイルが存在する
  • java.nio.Files#exists + 対象のファイルが存在しない


結果から一目瞭然ですが、「java.nio.Files#exists + 対象のファイルが存在しない」のパターンの性能が出ていません。なんてこったい。


macOS + Java 11 でのベンチマーク

このバグは、Java 9 時点で解消されてるらしいです。それならばJava 11でも問題ないはず!ということで、
macOS + Java 11(AdoptOpenJDK) でのベンチマークもとってみました。結果は以下の通りとなりました。

# JMH version: 1.21
# VM version: JDK 11.0.1, OpenJDK 64-Bit Server VM, 11.0.1+13
# VM invoker: /Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.1.jdk/Contents/Home/bin/java
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=56002:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Warmup: 10 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time

(中略)

Benchmark                                    Mode  Cnt    Score    Error   Units
FileExistsBenchmark.File_exists_found       thrpt    5  396.421 ± 40.107  ops/ms
FileExistsBenchmark.File_exists_not_found   thrpt    5  484.622 ± 20.577  ops/ms
FileExistsBenchmark.Files_exists_found      thrpt    5  880.723 ± 17.791  ops/ms
FileExistsBenchmark.Files_exists_not_found  thrpt    5  907.397 ± 26.587  ops/ms

こちらも結果から一目瞭然ですが、「java.nio.Files#exists + 対象のファイルが存在しない」のパターンの性能がちゃんと出ています。
というか、めっちゃ早くなってるじゃないですか笑


macOSでやったついでにWindows環境でも以下試してみました。
今回、macOSとWindowsで端末環境が異なる(別マシン)なので、各OS間で結果(絶対値)を直接的に比較することはできません。


Windows + Java 8 でのベンチマーク

Windows + Java 8 でのベンチマークは以下の通りとなりました

# JMH version: 1.21
# VM version: JDK 1.8.0_191, Java HotSpot(TM) 64-Bit Server VM, 25.191-b12
# VM invoker: C:\Program Files\Java\jdk1.8.0_191\jre\bin\java.exe
# VM options: -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2018.2.5\lib\idea_rt.jar=51996:C:\Program Files\JetBrains\IntelliJ IDEA 2018.2.5\bin -Dfile.encoding=UTF-8
# Warmup: 10 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time

(中略)

Benchmark                                    Mode  Cnt   Score    Error   Units
FileExistsBenchmark.File_exists_found       thrpt    5  41.620 ±  4.666  ops/ms
FileExistsBenchmark.File_exists_not_found   thrpt    5  89.797 ± 15.949  ops/ms
FileExistsBenchmark.Files_exists_found      thrpt    5  14.770 ±  2.707  ops/ms
FileExistsBenchmark.Files_exists_not_found  thrpt    5  18.481 ±  0.886  ops/ms

java.nio.Files#exists の結果が、対象ファイルが存在してても、存在しなくても性能でないという結果になりました。


Windows + Java 11 でのベンチマーク

Windows + Java 11 でのベンチマークは以下の通りとなりました

# JMH version: 1.21
# VM version: JDK 11.0.1, OpenJDK 64-Bit Server VM, 11.0.1+13
# VM invoker: C:\Program Files\Java\OpenJDK11U-jdk_x64_hotspot_11.0.1_13\jdk-11.0.1_13\bin\java.exe
# VM options: -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2018.2.5\lib\idea_rt.jar=49828:C:\Program Files\JetBrains\IntelliJ IDEA 2018.2.5\bin -Dfile.encoding=UTF-8
# Warmup: 10 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time

(中略)

Benchmark                                    Mode  Cnt   Score    Error   Units
FileExistsBenchmark.File_exists_found       thrpt    5  44.025 ±  0.957  ops/ms
FileExistsBenchmark.File_exists_not_found   thrpt    5  86.267 ± 16.522  ops/ms
FileExistsBenchmark.Files_exists_found      thrpt    5  14.424 ±  1.863  ops/ms
FileExistsBenchmark.Files_exists_not_found  thrpt    5  19.877 ±  1.545  ops/ms

あれ?・・・速くなってない。java.nio.Files#existsの結果が、全然速くなってない。
端末のせい?なんかミスったか???謎だ・・・。Windowsだとこういう仕様??


まとめ

macOS環境では、Java 8 では、「java.nio.Files#exists + 対象のファイルが存在しない」パターンが性能が出ないことが明確になりました。
というわけで、Java 8 環境では java.io.File#exists を使った方がいい場合もあるかもしれません。

また、Java 11 では、java.nio.Files#existsは性能が出ており、改善されていることが分かりました。

Windows環境の結果は謎だ・・・。



関連エントリ