Производительность дискового ввода-вывода больших файлов Java - PullRequest
17 голосов
/ 08 июня 2009

У меня на жестком диске два (по 2 ГБ) файла, и я хочу сравнить их друг с другом:

  • Копирование исходных файлов с помощью проводника Windows занимает ок. 2-4 минуты (то есть чтение и запись - на одном физическом и логическом диске).
  • Чтение с java.io.FileInputStream дважды и сравнение байтовых массивов на основе байтов на байт занимает 20+ минут.
  • java.io.BufferedInputStream размер буфера составляет 64 КБ, файлы читаются порциями, а затем сравниваются.
  • Сравнение выполнено в виде замкнутого цикла, подобного

    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?

EDIT:
Спасибо за ответы. Я сделал несколько экспериментов на их основе. Как показал Андреас

потоков или nio подходов мало чем отличаются.
Более важным является правильный размер буфера.

Это подтверждается моими собственными экспериментами. Поскольку файлы читаются большими кусками, даже дополнительные буферы (BufferedInputStream) ничего не дают. Оптимизация сравнения возможна, и я получил лучшие результаты при 32-кратном развертывании, но затраченное на сравнение время невелико по сравнению с чтением с диска, поэтому ускорение мало. Похоже, я ничего не могу поделать; - (

Ответы [ 10 ]

15 голосов
/ 11 июня 2009

Я опробовал три различных метода сравнения двух идентичных файлов по 3,8 ГБ с размерами буфера от 8 КБ до 1 МБ. первый первый метод использовал только два буферизованных входных потока

второй подход использует пул потоков, который читает в двух разных потоках и сравнивает в третьем. Это увеличило пропускную способность за счет высокой загрузки ЦП. управление пулом потоков занимает много времени при выполнении этих кратковременных задач.

третий подход использует 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");
    }
}
7 голосов
/ 08 июня 2009

С такими большими файлами, вы получите НАМНОГО лучшую производительность с java.nio.

Кроме того, чтение отдельных байтов с помощью потоков Java может быть очень медленным. Использование байтового массива (2-6K элементов из моего собственного опыта, ymmv, как кажется, зависит от платформы / приложения) значительно улучшит вашу производительность чтения с помощью потоков.

6 голосов
/ 12 июня 2009

После изменения функции сравнения 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;
}
6 голосов
/ 08 июня 2009

Чтение и запись файлов с помощью Java могут быть такими же быстрыми. Вы можете использовать FileChannels . Что касается сравнения файлов, то, очевидно, это займет много времени при сравнении байта с байтом Вот пример использования FileChannels и ByteBuffers (может быть дополнительно оптимизирован):

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;
}
5 голосов
/ 08 июня 2009

Ниже приводится хорошая статья о сравнительных достоинствах различных способов чтения файла в Java. Может быть полезным:

Как быстро читать файлы

2 голосов
/ 08 июня 2009

Вы можете взглянуть на статью Suns по настройке ввода / вывода (хотя уже немного устаревшую), возможно, вы сможете найти сходство между примерами и вашим кодом. Также обратите внимание на пакет java.nio , который содержит более быстрые элементы ввода / вывода, чем java.io. В журнале Dr. Dobbs Journal есть отличная статья о высокопроизводительном вводе-выводе с использованием java.nio .

Если это так, есть дополнительные примеры и советы по настройке, которые могут помочь вам ускорить ваш код.

Кроме того, в классе Arrays есть методы для сравнения встроенных байтовых массивов , возможно, они также могут быть использованы для ускорения работы и очистки цикла.

1 голос
/ 10 ноября 2013

Я обнаружил, что многие статьи, на которые есть ссылки в этом посте, действительно устарели (есть также некоторые очень проницательные вещи). Есть несколько статей, связанных с 2001 года, и информация в лучшем случае сомнительна. Мартин Томпсон из механического сочувствия написал довольно много об этом в 2011 году. Пожалуйста, обратитесь к тому, что он написал для фона и теории этого.

Я обнаружил, что NIO или нет NIO имеет очень мало общего с производительностью. Это намного больше о размере ваших выходных буферов (прочитайте байтовый массив на этом). NIO - это не волшебство.

Мне удалось взять примеры Мартина, использовать OutputStream эпохи 1.0 и заставить его кричать. NIO тоже быстрый, но самый большой показатель - это только размер выходного буфера, независимо от того, используете ли вы NIO или нет, если, конечно, вы не используете NIO с отображенной памятью, тогда это имеет значение. :)

Если вы хотите получить достоверную информацию об этом, см. Блог Мартина:

http://mechanical -sympathy.blogspot.com / 2011/12 / Java-последовательный ввод-вывод performance.html

Если вы хотите увидеть, как NIO не имеет большого значения (поскольку я смог написать примеры с использованием обычного ввода-вывода, которые были быстрее), посмотрите это:

http://www.dzone.com/links/fast_java_io_nio_is_always_faster_than_fileoutput.html

Я проверил свои предположения на новом ноутбуке с Windows с быстрым жестким диском, моем MacBook Pro с SSD, EC2 xlarge и EC2 4x большой с максимальным IOPS / высокоскоростным вводом / выводом (и вскоре на большом диске Массив оптоволоконных дисков NAS), поэтому он работает (с небольшими экземплярами EC2 есть некоторые проблемы, но если вы заботитесь о производительности ... собираетесь ли вы использовать небольшой экземпляр EC2?). Если вы используете настоящее аппаратное обеспечение, в моих тестах до сих пор традиционный IO всегда побеждает. Если вы используете высокий / IO EC2, то это также явный победитель. Если вы используете инстансы EC2 под напряжением, NIO может победить.

Ничто не заменит бенчмаркинг.

Во всяком случае, я не эксперт, я просто провел некоторое эмпирическое тестирование с использованием фреймворка, который сэр Мартин Томпсон написал в своем блоге.

Я перешел на следующий шаг и использовал Files.newInputStream (из JDK 7) с TransferQueue для создания рецепта для создания кричащего ввода / вывода Java (даже на небольших экземплярах EC2). Рецепт можно найти в нижней части этой документации для Boon (https://github.com/RichardHightower/boon/wiki/Auto-Growable-Byte-Buffer-like-a-ByteBuilder).. Это позволяет мне использовать традиционный OutputStream, но с чем-то, что хорошо работает на небольших экземплярах EC2. (Я главный автор Boon. Но я Я принимаю новых авторов. Оплата отстой. 0 $ в час. Но хорошая новость в том, что я могу удвоить вашу зарплату, когда захотите.)

Мои 2 цента.

Смотрите это, чтобы понять, почему TransferQueue так важно. http://php.sabscape.com/blog/?p=557

Ключевые уроки:

  1. Если вы заботитесь о производительности никогда, никогда, никогда не используйте BufferedOutputStream .
  2. NIO не всегда соответствует производительности.
  3. Размер буфера имеет наибольшее значение.
  4. Переработка буферов для высокоскоростной записи имеет решающее значение.
  5. GC может / будет / действительно подорвет вашу производительность для высокоскоростной записи.
  6. У вас должен быть какой-то механизм для повторного использования отработанных буферов.
1 голос
/ 09 июня 2009

Для лучшего сравнения попробуйте скопировать два файла одновременно. Жесткий диск может читать один файл гораздо эффективнее, чем два (поскольку голова должна двигаться вперед и назад, чтобы прочитать) Один из способов уменьшить это - использовать большие буферы, например, 16 МБ. с помощью ByteBuffer.

С ByteBuffer вы можете сравнивать 8 байтов за раз, сравнивая длинные значения с getLong ()

Если ваша Java эффективна, большая часть работы выполняется на диске / ОС для чтения и записи, поэтому она не должна быть намного медленнее, чем при использовании любого другого языка (так как диск / ОС является узким местом)

Не думайте, что Java работает медленно, пока вы не определили, что это не ошибка в вашем коде.

0 голосов
/ 13 июня 2009

Попробуйте установить буфер во входном потоке до нескольких мегабайт.

0 голосов
/ 08 июня 2009

DMA / SATA - это аппаратные / низкоуровневые технологии и невидимы для любого языка программирования.

Для ввода / вывода с отображением в память вы должны использовать java.nio, я считаю.

Вы уверены, что не читаете эти файлы по одному байту? Это было бы расточительно, я бы рекомендовал делать это по блокам, и каждый блок должен быть размером около 64 мегабайт, чтобы минимизировать поиск.

...