Вопрос

У меня есть два файла (по 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?

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

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

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

Это было полезно?

Решение

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

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

третий подход использует 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-6 тысяч элементов из моего собственного опыта, ymmv, поскольку это зависит от платформы / приложения) значительно улучшит вашу производительность чтения с потоками.

Чтение и запись файлов с помощью Java может быть таким же быстрым.Вы можете использовать Файловые каналы.Что касается сравнения файлов, очевидно, что это займет много времени, сравнивая байт с байтом Вот пример использования 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;
}

Ниже приведена хорошая статья об относительных достоинствах различных способов чтения файла в 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;
}

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

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

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

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

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

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

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

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

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

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

Если вам нужна актуальная авторитетная информация по этому поводу, смотрите блог Мартина:

http://mechanical-sympathy .blogspot.com/2011/12/java-sequential-io-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 large с максимальными IOPS / высокоскоростным вводом-выводом (и вскоре на большом диске NAS fibre disk array), так что это работает (есть некоторые проблемы с ним для небольших экземпляров EC2, но если вы заботитесь о производительности...собираетесь ли вы использовать небольшой экземпляр EC2?).Если вы используете реальное оборудование, то в моих тестах до сих пор традиционный ввод-вывод всегда выигрывал.Если вы используете high / IO EC2, то это тоже явный победитель.Если вы используете экземпляры EC2 с недостаточным питанием, NIO может выиграть.

Бенчмаркинг ничем не заменить.

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

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

Мои 2 цента.

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top