Самый быстрый способ суммирования целых чисел в текстовом файле

Вопрос

Предположим, что у вас есть большой текстовый файл ASCII со случайным неотрицательным целым числом в каждой строке, каждый в диапазоне от 0 до 1 000 000 000. В файле имеется 100 000 000 строк. Какой самый быстрый способ прочитать файл и вычислить сумму всех целых чисел?

Ограничение: у нас есть 10 МБ ОЗУ для работы. Файл имеет размер 1 ГБ, поэтому мы не хотим читать все это и обрабатывать его.

Вот несколько решений, которые я пробовал. Я нашел результаты довольно неожиданными.

Есть ли что-то быстрее, чем я пропустил?

Обратите внимание: все тайминги, приведенные ниже, предназначены для запуска алгоритма 10 раз в целом (запускать один раз и отбрасывать, запускать таймер, запускать 10 раз, таймер остановки). Машина довольно медленная Core 2 Duo.

Метод 1: естественный подход

Первое, что нужно попробовать, - это очевидный подход:

private long sumLineByLine() throws NumberFormatException, IOException {
    BufferedReader br = new BufferedReader(new FileReader(file));
    String line;
    long total = 0;
    while ((line = br.readLine()) != null) {
        int k = Integer.parseInt(line);
        total += k;
    }
    br.close();
    return total;
}

Обратите внимание, что максимально возможное возвращаемое значение равно 10 ^ 17, которое все еще легко вписывается в long, поэтому нам не нужно беспокоиться о переполнениях.

На моей машине запуск этого 11 раз и дисконтирование первого запуска занимает около 92,9 секунды.

Способ 2: незначительная настройка

Вдохновленный комментарием этот вопрос, я попробовал не создавать новый int k для хранения результата разбора строки, а вместо этого просто добавить синтаксический анализ прямо на total. Итак:

    while ((line = br.readLine()) != null) {
        int k = Integer.parseInt(line);
        total += k;
    }

становится следующим:

    while ((line = br.readLine()) != null)
        total += Integer.parseInt(line);

Я был уверен, что это не будет иметь никакого значения, и подумал, что очень вероятно, что компилятор будет генерировать один и тот же байт-код для двух версий. Но, к моему удивлению, он немного побрился: мы до 92,1 секунды.

Способ 3: ручное разбор целого числа

Одна вещь, которая беспокоит меня по поводу кода, заключается в том, что мы превращаем String в int, а затем добавляем его в конец. Не может быть проще добавить, когда мы идем? Что произойдет, если мы сами проанализируем String? Что-то вроде этого...

private long sumLineByLineManualParse() throws NumberFormatException,
        IOException {
    BufferedReader br = new BufferedReader(new FileReader(file));
    String line;
    long total = 0;
    while ((line = br.readLine()) != null) {
        char chs[] = line.toCharArray();
        int mul = 1;
        for (int i = chs.length - 1; i >= 0; i--) {
            char c = chs[i];
            switch (c) {
            case '0':
                break;
            case '1':
                total += mul;
                break;
            case '2':
                total += (mul << 1);
                break;
            case '4':
                total += (mul << 2);
                break;
            case '8':
                total += (mul << 3);
                break;
            default:
                total += (mul*((byte) c - (byte) ('0')));   
            }
            mul*=10;
        }
    }
    br.close();
    return total;
}

Это, я думал, может сэкономить немного времени, особенно с некоторыми оптимизациями бит-брейка для выполнения умножения. Но накладные расходы на преобразование в массив символов должны увеличивать прибыль: теперь требуется 148,2 секунды.

Способ 4: обработка в двоичном формате

Последнее, что мы можем попробовать - это обработать файл как двоичные данные.

Разбор целого числа с фронта неудобен, если вы не знаете его длины. Разборки в обратном направлении намного проще: первая цифра, с которой вы сталкиваетесь, - это единицы, а следующая - десятки и так далее. Таким образом, самый простой способ приблизиться ко всему этому - прочитать файл назад.

Если мы выделяем буфер byte[] (скажем) 8 МБ, мы можем заполнить его последним 8 МБ файла, обработать его, затем прочитать предыдущие 8 МБ и т.д. Нам нужно быть немного осторожным, чтобы мы не испортили число, которое мы находимся в середине разбора, когда переходим к следующему блоку, но это единственная проблема.

Когда мы сталкиваемся с цифрой, добавим ее (соответственно умножить в соответствии с ее положением в цифре) на общую сумму, а затем умножим коэффициент на 10, чтобы мы были готовы к следующей цифре. Если мы сталкиваемся с чем-либо, что не является цифрой (CR или LF), мы просто reset коэффициент.

private long sumBinary() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int lastRead = (int) raf.length();
    byte buf[] = new byte[8*1024*1024];
    int mul = 1;
    long total = 0;
    while (lastRead>0) {
        int len = Math.min(buf.length, lastRead);
        raf.seek(lastRead-len);
        raf.readFully(buf, 0, len);
        lastRead-=len;
        for (int i=len-1; i>=0; i--) {
            //48 is '0' and 57 is '9'
            if ((buf[i]>=48) && (buf[i]<=57)) {
                total+=mul*(buf[i]-48);
                mul*=10;
            } else
                mul=1;
        }
    }
    raf.close();
    return total;
}

Это работает в 30,8 секунд! То, что скорость возрастает в 3 раза по сравнению с предыдущим.

Последующие вопросы

  • Почему это так быстрее? Я ожидал, что он победит, но не настолько впечатляюще. Это главным образом накладные расходы на преобразование в String? И все беспокойство за кулисами о наборах символов и тому подобное?
  • Можем ли мы сделать это лучше, используя MappedByteBuffer, чтобы помочь? У меня такое ощущение, что накладные расходы при вызове методов для чтения из буфера замедлят работу, особенно при чтении назад из буфера.
  • Было бы лучше читать файл вперед, а не назад, но все же сканировать буфер назад? Идея состоит в том, что вы читаете первый фрагмент файла, а затем сканируете назад, но отбрасываете половину номера в конце. Затем, когда вы читаете следующий фрагмент, вы устанавливаете смещение так, чтобы вы читали с начала номера, который вы отбрасывали.
  • Есть ли что-нибудь, о чем я не думал, что может иметь существенное значение?

Обновление: более неожиданные результаты

Во-первых, наблюдение. Это должно было произойти раньше, но я думаю, что причина неэффективности чтения String - это не столько время, чтобы создать все объекты String, но и тот факт, что они настолько недолговечны: у нас есть 100 000 000 из них для сборщика мусора. Это должно нарушить его.

Теперь некоторые эксперименты, основанные на ответах/комментариях, опубликованных людьми.

Я обманываю с размером буфера?

Одно из предложений заключалось в том, что поскольку BufferedReader использует буфер по умолчанию 16 Кбайт, и я использовал буфер размером 8 МБ, я не сравниваюсь с подобным. Он должен быть быстрее, если вы используете больший буфер.

Вот удар. Метод sumBinary() (метод 4) вчера заработал за 30,8 секунды с буфером 8 МБ. Сегодня код не изменился, направление ветра изменилось, и мы находимся на 30,4 секунды. Если я сброшу размер буфера до 16 КБ, чтобы увидеть, насколько он медленнее, он становится быстрее! Теперь он работает в 23,7 секунды. Псих. Кто видел, что кто-то пришел?!

Немного экспериментов предполагает, что 16 КБ оптимально. Возможно, ребята из Java сделали те же эксперименты, и почему они пошли с 16 КБ!

Является ли проблема связью ввода/вывода?

Я тоже подумал об этом. Сколько времени тратится на доступ к диску и сколько на хруст числа? Если это почти весь доступ к диску, как было предложено хорошо поддержанным комментарием к одному из предложенных ответов, то мы не сможем сделать много улучшения, что бы мы ни делали.

Это легко проверить, запустив код, когда все синтаксические разборки и хруст числа комментируются, но при этом чтение все еще не повреждено:

private long sumBinary() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int lastRead = (int) raf.length();
    byte buf[] = new byte[16 * 1024];
    int mul = 1;
    long total = 0;
    while (lastRead > 0) {
        int len = Math.min(buf.length, lastRead);
        raf.seek(lastRead - len);
        raf.readFully(buf, 0, len);
        lastRead -= len;
        /*for (int i = len - 1; i >= 0; i--) {
            if ((buf[i] >= 48) && (buf[i] <= 57)) {
                total += mul * (buf[i] - 48);
                mul *= 10;
            } else
                mul = 1;
        }*/
    }
    raf.close();
    return total;
}

Теперь это выполняется в 3,7 секунды! Это не выглядит привязанным к I/O.

Конечно, некоторая скорость ввода-вывода будет поступать из обращений в кеш диска. Но на самом деле это не так: мы все еще занимаем 20 секунд времени процессора (также подтверждается с помощью команды Linux time), которая достаточно велика, чтобы попытаться уменьшить ее.

Сканирование вперед, а не назад

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

private long sumBinaryForward() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int fileLength = (int) raf.length();
    byte buf[] = new byte[16 * 1024];
    int acc = 0;
    long total = 0;
    int read = 0;
    while (read < fileLength) {
        int len = Math.min(buf.length, fileLength - read);
        raf.readFully(buf, 0, len);
        read += len;
        for (int i = 0; i < len; i++) {
            if ((buf[i] >= 48) && (buf[i] <= 57))
                acc = acc * 10 + buf[i] - 48;
            else {
                total += acc;
                acc = 0;
            }
        }
    }
    raf.close();
    return total;
}

Это выполняется в 20.0 секунд, удаляя версию обратного сканирования на расстояние. Ницца.

Кэш умножения

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

Дело в том, что в числах, которые мы сканируем, не более 10 цифр и всего 10 возможных цифр, поэтому только 100 значений для значения цифры составляют совокупную сумму. Мы можем прекомпилировать их, а затем использовать их в коде обратного сканирования. Это должно превзойти версию форвардного сканирования, потому что теперь мы полностью избавились от умножений. (Обратите внимание, что мы не можем сделать это с помощью прямого сканирования, потому что умножение происходит от аккумулятора, который может принимать любое значение до 10 ^ 9. Это только в обратном случае, что оба операнда ограничены несколькими возможностями.)

private long sumBinaryCached() throws IOException {
    int mulCache[][] = new int[10][10];
    int coeff = 1;
    for (int i = 0; i < 10; i++) {
        for (int j = 0; j < 10; j++)
            mulCache[i][j] = coeff * j;
        coeff *= 10;
    }

    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int lastRead = (int) raf.length();
    byte buf[] = new byte[16 * 1024];
    int mul = 0;
    long total = 0;
    while (lastRead > 0) {
        int len = Math.min(buf.length, lastRead);
        raf.seek(lastRead - len);
        raf.readFully(buf, 0, len);
        lastRead -= len;
        for (int i = len - 1; i >= 0; i--) {
            if ((buf[i] >= 48) && (buf[i] <= 57))
                total += mulCache[mul++][buf[i] - 48];
            else
                mul = 0;
        }
    }
    raf.close();
    return total;
}

Это выполняется в 26,1 секунды. Разочаровывать, если не сказать больше. Чтение назад менее эффективно с точки зрения ввода-вывода, но мы видели, что I/O не является главной головной болью здесь. Я ожидал, что это будет иметь большое положительное значение. Возможно, поиск массивов столь же дорог, как и умножения, которые мы заменили. (Я попытался сделать массив 16x16 и использовать бит-строки для индексации, но это не помогло.)

Похоже, что прямое сканирование находится там, где оно находится.

Использование MappedByteBuffer

Следующее, что нужно добавить, - это MappedByteBuffer, чтобы убедиться, что это более эффективно, чем использование raw RandomAccessFile. Это не нуждается в большом изменении кода.

private long sumBinaryForwardMap() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    byte buf[] = new byte[16 * 1024];
    final FileChannel ch = raf.getChannel();
    int fileLength = (int) ch.size();
    final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, 0,
            fileLength);
    int acc = 0;
    long total = 0;
    while (mb.hasRemaining()) {
        int len = Math.min(mb.remaining(), buf.length);
        mb.get(buf, 0, len);
        for (int i = 0; i < len; i++)
            if ((buf[i] >= 48) && (buf[i] <= 57))
                acc = acc * 10 + buf[i] - 48;
            else {
                total += acc;
                acc = 0;
            }
    }
    ch.close();
    raf.close();
    return total;
}

Кажется, это немного улучшило ситуацию: теперь мы находимся в 19.0 секунд. Мы взяли еще одну секунду с нашего личного успеха!

Как насчет многопоточности?

Один из предложенных ответов включает использование нескольких ядер. Мне немного стыдно, что это не пришло ко мне!

Ответ пришел для какой-то палки из-за предположения, что это проблема с привязкой к I/O. Это кажется немного суровым, в свете результатов о I/O! Конечно, стоит попробовать, в любом случае.

Мы сделаем это, используя fork/join. Здесь класс, представляющий результат вычисления на части файла, имея в виду, что слева может быть частичный результат (если мы начали половину пути через число), а частичный результат вправо (если буфер завершен на полпути через номер). У класса также есть метод, позволяющий нам склеить два таких результата вместе, в комбинированный результат для двух смежных подзадач.

private class SumTaskResult {
    long subtotal;
    int leftPartial;
    int leftMulCount;
    int rightPartial;

    public void append(SumTaskResult rightward) {
        subtotal += rightward.subtotal + rightPartial
                * rightward.leftMulCount + rightward.leftPartial;
        rightPartial = rightward.rightPartial;
    }
}

Теперь бит ключа: RecursiveTask, который вычисляет результат. Для небольших проблем (менее 64 символов) он вызывает computeDirectly() для вычисления результата в одном потоке; для больших задач он разбивается на две части, решает две подзадачи в отдельных потоках и затем объединяет результаты.

private class SumForkTask extends RecursiveTask<SumTaskResult> {

    private byte buf[];
    // startPos inclusive, endPos exclusive
    private int startPos;
    private int endPos;

    public SumForkTask(byte buf[], int startPos, int endPos) {
        this.buf = buf;
        this.startPos = startPos;
        this.endPos = endPos;
    }

    private SumTaskResult computeDirectly() {
        SumTaskResult result = new SumTaskResult();
        int pos = startPos;

        result.leftMulCount = 1;

        while ((buf[pos] >= 48) && (buf[pos] <= 57)) {
            result.leftPartial = result.leftPartial * 10 + buf[pos] - 48;
            result.leftMulCount *= 10;
            pos++;
        }

        int acc = 0;
        for (int i = pos; i < endPos; i++)
            if ((buf[i] >= 48) && (buf[i] <= 57))
                acc = acc * 10 + buf[i] - 48;
            else {
                result.subtotal += acc;
                acc = 0;
            }

        result.rightPartial = acc;
        return result;
    }

    @Override
    protected SumTaskResult compute() {
        if (endPos - startPos < 64)
            return computeDirectly();
        int mid = (endPos + startPos) / 2;
        SumForkTask left = new SumForkTask(buf, startPos, mid);
        left.fork();
        SumForkTask right = new SumForkTask(buf, mid, endPos);
        SumTaskResult rRes = right.compute();
        SumTaskResult lRes = left.join();
        lRes.append(rRes);
        return lRes;
    }

}

Обратите внимание, что это работает на byte[], а не на целом MappedByteBuffer. Причина этого в том, что мы хотим сохранить доступ к диску последовательным. Мы возьмем довольно большие куски, вилку/соединение, а затем перейдем к следующему фрагменту.

Здесь используется метод, который делает это. Обратите внимание, что мы увеличили размер буфера до 1 Мбайт (субоптимальный ранее, но более разумный здесь, похоже).

private long sumBinaryForwardMapForked() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    ForkJoinPool pool = new ForkJoinPool();

    byte buf[] = new byte[1 * 1024 * 1024];
    final FileChannel ch = raf.getChannel();
    int fileLength = (int) ch.size();
    final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, 0,
            fileLength);
    SumTaskResult result = new SumTaskResult();
    while (mb.hasRemaining()) {
        int len = Math.min(mb.remaining(), buf.length);
        mb.get(buf, 0, len);
        SumForkTask task = new SumForkTask(buf, 0, len);
        result.append(pool.invoke(task));
    }
    ch.close();
    raf.close();
    pool.shutdown();
    return result.subtotal;
}

Теперь вот душераздирающее разочарование: этот красиво многопоточный код теперь занимает 32,2 секунды. Почему так медленно? Я довольно долго отлаживал это, полагая, что сделал что-то ужасное.

Оказывается, требуется только одна небольшая настройка. Я думал, что порог 64 между небольшой проблемой и большой проблемой был разумным; оказывается, это было совершенно нелепо.

Подумайте об этом так. Суб-проблемы имеют одинаковый размер, поэтому они должны выполняться почти в одно и то же время. Таким образом, на самом деле нет смысла разбивать на куски больше, чем есть доступные процессоры. На машине, которую я использую, только с двумя ядрами, спустившись до порога 64, смешно: это просто добавляет дополнительные накладные расходы.

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

В любом случае, если я изменю порог на 512 КБ (половина размера буфера), он теперь завершается в 13,3 секунды. Переход на 128 КБ или 64 КБ позволит использовать больше ядер (до 8 или 16 соответственно) и не оказывает существенного влияния на время выполнения.

Значит, многопоточность делает.

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

Хорошая работа дня.

Полагаю, я должен, вероятно, попробовать использовать следующий GPU...

Postscript: генерация файла случайных чисел

Я создал случайные числа со следующим кодом, который я запускал и перенаправлял в файл. Очевидно, я не могу гарантировать, что у вас будут точно такие же случайные числа, которые у меня были:)

public static void genRandoms() {
    Random r = new Random();
    for (int i = 0; i < 100000000; i++)
        System.out.println(r.nextInt(1000000000));
}
public static void genRandoms() {
    Random r = new Random();
    for (int i = 0; i < 100000000; i++)
        System.out.println(r.nextInt(1000000000));
}

Ответы

Ответ 1

Я думаю, что есть и другой способ сделать это.

Это классическая проблема многопроцессного программирования. На языке C имеется библиотека MPI, которая решает эти проблемы.

Идея состоит в том, чтобы вырезать список целых чисел, например, в 4 частях, и каждая часть суммируется с помощью другого процесса. После завершения процессов суммируются вместе.

В java это можно сделать с помощью потоков (псевдопараллельно) и java concurrency.

Например, 4 разных потока, суммирующих 4 разные части списка. В конце они суммируются вместе.

Телефонные компании используют Grid Computers, которые используют эту технику параллельного программирования для суммирования своих транзакций.

Единственная проблема здесь (узкое место) - операция ввода-вывода. Чтение файла займет много времени. Если каким-то образом вы можете сделать несколько потоков, прочитайте разные части файла... Это очень сложный подход, и я думаю, что это не принесет много пользы, потому что диск не будет вращаться быстрее только потому, что он используется многими потоками, но есть и другая техника для создания подобных вещей. Подробнее об этом можно узнать здесь: Доступ к файлу через несколько потоков и здесь Чтение одного файла с несколькими потоками: необходимо ускорить?

Ответ 2

Основным узким местом будет файл IO. Анализ и сложение номеров не должны вносить вклад в алгоритм, поскольку это может быть сделано в отдельном потоке, пока файл ввода/вывода ожидает диск.

Несколько лет назад я исследовал, как читать файлы, как можно быстрее, и наткнулся на некоторые отличные рекомендации, которые я выполнил в качестве процедуры сканирования, как показано ниже:

// 4k buffer size.
static final int SIZE = 4 * 1024;
static byte[] buffer = new byte[SIZE];

// Fastest because a FileInputStream has an associated channel.
private static void ScanDataFile(Hunter p, FileInputStream f) throws FileNotFoundException, IOException {
    // Use a mapped and buffered stream for best speed.
    // See: http://nadeausoftware.com/articles/2008/02/java_tip_how_read_files_quickly
    final FileChannel ch = f.getChannel();
    long red = 0L;
    do {
        final long read = Math.min(Integer.MAX_VALUE, ch.size() - red);
        final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, red, read);
        int nGet;
        while (mb.hasRemaining() && p.ok()) {
            nGet = Math.min(mb.remaining(), SIZE);
            mb.get(buffer, 0, nGet);
            for (int i = 0; i < nGet && p.ok(); i++) {
                p.check(buffer[i]);
                //size += 1;
            }
        }
        red += read;
    } while (red < ch.size() && p.ok());
    // Finish off.
    p.close();
    ch.close();
    f.close();
}

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

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

Добавлено

Я не тестировал это, но это должно вписаться в ваши тесты и использовать ту же технику:

class Summer {

    long sum = 0;
    long val = 0;

    public void add(byte b) {
        if (b >= '0' && b <= '9') {
            val = (val * 10) + (b - '0');
        } else {
            sum += val;
            val = 0;
        }
    }

    public long getSum() {
        return sum + val;
    }
}

private long sumMapped() throws IOException {
    Summer sum = new Summer();
    FileInputStream f = new FileInputStream(file);
    final FileChannel ch = f.getChannel();
    long red = 0L;
    do {
        final long read = Math.min(Integer.MAX_VALUE, ch.size() - red);
        final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, red, read);
        int nGet;
        while (mb.hasRemaining()) {
            nGet = Math.min(mb.remaining(), SIZE);
            mb.get(buffer, 0, nGet);
            for (int i = 0; i < nGet; i++) {
                sum.add(buffer[i]);
            }
        }
        red += read;
    } while (red < ch.size());
    // Finish off.
    ch.close();
    f.close();
    return sum.getSum();
}

Ответ 3

Почему это происходит намного быстрее?

Создание строки намного дороже, чем небольшая математика.

Можем ли мы сделать это лучше, чем с помощью справки MappedByteBuffer?

Немного, да. Это то, что я использую. Он сохраняет копию памяти в память. то есть нет байта [].

У меня такое ощущение, что накладные расходы при вызове методов для чтения из буфера замедлят работу,

Методы вставляются, если они просты.

особенно при чтении назад из буфера.

Это не будет медленнее, на самом деле разбор форвардов проще или быстрее, потому что вы используете один * вместо двух.

Было бы лучше читать файл вперед, а не назад, но все же сканировать буфер назад?

Я не понимаю, зачем вам вообще нужно читать назад.

Идея состоит в том, что вы читаете первый фрагмент файла, а затем сканируете назад, но отбрасываете половину номера в конце. Затем, когда вы читаете следующий фрагмент, вы устанавливаете смещение так, чтобы вы читали с начала номера, который вы отбрасывали.

звучит излишне сложно. Я бы прочитал за один проход, отображение памяти во всем файле за один раз. Не нужно использовать куски, если размер файла не превышает 2 ГБ. и даже тогда я читал за один проход.

Есть ли что-нибудь, о чем я не думал, что может иметь существенное значение?

Если данные находятся в кеше диска, это будет иметь большее значение, чем что-либо еще.

Ответ 4

Вы можете увеличить размер буфера и быстрее кодировать String (в Unicode).

BufferedReader br = new BufferedReader(new InputStreamReader(
        new FileInputStream(file), StandardCharsets.US_ASCII),
        1_024_000_000);

Ваш метод устранения использования String с помощью двоичного ввода InputStream/RandomAccessFile заслуживает внимания.

Тогда было бы неплохо, если бы исходные файлы были сжаты. В Unix выберете формат gzip, где xxx.txt.gz будет сжиматься до xxx.txt. Это можно было бы прочитать с помощью GZipInputStream. Это имеет преимущество общего ускорения передачи файлов в каталог сервера и из него.

Ответ 5

Источник: http://nadeausoftware.com/articles/2008/02/java_tip_how_read_files_quickly

Для лучшей производительности чтения Java необходимо запомнить четыре вещи:

  • Сведение к минимуму операций ввода-вывода путем чтения массива за раз, а не байта за раз. Массив 8 Кбайт - хороший размер.
  • Минимизировать вызовы методов, получая данные массивом за раз, а не байтом за раз. Используйте индексирование массива для получения байтов в массиве.
  • Минимизировать блокировки синхронизации потоков, если вам не нужна безопасность потоков. Либо сделать меньше вызовов методов в потокобезопасном классе, либо использовать небезопасный класс, например FileChannel и MappedByteBuffer.
  • Минимизировать копирование данных между JVM/OS, внутренними буферами и массивами приложений. Используйте FileChannel с отображением памяти или прямым или завернутый массив ByteBuffer.

Ответ 6

На основе этого комментария: "Просто суммировать все байты быстрее", я предлагаю вариант принятого ответа.

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

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

Шаг уменьшения, объединяющий результаты нескольких потоков, также может быть выполнен с использованием аккумуляторов на месте. Заключительный этап вычисления итогов потребует умножения (или воспользуется тем фактом, что 10 имеет только два бита и использует битовые сдвиги и добавляет), но достаточно всего 9 умножений.

Ответ 7

Здесь есть несколько проблем.

  • Любое решение, основанное на линиях чтения, будет обрабатывать каждый символ дважды. Например, компиляторы не делают этого, они читают по одному символу за раз и отправляют его непосредственно.
  • Любое решение на основе readLine() собирается создавать строки.
  • Вы используете разные размеры буфера.
  • Вы используете разные технологии ввода-вывода.
  • В некоторых случаях вы используете преобразование символов, а в других - нет.
  • Вы переанализируете файл. Вам все равно, где находится пустое пространство, или сколько его там, если оно отделяет числа друг от друга.

Мое решение:

    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file), 8*1024*1024/2);
    long    total = 0;
    int i;
    while ((i = bis.read()) != -1)
    {
        byte    b = (byte)i;
        long    number = 0;
        while (b >= '0' && b <= '9')
        {
            number = number*10+b-'0';
            if ((i = bis.read()) == -1)
                break;
            b = (byte)i;
        }
        total += number;
    }