Maneira mais rápida de dividir uma String delimitada em Java

10

Estou construindo um comparador que fornece capacidade de classificação de várias colunas em uma String delimitada. Atualmente, estou usando o método split da classe String como minha escolha preferida para dividir a String bruta em tokens.

Essa é a melhor maneira de converter a String bruta em uma matriz String? Vou classificar milhões de linhas, então acho que a abordagem é importante.

Parece funcionar bem e é muito fácil, mas não tenho certeza se existe uma maneira mais rápida em java.

Aqui está como a classificação funciona no meu Comparador:

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;
}

Depois de comparar as várias abordagens, acredite ou não, o método split foi o mais rápido usando a versão mais recente do java. Você pode fazer o download do meu comparador completo aqui: https://sourceforge.net/projects/multicolumnrowcomparator/

Constantin
fonte
5
Vou salientar que a natureza da resposta a esta pergunta depende da implementação da jvm. O comportamento das strings (compartilhando uma matriz de apoio comum no OpenJDK, mas não no OracleJDK) é diferente. Essa diferença pode ter impactos significativos na divisão de strings e na criação de substrings, junto com a coleta de lixo e vazamentos de memória. Qual o tamanho dessas matrizes? Como você está fazendo isso agora? Você consideraria uma resposta que cria um novo tipo Stringish em vez de Java Strings reais?
1
Em particular, observe o StringTokenizer nextToken, que eventualmente chama o construtor String privado do pacote . Compare isso com as alterações documentadas em Alterações na representação interna da Cadeia de caracteres feita em Java 1.7.0_06
O tamanho da matriz depende do número de colunas, portanto é variável. Esse comparador de várias colunas é passado como um parâmetro assim: ExternalSort.mergeSortedFiles (fileList, new File ("BigFile.csv"), _comparator, Charset.defaultCharset (), false); A rotina de classificação externa classifica toda a cadeia de linhas; na verdade, é o comparador que faz a divisão e a classificação com base nas colunas de classificação
Constantin
Eu consideraria olhar para os tokenizers de lucene. O Lucene pode ser usado apenas como uma poderosa biblioteca de análise de texto com bom desempenho para tarefas simples e complexas.
Doug T.
Considere o Apache Commons Lang StringUtils.split[PreserveAllTokens](text, delimiter).
Reponha Monica

Respostas:

19

Eu escrevi um teste de benchmark rápido e sujo para isso. Ele compara 7 métodos diferentes, alguns dos quais requerem conhecimento específico dos dados que estão sendo divididos.

Para a divisão básica de uso geral, o Guava Splitter é 3,5x mais rápido que o String # split () e eu recomendo usá-lo. O stringtokenizer é um pouco mais rápido que isso e dividir-se com indexOf é duas vezes mais rápido do que isso.

Para obter o código e mais informações, consulte http://demeranville.com/battle-of-the-tokenizers-delimited-text-parser-performance/

tom
fonte
Só estou curioso sobre o JDK que você estava usando ... e se fosse 1,6, eu estaria mais interessado em ver uma recapitulação de seus resultados em 1,7.
1
era 1,6 eu acho. O código existe como um teste JUnit se você deseja executá-lo na versão 1.7. Nota String.split executa a correspondência de regex, que sempre será mais lenta que a divisão em um único caractere definido.
tom
1
Sim, no entanto, para a 1.6, o código StringTokenizer (e semelhante) chama um String.substring () que cria O (1) a criação da nova string usando a mesma matriz de backup. Isso foi alterado em 1.7 para fazer uma cópia da parte necessária da matriz de suporte, em vez de O (n). Isso pode ter um impacto único nos resultados, diminuindo a diferença entre a divisão e o StringTokenizer (diminuindo a velocidade de tudo o que antes era usado para substring).
1
Certamente verdade. A questão é a maneira como o StringTokenizer funciona passou de "para criar uma nova sequência, atribua 3 números inteiros" a "para criar uma nova sequência, faça uma cópia de matriz dos dados", o que mudará a rapidez com que essa parte é. A diferença entre as várias abordagens pode ser menor agora e seria interessante (se não por outro motivo que não seja interessante) fazer um acompanhamento com o Java 1.7.
1
Obrigado por esse artigo! Muito útil e utilizará para comparar várias abordagens.
Constantin
5

Como o @Tom escreve, uma abordagem do tipo indexOf é mais rápida que String.split(), uma vez que a última lida com expressões regulares e possui uma sobrecarga extra para elas.

No entanto, uma alteração no algoritmo pode fornecer uma super aceleração. Supondo que este comparador seja usado para classificar suas ~ 100.000 Strings, não escreva o Comparator<String>. Porque, no decorrer da sua classificação, a mesma String provavelmente será comparada várias vezes, então você a dividirá várias vezes, etc ...

Divida todas as Strings uma vez em String [] s e Comparator<String[]>classifique a String []. Então, no final, você pode combiná-los todos juntos.

Como alternativa, você também pode usar um mapa para armazenar em cache a String -> String [] ou vice-versa. Por exemplo, (esboçado) Observe também que você está trocando memória por velocidade, espero ter muita RAM

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
fonte
esse é um bom ponto
tom
Exigiria modificações no código de classificação externa, que pode ser encontrado aqui: code.google.com/p/externalsortinginjava
Constantin
1
Provavelmente é mais fácil usar um mapa então. Veja editar.
user949300
Dado que isso faz parte de um mecanismo de classificação externo (para lidar com muito mais dados do que a memória disponível pode caber), eu estava realmente buscando um "divisor" eficiente (sim, é um desperdício dividir a mesma String repetidamente, daí o meu necessidade original para fazer isso o mais rápido possível)
Constantin
Navegando brevemente no código ExternalSort, parece que se você limpou o cache no final (ou no início) de cada sortAndSave()chamada, não deve ficar sem memória devido a um cache enorme. Na IMO, o código deve ter alguns ganchos extras, como disparar eventos ou chamar métodos protegidos do nada, que usuários como você podem substituir. (Além disso, não devem ser todos os métodos estáticos para que eles possam fazer isso). Você pode entrar em contato com os autores e registrar uma solicitação.
usar o seguinte comando
2

De acordo com esses benchmarks , o StringTokenizer é mais rápido para dividir strings, mas não retorna uma matriz, o que o torna menos conveniente.

Se você precisar classificar milhões de linhas, recomendo usar um RDBMS.

Tulains Córdova
fonte
3
Isso estava no JDK 1.6 - as coisas nas strings são fundamentalmente diferentes no 1.7 - veja java-performance.info/changes-to-string-java-1-7-0_06 (em particular, criar uma substring não é mais O (1), mas em vez de O (n)). O link observa que no 1.6 Pattern.split usava String diferente da criação de String.substring ()) - veja o código vinculado no comentário acima para seguir o StringTokenizer.nextToken () e o construtor privado do pacote ao qual teve acesso.
1

Esse é o método que eu uso para analisar arquivos grandes (1GB +) delimitados por tabulação. Tem muito menos sobrecarga do que String.split(), mas é limitado a charum delimitador. Se alguém tiver um método mais rápido, eu gostaria de vê-lo. Isso também pode ser feito repetidamente CharSequencee CharSequence.subSequence, mas isso requer implementação CharSequence.indexOf(char)(consulte o método do pacote, String.indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)se estiver interessado).

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
fonte
Você comparou isso com String.split ()? Se sim, como ele se compara?
Jay Elston
@JayElston Em um arquivo de 900 MB, reduziu o tempo de divisão de 7,7 segundos para 6,2 segundos, portanto cerca de 20% mais rápido. Ainda é a parte mais lenta da minha análise de matriz de ponto flutuante. Eu estou supondo que muito do tempo restante é alocação de matriz. Pode ser possível cortar a alocação da matriz usando uma abordagem baseada em tokenizer com um deslocamento no método - que começaria a parecer mais com o método que citei acima do código.
vallismortis