Il modo più veloce per dividere una stringa delimitata in Java


10

Sto creando un comparatore che fornisce funzionalità di ordinamento multi-colonna su una stringa delimitata. Attualmente sto usando il metodo split dalla classe String come la mia scelta preferita per dividere la stringa grezza in token.

È il modo migliore per convertire la stringa grezza in un array di stringhe? Ordinerò milioni di righe, quindi penso che l'approccio sia importante.

Sembra funzionare bene ed è molto semplice, ma non sono sicuro che ci sia un modo più veloce in Java.

Ecco come funziona l'ordinamento nel mio comparatore:

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

Dopo aver confrontato i vari approcci, che ci crediate o no, il metodo split è stato il più veloce usando l'ultima versione di java. Puoi scaricare il mio comparatore completo qui: https://sourceforge.net/projects/multicolumnrowcomparator/


5
Sottolineerò che la natura della risposta a questa domanda dipende dall'implementazione di jvm. Il comportamento delle stringhe (condivisione di un array di supporto comune in OpenJDK, ma non in OracleJDK) differisce. Questa differenza può avere un impatto significativo sulla divisione delle stringhe e sulla creazione di sottostringhe, insieme alla garbage collection e alle perdite di memoria. Quanto sono grandi questi array? Come lo stai facendo adesso? Considereresti una risposta che crei un nuovo tipo Stringish piuttosto che le attuali stringhe Java?


La dimensione dell'array dipende dal numero di colonne, quindi è variabile. Questo comparatore a più colonne viene passato come parametro in questo modo: ExternalSort.mergeSortedFiles (fileList, nuovo file ("BigFile.csv"), _comparator, Charset.defaultCharset (), false); La routine di ordinamento esterna ordinerà l'intera stringa di riga, in realtà è il comparatore che esegue la divisione e l'ordinamento in base alle colonne di ordinamento
Constantin

Vorrei considerare i tokenizzatori di Lucene. Lucene può essere usato solo come una potente libreria di analisi del testo che si comporta bene sia per compiti semplici che complessi
Doug T.

Considera Apache Commons Lang's StringUtils.split[PreserveAllTokens](text, delimiter).
Ripristina Monica il

Risposte:


19

Ho scritto un test benchmark veloce e sporco per questo. Confronta 7 diversi metodi, alcuni dei quali richiedono una conoscenza specifica dei dati da suddividere.

Per la suddivisione di base per scopi generici, Guava Splitter è 3,5 volte più veloce di String # split () e consiglierei di usarlo. Stringtokenizer è leggermente più veloce di quello e dividersi con indexOf è due volte più veloce di nuovo.

Per il codice e ulteriori informazioni consultare http://demeranville.com/battle-of-the-tokenizers-delimited-text-parser-performance/


Sono solo curioso di sapere quale JDK stavi usando ... e se fosse 1.6, sarei più interessato a vedere un riepilogo dei tuoi risultati in 1.7.

1
era 1.6 credo. Il codice è disponibile come test JUnit se si desidera eseguirlo in 1.7. Nota String.split esegue la corrispondenza regex, che sarà sempre più lenta della divisione su un singolo carattere definito.
Tom,

1
Sì, tuttavia per 1.6, il codice StringTokenizer (e simili) chiama String.substring () che esegue la creazione O (1) della nuova stringa utilizzando lo stesso array di backup. Questo è stato modificato in 1.7 per creare una copia della parte necessaria dell'array di supporto anziché per O (n). Ciò potrebbe avere un impatto singolare sui risultati, riducendo la differenza tra split e StringTokenizer (rallentando tutto ciò che utilizzava la sottostringa in precedenza).

1
Certamente vero. Il fatto è che il modo in cui funziona StringTokenizer è passato da "per creare una nuova stringa e assegnare 3 numeri interi" a "per creare una nuova stringa, fare una copia dell'array dei dati" che cambierà la velocità di quella parte. La differenza tra i vari approcci potrebbe essere meno ora e sarebbe interessante (se non altro per il suo interesse) fare un follow-up con Java 1.7.

1
Grazie per quell'articolo! Molto utile e utilizzerà per confrontare vari approcci.
Constantin,

5

Come scrive @Tom, un approccio di tipo indexOf è più veloce di String.split(), poiché quest'ultimo si occupa delle espressioni regolari e ha un sacco di costi aggiuntivi per loro.

Tuttavia, una modifica dell'algoritmo che potrebbe darti un supervelocità. Supponendo che questo comparatore verrà utilizzato per ordinare le ~ 100.000 stringhe, non scrivere il Comparator<String>. Perché, nel corso del tuo ordinamento, la stessa stringa verrà probabilmente confrontata più volte, quindi la suddividerai più volte, ecc ...

Dividi tutte le stringhe una volta in String [] s, e Comparator<String[]>ordina una stringa []. Quindi, alla fine, puoi combinarli tutti insieme.

In alternativa, puoi anche utilizzare una mappa per memorizzare nella cache String -> String [] o viceversa. es. (impreciso) Nota anche che stai scambiando memoria per la velocità, spero che tu abbia molta 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
}

questo è un buon punto
tom

Richiederebbe la modifica del codice di ordinamento esterno che può essere trovato qui: code.google.com/p/externalsortinginjava
Constantin

1
Probabilmente è più facile usare una mappa allora. Vedi modifica.
user949300

Dato che questo fa parte di un motore di ordinamento esterno (per gestire molti più dati di quelli che potrebbero essere contenuti nella memoria disponibile), stavo davvero cercando un efficiente "splitter" (sì, è inutile dividere ripetutamente la stessa stringa, quindi il mio necessità originale di farlo il più velocemente possibile)
Constantin,

Esplorando brevemente il codice ExternalSort, sembra che se hai cancellato la cache alla fine (o all'avvio) di ogni sortAndSave()chiamata, non dovresti esaurire la memoria a causa di una cache enorme. IMO, il codice dovrebbe avere alcuni hook aggiuntivi come l'attivazione di eventi o la chiamata di metodi protetti do-nothing che gli utenti come te potrebbero ignorare. (Inoltre, non dovrebbero essere tutti i metodi statici in modo che possano farlo ). Potresti voler contattare gli autori e presentare una richiesta.
user949300

2

Secondo questi parametri , StringTokenizer è più veloce per dividere le stringhe ma non restituisce un array che lo rende meno conveniente.

Se hai bisogno di ordinare milioni di righe ti consiglio di usare un RDBMS.


3
Questo era sotto JDK 1.6 - le cose nelle stringhe sono sostanzialmente diverse in 1.7 - vedi java-performance.info/changes-to-string-java-1-7-0_06 (in particolare, la creazione di una sottostringa non è più O (1) ma piuttosto O (n)). Il link osserva che in 1.6 Pattern.split ha usato una stringa diversa rispetto a String.substring ()) - vedi il codice collegato nel commento sopra per seguire StringTokenizer.nextToken () e il costruttore privato del pacchetto a cui aveva accesso.

1

Questo è il metodo che utilizzo per l'analisi di file di grandi dimensioni (1 GB +) delimitati da tabulazioni. Ha un sovraccarico molto inferiore rispetto a String.split(), ma è limitato a charcome delimitatore. Se qualcuno ha un metodo più veloce, mi piacerebbe vederlo. Questo può essere fatto anche su CharSequencee CharSequence.subSequence, ma ciò richiede l'implementazione CharSequence.indexOf(char)(fare riferimento al metodo del pacchetto String.indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)se interessati).

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

Hai confrontato questo vs String.split ()? In tal caso, come si confronta?
Jay Elston,

@JayElston Su un file da 900 MB, ha ridotto il tempo parziale da 7,7 secondi a 6,2 secondi, quindi circa il 20% più veloce. È ancora la parte più lenta dell'analisi della mia matrice in virgola mobile. Immagino che gran parte del tempo rimanente sia allocazione di array. Potrebbe essere possibile tagliare l'allocazione della matrice usando un approccio basato su tokenizer con un offset nel metodo - che inizierebbe ad assomigliare più al metodo che ho citato sopra il codice.
Vallismortis,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.