Le moyen le plus rapide de diviser une chaîne délimitée en Java

10

Je construis un comparateur qui offre une capacité de tri multi-colonnes sur une chaîne délimitée. J'utilise actuellement la méthode de fractionnement de la classe String comme mon choix préféré pour diviser la chaîne brute en jetons.

Est-ce le moyen le plus performant de convertir la chaîne brute en un tableau de chaînes? Je vais trier des millions de lignes, donc je pense que l'approche est importante.

Il semble fonctionner correctement et est très facile, mais vous ne savez pas s'il existe un moyen plus rapide en java.

Voici comment fonctionne le tri dans mon comparateur:

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

Après avoir comparé les différentes approches, croyez-le ou non, la méthode de fractionnement a été la plus rapide en utilisant la dernière version de java. Vous pouvez télécharger mon comparateur complet ici: https://sourceforge.net/projects/multicolumnrowcomparator/

Constantin
la source
5
Je soulignerai que la nature de la réponse à cette question dépend de la mise en œuvre du jvm. Le comportement des chaînes (partageant un tableau de support commun dans OpenJDK, mais pas dans OracleJDK) diffère. Cette différence peut avoir des impacts importants sur le fractionnement des chaînes et la création de sous-chaînes, ainsi que le garbage collection et les fuites de mémoire. Quelle est la taille de ces tableaux? Comment tu fais ça maintenant? Envisageriez-vous une réponse qui crée un nouveau type Stringish plutôt que de véritables chaînes Java?
1
En particulier, regardez StringTokenizer nextToken qui appelle finalement le constructeur de chaîne privé du package . Comparez cela aux changements documentés dans Modifications de la représentation interne des chaînes effectuées en Java 1.7.0_06
La taille du tableau dépend du nombre de colonnes, elle est donc variable. Ce comparateur à plusieurs colonnes est transmis comme un paramètre comme ceci: ExternalSort.mergeSortedFiles (fileList, new File ("BigFile.csv"), _comparator, Charset.defaultCharset (), false); La routine de tri externe triera toute la chaîne de lignes, c'est en fait le comparateur qui effectue le fractionnement et le tri en fonction des colonnes de tri
Constantin
J'envisagerais de regarder les jetons de lucene. Lucene peut être utilisée comme une simple bibliothèque d'analyse de texte performante pour les tâches simples et complexes
Doug T.
Prenons l'exemple d'Apache Commons Lang StringUtils.split[PreserveAllTokens](text, delimiter).
Rétablir Monica

Réponses:

19

J'ai écrit un test de référence rapide et sale pour cela. Il compare 7 méthodes différentes, dont certaines nécessitent une connaissance spécifique des données divisées.

Pour le fractionnement général de base, Guava Splitter est 3,5 fois plus rapide que String # split () et je recommanderais de l'utiliser. Stringtokenizer est légèrement plus rapide que cela et vous séparer avec indexOf est deux fois plus rapide que de nouveau.

Pour le code et plus d'informations, voir http://demeranville.com/battle-of-the-tokenizers-delimited-text-parser-performance/

à M
la source
Je suis simplement curieux de savoir quel JDK vous utilisiez ... et s'il s'agissait de 1.6, je serais très intéressé à voir un récapitulatif de vos résultats en 1.7.
1
c'était 1,6 je pense. Le code est là en tant que test JUnit si vous voulez l'exécuter en 1.7. Remarque String.split effectue une correspondance d'expression régulière, qui sera toujours plus lente que le fractionnement sur un seul caractère défini.
Tom
1
Oui, cependant pour 1.6, le code StringTokenizer (et similaire) appelle un String.substring () qui fait la création O (1) de la nouvelle chaîne en utilisant le même tableau de sauvegarde. Cela a été changé en 1.7 pour faire une copie de la partie nécessaire du tableau de support plutôt que pour O (n). Cela pourrait avoir un impact unique sur vos résultats, ce qui réduirait la différence entre le fractionnement et StringTokenizer (ralentissant tout ce qui utilisait la sous-chaîne auparavant).
1
Certainement vrai. Le fait est que le fonctionnement de StringTokenizer est passé de "pour créer une nouvelle chaîne, attribuer 3 entiers" à "pour créer une nouvelle chaîne, faire une copie de tableau des données", ce qui changera la vitesse de cette partie. La différence entre les différentes approches peut être moindre maintenant et il serait intéressant (si pour aucune autre raison que son intérêt) de faire un suivi avec Java 1.7.
1
Merci pour cet article! Très utile et servira à comparer différentes approches.
Constantin
5

Comme l'écrit @Tom, une approche de type indexOf est plus rapide que String.split(), car cette dernière traite des expressions régulières et a beaucoup de surcharge supplémentaire pour elles.

Cependant, un changement d'algorithme qui pourrait vous donner une super accélération. En supposant que ce comparateur sera utilisé pour trier vos ~ 100 000 chaînes, n'écrivez pas le Comparator<String>. Parce que, au cours de votre tri, la même chaîne sera probablement comparée plusieurs fois, vous la diviserez donc plusieurs fois, etc.

Divisez toutes les chaînes une fois en chaînes [] et Comparator<String[]>triez la chaîne []. Ensuite, à la fin, vous pouvez les combiner tous ensemble.

Alternativement, vous pouvez également utiliser une carte pour mettre en cache la chaîne -> chaîne [] ou vice versa. Par exemple (sommaire) Notez également que vous échangez de la mémoire pour la vitesse, j'espère que vous avez beaucoup de 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
la source
C'est un bon point.
tom
Il faudrait modifier le code de tri externe qui peut être trouvé ici: code.google.com/p/externalsortinginjava
Constantin
1
Il est alors probablement plus facile d'utiliser une carte. Voir modifier.
user949300
Étant donné que cela fait partie d'un moteur de tri externe (pour traiter beaucoup plus de données que ne peut en contenir la mémoire disponible), je recherchais vraiment un "séparateur" efficace (oui, il est inutile de diviser la même chaîne à plusieurs reprises, d'où mon besoin initial de le faire le plus rapidement possible)
Constantin
En parcourant brièvement le code ExternalSort, il semble que si vous avez effacé votre cache à la fin (ou au début) de chaque sortAndSave()appel, vous ne devriez pas manquer de mémoire en raison d'un énorme cache. OMI, le code devrait avoir quelques hooks supplémentaires comme le déclenchement d'événements ou l'appel de méthodes protégées par rien que des utilisateurs comme vous pourraient remplacer. (De plus, toutes les méthodes statiques ne doivent pas être utilisées pour qu'elles puissent le faire. ) Vous voudrez peut-être contacter les auteurs et déposer une demande.
user949300
2

Selon ces benchmarks , StringTokenizer est plus rapide pour le fractionnement de chaînes, mais il ne renvoie pas de tableau, ce qui le rend moins pratique.

Si vous avez besoin de trier des millions de lignes, je vous recommande d'utiliser un SGBDR.

Tulains Córdova
la source
3
C'était sous JDK 1.6 - les choses dans les chaînes sont fondamentalement différentes dans 1.7 - voir java-performance.info/changes-to-string-java-1-7-0_06 (en particulier, créer une sous-chaîne n'est plus O (1) mais plutôt O (n)). Le lien note que dans 1.6 Pattern.split a utilisé une création de chaîne différente de String.substring ()) - voir le code lié dans le commentaire ci-dessus pour suivre StringTokenizer.nextToken () et le constructeur privé du package auquel il avait accès.
1

C'est la méthode que j'utilise pour analyser de gros fichiers (1 Go +) délimités par des tabulations. Il a beaucoup moins de frais généraux que String.split(), mais est limité à charun délimiteur. Si quelqu'un a une méthode plus rapide, j'aimerais la voir. Cela peut également être fait sur CharSequenceet CharSequence.subSequence, mais cela nécessite une implémentation CharSequence.indexOf(char)(reportez-vous à la méthode du package String.indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)si vous êtes intéressé).

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
la source
Avez-vous évalué ce vs String.split ()? Si oui, comment se compare-t-il?
Jay Elston
@JayElston Sur un fichier de 900 Mo, il a réduit le temps intermédiaire de 7,7 secondes à 6,2 secondes, soit environ 20% plus rapidement. C'est toujours la partie la plus lente de mon analyse matricielle à virgule flottante. Je suppose que la plupart du temps restant est l'allocation de tableaux. Il pourrait être possible de couper l'allocation de matrice en utilisant une approche basée sur un tokenizer avec un décalage dans la méthode - qui commencerait à ressembler davantage à la méthode que j'ai citée ci-dessus le code.
vallismortis