Java ラージ ファイルのディスク IO パフォーマンス
-
12-09-2019 - |
質問
ハードディスク上に 2 つのファイル (それぞれ 2GB) があり、それらを相互に比較したいと考えています。
- Windows エクスプローラーを使用して元のファイルをコピーするには、約 1 分かかります。2 ~ 4 分 (つまり、同じ物理ディスクと論理ディスク上での読み取りと書き込み)。
- 一緒に読む
java.io.FileInputStream
2 回実行し、バイト配列をバイトごとに比較するには 20 分以上かかります。 java.io.BufferedInputStream
バッファーは 64kb で、ファイルはチャンク単位で読み取られて比較されます。比較は次のようなタイトなループで行われます
int numRead = Math.min(numRead[0], numRead[1]); for (int k = 0; k < numRead; k++) { if (buffer[1][k] != buffer[0][k]) { return buffer[0][k] - buffer[1][k]; } }
これを高速化するにはどうすればよいでしょうか?NIO はプレーン ストリームよりも高速であると考えられていますか?Java は DMA/SATA テクノロジを使用できず、代わりに低速な OS-API 呼び出しを実行しますか?
編集:
ご回答ありがとうございます。それらをもとにいくつかの実験をしてみました。アンドレアスが示したように
ストリームまたは
nio
アプローチに大きな違いはありません。
さらに重要なのは、正しいバッファ サイズです。
これは私自身の実験によって確認されています。ファイルは大きな塊で読み取られるため、追加のバッファー (BufferedInputStream
)何も与えないでください。比較の最適化が可能で、32 倍展開で最良の結果が得られましたが、比較にかかる時間がディスク読み取りに比べて小さいため、高速化はわずかです。私にできることは何もないようです;-(
解決
Iは、8キロバイトと1メガバイトの間のバッファサイズを有する2つの同一の3,8 GBのファイルを比較する3つの異なる方法を試してみました。 まず、第1の方法は、ちょうど2つのバッファリングされた入力ストリームを使用する
第2のアプローチは、2つの異なるスレッドを読み込み、第1に比較するスレッドプールを使用します。これは、高いCPU使用率を犠牲にしてわずかに高いスループットを得ました。スレッドプールの管理は、これらの短期実行タスクとオーバーヘッドがかかります。
第三のアプローチが使用NIO、laginimainebによって投稿として
あなたが見ることができるように、、一般的なアプローチはあまり違いはありません。より重要な正しいバッファサイズです。
私は1つのバイトを読ん少ないスレッドを使用していることを不思議なものです。私はタフなエラーを見つけることができませんでした。
comparing just with two streams
I was equal, even after 3684070360 bytes and reading for 704813 ms (4,98MB/sec * 2) with a buffer size of 8 kB
I was equal, even after 3684070360 bytes and reading for 578563 ms (6,07MB/sec * 2) with a buffer size of 16 kB
I was equal, even after 3684070360 bytes and reading for 515422 ms (6,82MB/sec * 2) with a buffer size of 32 kB
I was equal, even after 3684070360 bytes and reading for 534532 ms (6,57MB/sec * 2) with a buffer size of 64 kB
I was equal, even after 3684070360 bytes and reading for 422953 ms (8,31MB/sec * 2) with a buffer size of 128 kB
I was equal, even after 3684070360 bytes and reading for 793359 ms (4,43MB/sec * 2) with a buffer size of 256 kB
I was equal, even after 3684070360 bytes and reading for 746344 ms (4,71MB/sec * 2) with a buffer size of 512 kB
I was equal, even after 3684070360 bytes and reading for 669969 ms (5,24MB/sec * 2) with a buffer size of 1024 kB
comparing with threads
I was equal, even after 3684070359 bytes and reading for 602391 ms (5,83MB/sec * 2) with a buffer size of 8 kB
I was equal, even after 3684070359 bytes and reading for 523156 ms (6,72MB/sec * 2) with a buffer size of 16 kB
I was equal, even after 3684070359 bytes and reading for 527547 ms (6,66MB/sec * 2) with a buffer size of 32 kB
I was equal, even after 3684070359 bytes and reading for 276750 ms (12,69MB/sec * 2) with a buffer size of 64 kB
I was equal, even after 3684070359 bytes and reading for 493172 ms (7,12MB/sec * 2) with a buffer size of 128 kB
I was equal, even after 3684070359 bytes and reading for 696781 ms (5,04MB/sec * 2) with a buffer size of 256 kB
I was equal, even after 3684070359 bytes and reading for 727953 ms (4,83MB/sec * 2) with a buffer size of 512 kB
I was equal, even after 3684070359 bytes and reading for 741000 ms (4,74MB/sec * 2) with a buffer size of 1024 kB
comparing with nio
I was equal, even after 3684070360 bytes and reading for 661313 ms (5,31MB/sec * 2) with a buffer size of 8 kB
I was equal, even after 3684070360 bytes and reading for 656156 ms (5,35MB/sec * 2) with a buffer size of 16 kB
I was equal, even after 3684070360 bytes and reading for 491781 ms (7,14MB/sec * 2) with a buffer size of 32 kB
I was equal, even after 3684070360 bytes and reading for 317360 ms (11,07MB/sec * 2) with a buffer size of 64 kB
I was equal, even after 3684070360 bytes and reading for 643078 ms (5,46MB/sec * 2) with a buffer size of 128 kB
I was equal, even after 3684070360 bytes and reading for 865016 ms (4,06MB/sec * 2) with a buffer size of 256 kB
I was equal, even after 3684070360 bytes and reading for 716796 ms (4,90MB/sec * 2) with a buffer size of 512 kB
I was equal, even after 3684070360 bytes and reading for 652016 ms (5,39MB/sec * 2) with a buffer size of 1024 kB
使用コード:
import junit.framework.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.concurrent.*;
public class FileCompare {
private static final int MIN_BUFFER_SIZE = 1024 * 8;
private static final int MAX_BUFFER_SIZE = 1024 * 1024;
private String fileName1;
private String fileName2;
private long start;
private long totalbytes;
@Before
public void createInputStream() {
fileName1 = "bigFile.1";
fileName2 = "bigFile.2";
}
@Test
public void compareTwoFiles() throws IOException {
System.out.println("comparing just with two streams");
int currentBufferSize = MIN_BUFFER_SIZE;
while (currentBufferSize <= MAX_BUFFER_SIZE) {
compareWithBufferSize(currentBufferSize);
currentBufferSize *= 2;
}
}
@Test
public void compareTwoFilesFutures()
throws IOException, ExecutionException, InterruptedException {
System.out.println("comparing with threads");
int myBufferSize = MIN_BUFFER_SIZE;
while (myBufferSize <= MAX_BUFFER_SIZE) {
start = System.currentTimeMillis();
totalbytes = 0;
compareWithBufferSizeFutures(myBufferSize);
myBufferSize *= 2;
}
}
@Test
public void compareTwoFilesNio() throws IOException {
System.out.println("comparing with nio");
int myBufferSize = MIN_BUFFER_SIZE;
while (myBufferSize <= MAX_BUFFER_SIZE) {
start = System.currentTimeMillis();
totalbytes = 0;
boolean wasEqual = isEqualsNio(myBufferSize);
if (wasEqual) {
printAfterEquals(myBufferSize);
} else {
Assert.fail("files were not equal");
}
myBufferSize *= 2;
}
}
private void compareWithBufferSize(int myBufferSize) throws IOException {
final BufferedInputStream inputStream1 =
new BufferedInputStream(
new FileInputStream(new File(fileName1)),
myBufferSize);
byte[] buff1 = new byte[myBufferSize];
final BufferedInputStream inputStream2 =
new BufferedInputStream(
new FileInputStream(new File(fileName2)),
myBufferSize);
byte[] buff2 = new byte[myBufferSize];
int read1;
start = System.currentTimeMillis();
totalbytes = 0;
while ((read1 = inputStream1.read(buff1)) != -1) {
totalbytes += read1;
int read2 = inputStream2.read(buff2);
if (read1 != read2) {
break;
}
if (!Arrays.equals(buff1, buff2)) {
break;
}
}
if (read1 == -1) {
printAfterEquals(myBufferSize);
} else {
Assert.fail("files were not equal");
}
inputStream1.close();
inputStream2.close();
}
private void compareWithBufferSizeFutures(int myBufferSize)
throws ExecutionException, InterruptedException, IOException {
final BufferedInputStream inputStream1 =
new BufferedInputStream(
new FileInputStream(
new File(fileName1)),
myBufferSize);
final BufferedInputStream inputStream2 =
new BufferedInputStream(
new FileInputStream(
new File(fileName2)),
myBufferSize);
final boolean wasEqual = isEqualsParallel(myBufferSize, inputStream1, inputStream2);
if (wasEqual) {
printAfterEquals(myBufferSize);
} else {
Assert.fail("files were not equal");
}
inputStream1.close();
inputStream2.close();
}
private boolean isEqualsParallel(int myBufferSize
, final BufferedInputStream inputStream1
, final BufferedInputStream inputStream2)
throws InterruptedException, ExecutionException {
final byte[] buff1Even = new byte[myBufferSize];
final byte[] buff1Odd = new byte[myBufferSize];
final byte[] buff2Even = new byte[myBufferSize];
final byte[] buff2Odd = new byte[myBufferSize];
final Callable<Integer> read1Even = new Callable<Integer>() {
public Integer call() throws Exception {
return inputStream1.read(buff1Even);
}
};
final Callable<Integer> read2Even = new Callable<Integer>() {
public Integer call() throws Exception {
return inputStream2.read(buff2Even);
}
};
final Callable<Integer> read1Odd = new Callable<Integer>() {
public Integer call() throws Exception {
return inputStream1.read(buff1Odd);
}
};
final Callable<Integer> read2Odd = new Callable<Integer>() {
public Integer call() throws Exception {
return inputStream2.read(buff2Odd);
}
};
final Callable<Boolean> oddEqualsArray = new Callable<Boolean>() {
public Boolean call() throws Exception {
return Arrays.equals(buff1Odd, buff2Odd);
}
};
final Callable<Boolean> evenEqualsArray = new Callable<Boolean>() {
public Boolean call() throws Exception {
return Arrays.equals(buff1Even, buff2Even);
}
};
ExecutorService executor = Executors.newCachedThreadPool();
boolean isEven = true;
Future<Integer> read1 = null;
Future<Integer> read2 = null;
Future<Boolean> isEqual = null;
int lastSize = 0;
while (true) {
if (isEqual != null) {
if (!isEqual.get()) {
return false;
} else if (lastSize == -1) {
return true;
}
}
if (read1 != null) {
lastSize = read1.get();
totalbytes += lastSize;
final int size2 = read2.get();
if (lastSize != size2) {
return false;
}
}
isEven = !isEven;
if (isEven) {
if (read1 != null) {
isEqual = executor.submit(oddEqualsArray);
}
read1 = executor.submit(read1Even);
read2 = executor.submit(read2Even);
} else {
if (read1 != null) {
isEqual = executor.submit(evenEqualsArray);
}
read1 = executor.submit(read1Odd);
read2 = executor.submit(read2Odd);
}
}
}
private boolean isEqualsNio(int myBufferSize) throws IOException {
FileChannel first = null, seconde = null;
try {
first = new FileInputStream(fileName1).getChannel();
seconde = new FileInputStream(fileName2).getChannel();
if (first.size() != seconde.size()) {
return false;
}
ByteBuffer firstBuffer = ByteBuffer.allocateDirect(myBufferSize);
ByteBuffer secondBuffer = ByteBuffer.allocateDirect(myBufferSize);
int firstRead, secondRead;
while (first.position() < first.size()) {
firstRead = first.read(firstBuffer);
totalbytes += firstRead;
secondRead = seconde.read(secondBuffer);
if (firstRead != secondRead) {
return false;
}
if (!nioBuffersEqual(firstBuffer, secondBuffer, firstRead)) {
return false;
}
}
return true;
} finally {
if (first != null) {
first.close();
}
if (seconde != null) {
seconde.close();
}
}
}
private static boolean nioBuffersEqual(ByteBuffer first, ByteBuffer second, final int length) {
if (first.limit() != second.limit() || length > first.limit()) {
return false;
}
first.rewind();
second.rewind();
for (int i = 0; i < length; i++) {
if (first.get() != second.get()) {
return false;
}
}
return true;
}
private void printAfterEquals(int myBufferSize) {
NumberFormat nf = new DecimalFormat("#.00");
final long dur = System.currentTimeMillis() - start;
double seconds = dur / 1000d;
double megabytes = totalbytes / 1024 / 1024;
double rate = (megabytes) / seconds;
System.out.println("I was equal, even after " + totalbytes
+ " bytes and reading for " + dur
+ " ms (" + nf.format(rate) + "MB/sec * 2)" +
" with a buffer size of " + myBufferSize / 1024 + " kB");
}
}
他のヒント
このような大きなファイルでは、 はるかに優れたパフォーマンスが得られます java.nio。
さらに、Java ストリームでの単一バイトの読み取りは非常に遅くなる可能性があります。バイト配列 (私自身の経験では 2 ~ 6K 要素、プラットフォーム/アプリケーション固有と思われる ymmv) を使用すると、ストリームでの読み取りパフォーマンスが大幅に向上します。
レディングとJavaでファイルを書き込むには、同じように速くすることができます。あなたは FileChannels に使用することができます。 ファイルを比較すると、明らかにこれはバイトにバイトを比較する多くの時間がかかります ここでは(さらに最適化することができる)FileChannelsとたByteBufferを使用した例です。
public static boolean compare(String firstPath, String secondPath, final int BUFFER_SIZE) throws IOException {
FileChannel firstIn = null, secondIn = null;
try {
firstIn = new FileInputStream(firstPath).getChannel();
secondIn = new FileInputStream(secondPath).getChannel();
if (firstIn.size() != secondIn.size())
return false;
ByteBuffer firstBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
ByteBuffer secondBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
int firstRead, secondRead;
while (firstIn.position() < firstIn.size()) {
firstRead = firstIn.read(firstBuffer);
secondRead = secondIn.read(secondBuffer);
if (firstRead != secondRead)
return false;
if (!buffersEqual(firstBuffer, secondBuffer, firstRead))
return false;
}
return true;
} finally {
if (firstIn != null) firstIn.close();
if (secondIn != null) firstIn.close();
}
}
private static boolean buffersEqual(ByteBuffer first, ByteBuffer second, final int length) {
if (first.limit() != second.limit())
return false;
if (length > first.limit())
return false;
first.rewind(); second.rewind();
for (int i=0; i<length; i++)
if (first.get() != second.get())
return false;
return true;
}
以下は、Javaでファイルを読み込むためのさまざまな方法の優劣に良い記事です。いくつかの使用であってもよい:
迅速にファイルを読み取る方法
あなたのNIOコンペア機能を変更した後、私は次のような結果を得ています。
I was equal, even after 4294967296 bytes and reading for 304594 ms (13.45MB/sec * 2) with a buffer size of 1024 kB
I was equal, even after 4294967296 bytes and reading for 225078 ms (18.20MB/sec * 2) with a buffer size of 4096 kB
I was equal, even after 4294967296 bytes and reading for 221351 ms (18.50MB/sec * 2) with a buffer size of 16384 kB
注:このファイルは、37メガバイト/秒
のレートで読み取られていることを意味速いドライブに同じことを実行している。
I was equal, even after 4294967296 bytes and reading for 178087 ms (23.00MB/sec * 2) with a buffer size of 1024 kB
I was equal, even after 4294967296 bytes and reading for 119084 ms (34.40MB/sec * 2) with a buffer size of 4096 kB
I was equal, even after 4294967296 bytes and reading for 109549 ms (37.39MB/sec * 2) with a buffer size of 16384 kB
注:このファイルは74.8メガバイト/秒
のレートで読み取られていることを意味private static boolean nioBuffersEqual(ByteBuffer first, ByteBuffer second, final int length) {
if (first.limit() != second.limit() || length > first.limit()) {
return false;
}
first.rewind();
second.rewind();
int i;
for (i = 0; i < length-7; i+=8) {
if (first.getLong() != second.getLong()) {
return false;
}
}
for (; i < length; i++) {
if (first.get() != second.get()) {
return false;
}
}
return true;
}
ご覧いただけます I/O チューニングに関する Sun の記事 (すでに少し古いですが)、そこにある例とコードの間に類似点が見つかるかもしれません。こちらもご覧ください java.nio java.io よりも高速な I/O 要素を含むパッケージ。博士。Dobbs Journal に非常に素晴らしい記事があります java.nioを使用した高性能IO.
その場合は、コードの高速化に役立つ追加の例とチューニングのヒントがそこにあります。
さらに、Arrays クラスには バイト配列を比較するためのメソッド これらは、処理を高速化し、ループを少し整理するためにも使用できるかもしれません。
より良い比較のために、一度に2つのファイルをコピーしてみてください。ハードドライブは、はるかに効率的に2を読んだのファイルを読むことができる(頭を読み取るために、前後に移動しなければならないとして) これを低減するための一つの方法は、例えば、大きなバッファを使用することです16メガバイトByteBufferのでます。
のByteBufferを使用すると、にGetLong()
との長い値を比較することにより、一度に8バイトを比較することができます あなたのJavaが効率的であるならば、それは他の言語を使用するよりもはるかに遅いではありませんので、、作業のほとんどは、読み取りと書き込みのためのディスク/ OSである(ディスク/ OSがボトルネックであるとして)
あなたのコードでそのないバグを決定するまでJavaは遅いと思ってはいけません。
この投稿にリンクされている記事の多くは非常に古いことがわかりました (非常に洞察力に富んだ記事もいくつかあります)。2001 年からリンクされている記事がいくつかありますが、その情報はよく言っても疑わしいものです。メカニカル・シンパシーのマーティン・トンプソンは、2011 年にこれについてかなり詳しく書きました。この背景と理論については彼が書いたものを参照してください。
NIO かどうかはパフォーマンスとはほとんど関係がないことがわかりました。それは、出力バッファー (そのバッファーの読み取りバイト配列) のサイズに大きく関係します。NIO は Web スケールを高速化する魔法のソースではありません。
Martin の例を参考にして、1.0 時代の OutputStream を使用して、それを叫ばせることができました。NIO も高速ですが、最大の指標は単なる出力バッファのサイズであり、NIO を使用するかどうかではありません。もちろん、メモリ マップされた NIO を使用している場合を除き、それが重要です。:)
これに関する信頼できる最新情報が必要な場合は、Martin のブログを参照してください。
http://mechanical-pathy.blogspot.com/2011/12/java-sequential-io-performance.html
NIO がどのように大きな違いを生まないのかを確認したい場合は (通常の IO を使用した例を書くことができたので、より高速でした)、これを参照してください。
http://www.dzone.com/links/fast_java_io_nio_is_always_faster_than_fileoutput.html
私は、高速ハードディスクを搭載した新しい Windows ラップトップ、SSD を搭載した MacBook Pro、最大 IOPS/高速 I/O を備えた EC2 xlarge、および EC2 4xlarge で私の仮定をテストしました (そして、すぐに大容量ディスク NAS ファイバー ディスクでもテストしました)配列)なので機能します(小規模な EC2 インスタンスではいくつかの問題がありますが、パフォーマンスを重視する場合は...小さな EC2 インスタンスを使用しますか?)。実際のハードウェアを使用する場合、これまでのテストでは、従来の IO が常に勝ちます。高/IO EC2 を使用している場合、これも明らかに勝者です。電力が不足している EC2 インスタンスを使用する場合、NIO が勝つ可能性があります。
ベンチマークに代わるものはありません。
とにかく、私は専門家ではありません。マーティン・トンプソン卿がブログ投稿で書いたフレームワークを使用して、いくつかの実証テストを行っただけです。
これを次のステップに進めて使用しました Files.newInputStream (JDK 7 より) 転送キュー Java I/O を高鳴らせるレシピを作成します (小規模な EC2 インスタンスでも)。レシピは、Boon のこのドキュメントの最後にあります (https://github.com/RichardHightower/boon/wiki/Auto-Growable-Byte-Buffer-like-a-ByteBuilder)。これにより、従来の OutputStream を使用できるようになりますが、より小規模な EC2 インスタンスで適切に動作するものを使用できます。(私はBoonの主な著者です。ただし、新しい作家さんも受け付けています。給料は最悪だ。1時間あたり0ドル。でも良いニュースは、いつでも好きな時に給料を2倍にすることができるということです。)
私の2セント。
その理由については、これを参照してください 転送キュー は重要。 http://php.sabscape.com/blog/?p=557
主な学び:
- パフォーマンスを重視する場合は、決して使用しないでください。 BufferedOutputStream.
- NIO は必ずしもパフォーマンスと同等ではありません。
- バッファ サイズが最も重要です。
- 高速書き込みのためにバッファをリサイクルすることは重要です。
- GC は、高速書き込みのパフォーマンスを破壊する可能性があります/破壊する可能性があります/実際に破壊します。
- 使用済みのバッファを再利用するには、何らかのメカニズムが必要です。
DMA / SATAハードウェア/低レベルtechlonogiesであり、いかなるプログラミング言語には表示されません。
あなたがjava.nioの使用すべきメモリマップされた入力/出力のために、私は信じています。
あなたは1バイトでこれらのファイルを読んでいないことを確認していますか?それは無駄だろう、私はそれをブロックごとに行うことをお勧めしたい、各ブロックが求めて最小化するために64メガバイトのようなものである必要があります。
数メガバイトまでの入力ストリームにバッファを設定してみてください。