Низкая производительность с большими списками Java - PullRequest
9 голосов
/ 07 марта 2012

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

Я читаю файл английского текста объемом 8 ГБ в формате UTF-8 с одним предложением в строке. Я хочу split() каждую строку на пустом месте и сохранить полученные массивы String в ArrayList<String[]> для дальнейшей обработки. Вот упрощенная программа, которая демонстрирует проблему:

/** Load whitespace-delimited tokens from stdin into memory. */
public class LoadTokens {
    private static final int INITIAL_SENTENCES = 66000000;

    public static void main(String[] args) throws IOException {
        List<String[]> sentences = new ArrayList<String[]>(INITIAL_SENTENCES);
        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
        long numTokens = 0;
        String line;

        while ((line = stdin.readLine()) != null) {
            String[] sentence = line.split("\\s+");
            if (sentence.length > 0) {
                sentences.add(sentence);
                numTokens += sentence.length;
            }
        }
        System.out.println("Read " + sentences.size() + " sentences, " + numTokens + " tokens.");
    }
}

Кажется довольно обрезанным, верно? Вы заметите, что я даже предварительно настроил мой ArrayList; У меня чуть меньше 66 миллионов предложений и 1,3 миллиарда токенов. Теперь, если вы возьмете ссылку на размеры объектов Java и свой карандаш, вы обнаружите, что для этого потребуется:

  • 66e6 String[] ссылки @ 8 байт ea = 0,5 ГБ
  • 66e6 String[] объекты @ 32 байта ea = 2 ГБ
  • 66e6 char[] объекты @ 32 байта ea = 2 ГБ
  • 1,3e9 String ссылки @ 8 байт ea = 10 ГБ
  • 1,3e9 String с @ 44 байта ea = 53 ГБ
  • 8e9 char с @ 2 байта ea = 15 ГБ

83 ГБ . (Вы заметите, что мне действительно нужно использовать 64-битные размеры объектов, поскольку сжатые ООП не могут помочь мне с кучей> 32 ГБ.) Нам повезло, что у нас есть машина RedHat 6 с 128 ГБ ОЗУ, поэтому я запускаю 64-разрядную серверную виртуальную машину Java HotSpot (TM) (сборка 20.4-b02, смешанный режим) из набора Java SE 1.6.0_29 с pv giant-file.txt | java -Xmx96G -Xms96G LoadTokens, чтобы быть в безопасности, и откидываюсь назад, пока смотрю top.

Где-то менее чем на полпути через вход, при RSS 50-60 ГБ, параллельный сборщик мусора загружает до 1300% ЦП (блок 16 процедур) и процесс чтения останавливается. Затем идет еще несколько ГБ, затем прогресс останавливается еще дольше. Он заполняет 96 ГБ и еще не готов. Я позволил этому работать полтора часа, и он просто тратит ~ 90% системного времени на сборку мусора. Это кажется крайним.

Чтобы убедиться, что я не сошла с ума, я взяла эквивалентный Python (все две строки;), и он завершился примерно через 12 минут и 70 ГБ RSS.

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

Ответы [ 4 ]

4 голосов
/ 07 марта 2012

-XX:+UseConcMarkSweepGC: заканчивается за 78 ГБ и ~ 12 минут. (Почти так же хорошо, как Python!) Спасибо за помощь всем.

2 голосов
/ 07 марта 2012

Вы должны использовать следующие приемы:

  • Помогите JVM собрать одинаковые токены в одну строковую ссылку благодаря sentences.add(sentence.intern()).Подробнее см. String.intern .Насколько я знаю, он также должен иметь эффект, о котором говорил Джон Скит: он разбивает массив символов на мелкие кусочки.

  • Использование экспериментальных опций HotSpot для сжатия Stringи реализации char [] и связанные с ними:

    -XX:+UseCompressedStrings -XX:+UseStringCache -XX:+OptimizeStringConcat
    

При таком объеме памяти вы должны настроить свою систему и JVM на использование больших страниц .

Действительно сложно повысить производительность только при настройке ГХ и более чем на 5%.Вы должны сначала уменьшить потребление памяти вашим приложением благодаря профилированию.

Кстати, мне интересно, действительно ли вам нужно получить полное содержимое книги в памяти - я не знаю, что ваш код делает дальше со всемипредложения, но вы должны рассмотреть альтернативный вариант, например Lucene indexing tool для подсчета слов или извлечения любой другой информации из вашего текста.

2 голосов
/ 07 марта 2012

Идея 1

Начните с рассмотрения этого:

while ((line = stdin.readLine()) != null) {

По крайней мере, использовал в том случае, если readLine вернет String с поддержкой char[] не менее 80 символов. Будет ли это проблемой, зависит от того, что делает следующая строка:

String[] sentence = line.split("\\s+");

Вы должны определить, имеют ли строки, возвращаемые split, одинаковые значения char[].

Если они есть (и если ваши строки часто короче 80 символов), вы должны использовать:

line = new String(line);

Это создаст клон копии строки с массивом строк «правильного размера»

Если они не , то вам, возможно, следует выработать какой-то способ создания того же поведения, но изменить его, чтобы они do использовали одну и ту же основу char[] (т.е. они подстроки исходной строки) - и, конечно же, выполните ту же операцию клонирования. Вам не нужно выделять char[] на слово, так как это будет тратить гораздо больше памяти, чем пробелы.

Идея 2

Ваш заголовок говорит о плохой производительности списков - но, конечно, вы можете легко вычеркнуть список из этого уравнения, просто создав String[][], по крайней мере, для целей тестирования. Похоже, вы уже знаете размер файла - и если вы этого не сделаете, вы можете запустить его через wc, чтобы проверить заранее. Просто чтобы посмотреть, сможете ли вы избежать этой проблемы , чтобы начать с .

Идея 3

Сколько различных слов в вашем корпусе? Рассматривали ли вы сохранить HashSet<String> и добавлять каждое слово к нему, когда вы сталкиваетесь с ним? Таким образом, вы, вероятно, в конечном итоге получите на гораздо меньше строк. На этом этапе вы, вероятно, захотите отказаться от «единственной поддержки char[] на строку» из первой идеи - вы бы хотели , чтобы каждая строка поддерживалась собственным массивом символов, иначе строка с одно новое слово все еще будет требовать много символов. (В качестве альтернативы для реальной тонкой настройки вы можете увидеть, сколько «новых слов» имеется в строке, и клонировать каждую строку или нет.)

0 голосов
/ 07 марта 2012

Вы должны проверить, как ваше пространство кучи разбивается на части (PermGen, OldGen, Eden и Survivors) благодаря VisualGC , который теперь является плагином для VisualVM .

В вашем случае вы, вероятно, захотите уменьшить Eden и Survivors, чтобы увеличить OldGen, чтобы ваш GC не превращался в сбор полного OldGen ...

Для этого вы должны использовать дополнительные параметры, такие как:

-XX:NewRatio=2 -XX:SurvivorRatio=8

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

Если все эти String должны жить в памяти все время жизни JVM, было бы неплохо включить их в PermGen, определенный достаточно большим с -XX:MaxPermSize, и избежать сбора в этой зоне благодаря -Xnoclassgc.

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

-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:verbosegc.log
...