Самый быстрый способ разбить строку с разделителями в Java

10

Я строю компаратор, который обеспечивает возможность сортировки по нескольким столбцам в строке с разделителями. В настоящее время я использую метод split из класса String в качестве предпочтительного способа разделения необработанной строки на токены.

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

Кажется, что он работает нормально и очень просто, но не уверен, есть ли более быстрый путь в Java.

Вот как работает сортировка в моем компараторе:

public int compare(String a, String b) {

    String[] aValues = a.split(_delimiter, _columnComparators.length);
    String[] bValues = b.split(_delimiter, _columnComparators.length);
    int result = 0;

    for( int index : _sortColumnIndices ) {
        result = _columnComparators[index].compare(aValues[index], bValues[index]);
        if(result != 0){
            break;
        }
    }
    return result;
}

Поверьте или нет, после сравнения различных подходов, метод split был самым быстрым с использованием последней версии Java. Вы можете скачать мой заполненный компаратор здесь: https://sourceforge.net/projects/multicolumnrowcomparator/

Constantin
источник
5
Я укажу, что характер ответа на этот вопрос зависит от реализации jvm. Поведение строк (использование общего вспомогательного массива в OpenJDK, но не в OracleJDK) отличается. Это различие может оказать значительное влияние на разбиение строк и создание подстрок, а также на сборку мусора и утечки памяти. Насколько велики эти массивы? Как ты это делаешь сейчас? Считаете ли вы ответ, который делает для нового типа Stringish, а не фактические строки Java?
1
В частности, посмотрите на StringTokenizer nextToken, который в конечном итоге вызывает пакетный конструктор String . Сравните это с изменениями, задокументированными в документе « Изменения во внутреннем представлении строки», внесенными в Java 1.7.0_06
Размер массива зависит от количества столбцов, поэтому он является переменным. Этот многостолбцовый компаратор передается в качестве параметра, например, так: ExternalSort.mergeSortedFiles (fileList, новый файл ("BigFile.csv"), _comparator, Charset.defaultCharset (), false); Процедура внешней сортировки будет сортировать всю строку строки, на самом деле это компаратор, который выполняет разбиение и сортировку на основе столбцов сортировки
Constantin
Я бы подумал посмотреть на токенизаторы Люцена. Lucene может использоваться как просто мощная библиотека анализа текста, которая хорошо справляется как с простыми, так и со сложными задачами
Даг Т.
Рассмотрим Apache Commons Lang's StringUtils.split[PreserveAllTokens](text, delimiter).
Восстановить Монику

Ответы:

19

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

Для базового расщепления общего назначения Guava Splitter в 3,5 раза быстрее, чем String # split (), и я бы рекомендовал использовать это. Stringtokenizer немного быстрее, а разделение с помощью indexOf вдвое быстрее, чем снова.

Для получения кода и дополнительной информации см. Http://demeranville.com/battle-of-the-tokenizers-delimited-text-parser-performance/

Том
источник
Мне просто любопытно, какой JDK вы использовали ... и если бы это был 1.6, мне было бы очень интересно увидеть резюме ваших результатов в 1.7.
1
это был 1.6 я думаю. Код представлен в виде теста JUnit, если вы хотите запустить его в 1.7. Примечание. String.split выполняет сопоставление регулярных выражений, которое всегда будет медленнее, чем разбиение по одному определенному символу.
Том
1
Да, однако для 1.6 код StringTokenizer (и аналогичный) вызывает функцию String.substring (), которая выполняет O (1) создание новой строки с использованием того же резервного массива. Это было изменено в 1.7, чтобы сделать копию необходимой части резервного массива, а не для O (n). Это может оказать единственное влияние на ваши результаты, уменьшив разницу между split и StringTokenizer (замедляя все, что раньше использовало подстроку).
1
Конечно, правда. Дело в том, что способ, которым работает StringTokenizer, перешел от «создать новую строку, назначить 3 целых числа» к «создать новую строку, сделать копию массива данных», которая изменит скорость выполнения этой части. Разница между различными подходами теперь может быть меньше, и было бы интересно (если не по какой-либо другой причине, кроме его интересной) сделать продолжение с Java 1.7.
1
Спасибо за эту статью! Очень полезно и будет использовать для сравнения различных подходов.
Константин
5

Как пишет @Tom, подход типа indexOf быстрее, чем String.split()последний, так как последний имеет дело с регулярными выражениями и имеет много дополнительных накладных расходов для них.

Тем не менее, одно изменение алгоритма может дать вам супер ускорение. Предполагая, что этот компаратор будет использоваться для сортировки ваших ~ 100 000 строк, не пишите Comparator<String>. Потому что в вашем роде одна и та же строка, скорее всего, будет сравниваться несколько раз, поэтому вы будете разбивать ее несколько раз и т. Д.

Разделите все строки один раз на строки [] и получите Comparator<String[]>сортировку строки []. Затем, в конце, вы можете объединить их все вместе.

Кроме того, вы также можете использовать карту для кэширования String -> String [] или наоборот. например (схематично) Также обратите внимание, что вы торгуете памятью на скорость, надеюсь, у вас много оперативной памяти

HashMap<String, String[]> cache = new HashMap();

int compare(String s1, String s2) {
   String[] cached1 = cache.get(s1);
   if (cached1  == null) {
      cached1 = mySuperSplitter(s1):
      cache.put(s1, cached1);
   }
   String[] cached2 = cache.get(s2);
   if (cached2  == null) {
      cached2 = mySuperSplitter(s2):
      cache.put(s2, cached2);
   }

   return compareAsArrays(cached1, cached2);  // real comparison done here
}
user949300
источник
Это хороший момент.
Том
Для этого потребуется изменить
Constantin
1
Наверное, проще всего использовать карту тогда. Смотрите редактировать.
user949300
Учитывая, что это является частью внешнего механизма сортировки (чтобы иметь дело с гораздо большим количеством данных, чем может поместиться в доступной памяти), я действительно стремился к эффективному «разделителю» (да, разбивать одну и ту же строку несколько раз, поэтому мой Первоначально нужно сделать это как можно быстрее)
Константин
Кратко просматривая код ExternalSort, похоже, что если вы очищали кеш в конце (или в начале) каждого sortAndSave()вызова, то вам не нужно исчерпывать память из-за огромного кеша. IMO, в коде должно быть несколько дополнительных хуков, таких как события срабатывания или вызов методов, которые ничего не делают, которые вы можете переопределить. (Кроме того, это не должны быть все статические методы, чтобы они могли это делать ). Возможно, вы захотите связаться с авторами и подать запрос.
user949300
2

Согласно этим тестам , StringTokenizer быстрее разбивает строки, но не возвращает массив, что делает его менее удобным.

Если вам нужно отсортировать миллионы строк, я бы порекомендовал использовать СУБД.

Тулаинс Кордова
источник
3
Это было в JDK 1.6 - вещи в строках принципиально отличаются в 1.7 - см. Java-performance.info/changes-to-string-java-1-7-0_06 (в частности, создание подстроки больше не является O (1), но скорее O (n)). Ссылка отмечает, что в 1.6 Pattern.split использовалось создание String, отличное от String.substring ()) - см. Код, связанный в комментарии выше, чтобы следовать StringTokenizer.nextToken () и частному конструктору пакета, к которому он имел доступ.
1

Этот метод я использую для разбора больших (1 ГБ +) файлов с разделителями табуляции. Он имеет гораздо меньше накладных расходов, чем String.split()ограничитель, но ограничен ими char. Если у кого-то есть более быстрый метод, я бы хотел его увидеть. Это также может быть сделано поверх CharSequenceи CharSequence.subSequence, но это требует реализации CharSequence.indexOf(char)(обратитесь к методу пакета, String.indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)если интересно).

public static String[] split(final String line, final char delimiter)
{
    CharSequence[] temp = new CharSequence[(line.length() / 2) + 1];
    int wordCount = 0;
    int i = 0;
    int j = line.indexOf(delimiter, 0); // first substring

    while (j >= 0)
    {
        temp[wordCount++] = line.substring(i, j);
        i = j + 1;
        j = line.indexOf(delimiter, i); // rest of substrings
    }

    temp[wordCount++] = line.substring(i); // last substring

    String[] result = new String[wordCount];
    System.arraycopy(temp, 0, result, 0, wordCount);

    return result;
}
vallismortis
источник
Вы сравнивали это с String.split ()? Если да, то как это сравнить?
Джей Элстон
@JayElston В файле размером 900 МБ время разделения сократилось с 7,7 до 6,2 секунды, что примерно на 20% быстрее. Это все еще самая медленная часть моего анализа матрицы с плавающей точкой. Я предполагаю, что большую часть оставшегося времени занимает распределение массивов. Можно было бы сократить распределение матриц, используя подход, основанный на токенизаторе, со смещением в методе - который начал бы больше походить на метод, который я привел выше кода.
vallismortis