2009-06-12 6 views
5

Воодушевленные this, и тот факт, я имею миллиарды строки для синтаксического анализа, я попытался изменить мой код, чтобы принять StringTokenizer вместо String []Репликация String.split с StringTokenizer

Единственное, что осталось между мной и получать, что вкусный прирост производительности x2 является тот факт, что, когда вы делаете

"dog,,cat".split(",") 
//output: ["dog","","cat"] 

StringTokenizer("dog,,cat") 
// nextToken() = "dog" 
// nextToken() = "cat" 

Как я могу достичь подобных результатов с StringTokenizer? Есть ли более быстрые способы сделать это?

ответ

12

Вы только на самом деле символизируете запятые? Если это так, я бы написал свой собственный токенизатор - он вполне может оказаться еще более эффективным, чем более универсальный StringTokenizer, который может искать несколько токенов, и вы можете заставить его вести себя, как бы вы ни хотели. Для такого простого использования это может быть простая реализация.

Если бы это было полезно, вы могли бы даже реализовать Iterable<String> и получить поддержку расширенного цикла для сильной печати вместо поддержки Enumeration, предоставляемой StringTokenizer. Дайте мне знать, если вы хотите, чтобы какая-либо помощь кодировала такого зверя - это действительно не должно быть слишком сложно.

Кроме того, я попытаюсь выполнить тесты производительности на ваших фактических данных, прежде чем переходить слишком далеко от существующего решения. Вы не знаете, сколько вашего времени выполнения фактически потрачено на String.split? Я знаю, что у вас много строк для синтаксического анализа, но если после этого вы делаете что-то важное с ними, я бы ожидал, что это будет гораздо более значительным, чем расщепление.

+1

+1, мне это нравится идея реализации Iterable ! – coobird

+0

Спасибо, Джон, я провел парсинг (используя множество индексов), и теперь он быстрее x4! – Dani

2

В зависимости от того, какие строки вам нужно tokenize, вы можете написать собственный разделитель на основе String.indexOf(), например. Вы также можете создать многоядерное решение для повышения производительности еще больше, поскольку токенизация строк независима друг от друга. Работа над партиями -lets - 100 строк на ядро. Сделайте String.split() или еще что-нибудь.

-1

Если ваш вход структурирован, вы можете посмотреть компилятор JavaCC. Он генерирует класс java, читающий ваш ввод. Это будет выглядеть следующим образом:

TOKEN { <CAT: "cat"> , <DOG:"gog"> } 

input: (cat() | dog())* 


cat: <CAT> 
    { 
    animals.add(new Animal("Cat")); 
    } 

dog: <DOG> 
    { 
    animals.add(new Animal("Dog")); 
    } 
2

Вместо StringTokenizer, вы могли бы попробовать класс StrTokenizer из Apache Commons Lang, который я цитирую:

Этот класс может разбить строку на множество мелких строк. Он нацелен на аналогичную работу с StringTokenizer, однако он предлагает гораздо больше контроля и гибкости, включая реализацию интерфейса ListIterator.

Пустые токены могут быть удалены или возвращены как null.

Это похоже на то, что вам нужно, я думаю?

4

Примечание. Проведя несколько быстрых тестов, сканер оказывается примерно в четыре раза медленнее, чем String.split. Следовательно, не используйте Scanner.

(я покидаю пост до записи о том, что сканер является плохой идеей в этом случае (читается как:. Не downvote меня предложивший сканер, пожалуйста ...))

Предполагая вы используете Java 1.5 или выше, попробуйте Scanner, который реализует Iterator<String>, как это происходит:

Scanner sc = new Scanner("dog,,cat"); 
sc.useDelimiter(","); 
while (sc.hasNext()) { 
    System.out.println(sc.next()); 
} 

дает:

dog 

cat 
+2

Я считаю, что Scanner использует внутреннее выражение, поэтому OP не может получить повышение производительности, которое они ищут. Стоит попробовать, хотя и с подходящим эталоном :) –

+2

Быстрый опрос производительности дает мне 47 мс для StringTokenizer, 625 мс для String.split и 2235 мс для сканера. Поэтому я отказываюсь от своего предложения. Не используйте Scanner, это ужасно медленно. – Zarkonnen

1

Вы могли бы сделать что-то подобное. Это не идеально, но это может сработать для вас.

public static List<String> find(String test, char c) { 
    List<String> list = new Vector<String>(); 
    start; 
    int i=0; 
    while (i<=test.length()) { 
     int start = i; 
     while (i<test.length() && test.charAt(i)!=c) { 
      i++; 
     } 
     list.add(test.substring(start, i)); 
     i++; 
    } 
    return list; 
} 

Если возможно вы можете ommit прейскурантной вещи и сразу сделать что-то подстроку:

public static void split(String test, char c) { 
    int i=0; 
    while (i<=test.length()) { 
     int start = i; 
     while (i<test.length() && test.charAt(i)!=c) { 
      i++; 
     } 
     String s = test.substring(start,i); 
     // do something with the string here 
     i++; 
    } 
} 

На моей системе последнего метод работает быстрее, чем StringTokenizer-решение, но вы можете проверить как это работает для вас. (Конечно, вы могли бы сделать этот метод немного короче, обойдя {} второго во время просмотра, и, конечно, вы могли бы использовать for-loop вместо внешнего while-loop и включая последний i ++ в это, но я didn ' т сделать это здесь, потому что я считаю, что плохой стиль.

0

Ну, самый быстрый, что вы могли бы сделать, чтобы вручную переместить строку, например

List<String> split(String s) { 
     List<String> out= new ArrayList<String>(); 
      int idx = 0; 
      int next = 0; 
     while ((next = s.indexOf(',', idx)) > -1) { 
      out.add(s.substring(idx, next)); 
      idx = next + 1; 
     } 
     if (idx < s.length()) { 
      out.add(s.substring(idx)); 
     } 
       return out; 
    } 

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

В конечном счете, это, вероятно, не стоит беспокоить.

10

После измельчения с классом StringTokenizer я не смог найти способ удовлетворить требования по возврату ["dog", "", "cat"].

Кроме того, класс StringTokenizer оставлен только по соображениям совместимости, а также использование String.split. Из спецификации API для StringTokenizer:

StringTokenizer является устаревшим классом , который сохраняется для совместимости причин, хотя его использование не рекомендуется в новом коде. Это рекомендуется, чтобы любой, кто ищет эту функцию , использует метод split of String или java.util.regex пакет вместо этого.

Поскольку проблема является предположительно низкой производительностью метода String.split, нам нужно найти альтернативу.

Примечание: Я говорю «якобы плохую работу», потому что трудно определить, что каждый случай использования будет приводить в StringTokenizer превосходя методу String.split. Кроме того, во многих случаях, если токенизация строк действительно является узким местом приложения, определяемым надлежащим профилированием, я чувствую, что в конечном итоге это будет преждевременная оптимизация. Я был бы склонен сказать, что писать код, который имеет смысл и легко понять, прежде чем приступать к оптимизации.

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

Сверните свой собственный токензиер!

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

class MyTokenizer implements Iterable<String>, Iterator<String> { 
    String delim = ","; 
    String s; 
    int curIndex = 0; 
    int nextIndex = 0; 
    boolean nextIsLastToken = false; 

    public MyTokenizer(String s, String delim) { 
    this.s = s; 
    this.delim = delim; 
    } 

    public Iterator<String> iterator() { 
    return this; 
    } 

    public boolean hasNext() { 
    nextIndex = s.indexOf(delim, curIndex); 

    if (nextIsLastToken) 
     return false; 

    if (nextIndex == -1) 
     nextIsLastToken = true; 

    return true; 
    } 

    public String next() { 
    if (nextIndex == -1) 
     nextIndex = s.length(); 

    String token = s.substring(curIndex, nextIndex); 
    curIndex = nextIndex + 1; 

    return token; 
    } 

    public void remove() { 
    throw new UnsupportedOperationException(); 
    } 
} 

MyTokenizer примет String tokenize и String в качестве разделителя и использовать метод String.indexOf для выполнения поиска разделителей. Токены производятся методом String.substring.

Я бы предположил, что могут быть некоторые улучшения в производительности, работая над строкой на уровне char[], а не на уровне String. Но я оставлю это упражнение для читателя.

Класс также реализует Iterable и Iterator для того, чтобы воспользоваться преимуществами конструкций for-each петли, которая была введена в Java 5. StringTokenizer является Enumerator, и не поддерживает for-each конструкцию.

Быстрее ли это?

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

  1. Использование StringTokenizer.
  2. Использование нового MyTokenizer.
  3. Использование String.split.
  4. Использование предварительно скомпилированного регулярного выражения на Pattern.compile.

В четырех методах строка "dog,,cat" была разделена на жетоны. Хотя значение StringTokenizer включено в сравнение, следует отметить, что он не вернет желаемый результат ["dog", "", "cat].

Повторяемость была повторена в общей сложности 1 миллион раз, чтобы дать достаточно времени, чтобы заметить разницу в методах.

Код, используемый для простого теста была следующей:

long st = System.currentTimeMillis(); 
for (int i = 0; i < 1e6; i++) { 
    StringTokenizer t = new StringTokenizer("dog,,cat", ","); 
    while (t.hasMoreTokens()) { 
    t.nextToken(); 
    } 
} 
System.out.println(System.currentTimeMillis() - st); 

st = System.currentTimeMillis(); 
for (int i = 0; i < 1e6; i++) { 
    MyTokenizer mt = new MyTokenizer("dog,,cat", ","); 
    for (String t : mt) { 
    } 
} 
System.out.println(System.currentTimeMillis() - st); 

st = System.currentTimeMillis(); 
for (int i = 0; i < 1e6; i++) { 
    String[] tokens = "dog,,cat".split(","); 
    for (String t : tokens) { 
    } 
} 
System.out.println(System.currentTimeMillis() - st); 

st = System.currentTimeMillis(); 
Pattern p = Pattern.compile(","); 
for (int i = 0; i < 1e6; i++) { 
    String[] tokens = p.split("dog,,cat"); 
    for (String t : tokens) { 
    } 
} 
System.out.println(System.currentTimeMillis() - st); 

Полученные результаты

Испытания проводились с использованием Java SE 6 (сборка 1.6.0_12-B04), и результаты были следующее:

 
        Run 1 Run 2 Run 3 Run 4 Run 5 
        ----- ----- ----- ----- ----- 
StringTokenizer  172  188  187  172  172 
MyTokenizer   234  234  235  234  235 
String.split  1172  1156  1171  1172  1156 
Pattern.compile  906  891  891  907  906 

Таким образом, как видно из ограниченного тестирования и только пять трасс, то StringTokenizer сделал на самом деле с ome out the fastest, но MyTokenizer пришел в себя как второй.Затем String.split был самым медленным, а предварительно скомпилированное регулярное выражение было немного быстрее, чем метод split.

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

+0

Я думаю, что этот метод должен быть следующим: public String next() { if (nextIndex == -1) nextIndex = s.length(); String token = s.substring (curIndex, nextIndex); curIndex = nextIndex + delim.length(); знак возврата; } –

0

Я бы порекомендовал Google Guava Splitter.
я сравнил его с coobird испытания и получили следующие результаты:

StringTokenizer 104
Google гуавы Splitter 142
String.split 446
регулярных выражений 299