web-dev-qa-db-fra.com

Moyen le plus rapide de supprimer tous les caractères non imprimables d'une chaîne Java

Quel est le moyen le plus rapide de supprimer tous les caractères non imprimables d'une String en Java?

Jusqu'ici, j'ai essayé et mesuré une chaîne de 138 octets et 131 caractères:

  • La chaîne replaceAll() - méthode la plus lente .____.
    • 517009 résultats/sec
  • Précompilez un modèle, puis utilisez replaceAll().__ de Matcher.
    • 637836 résultats/sec
  • Utilisez StringBuffer, obtenez des points de code en utilisant codepointAt() un par un et ajoutez-le à StringBuffer
    • 711946 résultats/sec
  • Utilisez StringBuffer, obtenez des caractères en utilisant charAt() un par un et ajoutez-les à StringBuffer
    • 1052964 résultats/sec
  • Préallouez un tampon char[], récupérez les caractères en utilisant charAt() un par un et remplissez ce tampon, puis reconvertissez-le en chaîne
    • 2022653 résultats/sec
  • Preallocate 2 char[] buffers - anciens et nouveaux, récupérez tous les caractères de String existants en une seule fois avec getChars(), parcourez les anciens tampons un à un et remplissez les nouveaux tampons, puis convertissez les nouveaux tampons en String - ma version la plus rapide
    • 2502502 résultats/sec
  • Même chose avec 2 tampons - n'utilise que byte[], getBytes() et spécifie le codage comme "utf-8"
    • 857485 résultats/sec
  • Même chose avec 2 tampons byte[], mais en spécifiant le codage comme constante Charset.forName("utf-8")
    • 791076 résultats/sec
  • Même chose avec 2 tampons byte[], mais en spécifiant le codage comme codage local à 1 octet (chose à peine saine à faire)
    • 370164 résultats/sec

Mon meilleur essai a été le suivant:

    char[] oldChars = new char[s.length()];
    s.getChars(0, s.length(), oldChars, 0);
    char[] newChars = new char[s.length()];
    int newLen = 0;
    for (int j = 0; j < s.length(); j++) {
        char ch = oldChars[j];
        if (ch >= ' ') {
            newChars[newLen] = ch;
            newLen++;
        }
    }
    s = new String(newChars, 0, newLen);

Des idées sur la façon de le rendre encore plus rapide?

Des points bonus pour répondre à une question très étrange: pourquoi utiliser directement le nom du jeu de caractères "utf-8" donne de meilleures performances que d'utiliser une constante statique pré-allouée Charset.forName("utf-8")?

Mettre à jour

  • La suggestion de freaket freak donne des résultats impressionnants de 3105590/sec, soit une amélioration de + 24%!
  • La suggestion de Ed Staub donne une autre amélioration: 3471017 résultats/s, soit + 12% par rapport au meilleur résultat précédent.

Mise à jour 2

J'ai fait de mon mieux pour rassembler toutes les solutions proposées et ses mutations croisées et les publier sous la forme d'un petit cadre d'analyse comparative sur github . Actuellement, il contient 17 algorithmes. L’un d’eux est "spécial" - l’algorithme Voo1 ( fourni par SO utilisateur Voo ) utilise des astuces de réflexion complexes pour atteindre des vitesses stellaires, mais il perturbe l’état des chaînes JVM, il est comparé séparément.

Vous pouvez le vérifier et l'exécuter pour déterminer les résultats sur votre boîte. Voici un résumé des résultats que j'ai sur le mien. C'est specs:

  • Debian sid
  • Linux 2.6.39-2-AMD64 (x86_64)
  • Java installé à partir d'un package Sun-Java6-jdk-6.24-1, JVM s'identifie sous le nom de
    • Environnement d'exécution Java SE (version 1.6.0_24-b07)
    • Serveur Java 64 bits Java HotSpot VM (version 19.1-b02, mode mixte)

Différents algorithmes montrent finalement des résultats différents en fonction d'un ensemble différent de données d'entrée. J'ai couru un benchmark dans 3 modes:

Même chaîne simple

Ce mode fonctionne sur une même chaîne unique fournie par la classe StringSource en tant que constante. La confrontation est:

 Ops/s │ Algorithme 
 ──────────┼──────────────────────────────
 6 535 947 │ Voo1 
 350 
 5 350 454 RatchetF2EdStaub1GreyCat1 
 5 249 343 │ EdStaub1 
 5 002 50 __. 2 790 178 │ RatchetFreak2EdStaub1GreyCat2 
 2 583 311 RatchetFreak2 
 1 274 859 StringBuilderChar 
 1 138 174 StringBuilderCodePoint 
 994 727 │ ArrayOfByteUTF8String 
 918 611 │ ArrayOfByteUTF8Const 
 756 086 │ MatcherReplace 
 598 945 │ StringReplaceAll 
 460 045 ArrayOfByteWindows1251 

Sous forme de graphique: Même graphique à chaîne unique http://www.greycat.ru/img/os-chart-single.png

Plusieurs chaînes, 100% des chaînes contiennent des caractères de contrôle

De nombreuses chaînes aléatoires ont été générées par le fournisseur de chaîne source à l'aide du jeu de caractères (0..127) - ainsi, presque toutes les chaînes contiennent au moins un caractère de contrôle. Les algorithmes ont reçu les chaînes de ce tableau pré-généré en alternance.

 Ops/s │ Algorithme 
 ──────────┼──────────────────────────────
 2 123 142 │ Voo1 
 ──────────┼───────────────────────────── B __. __ au au │ │ at at at at at at at at at at at at at at at at at at at at at at at at at at at at at at at __. 1 366 494 │ RatchetFreak2 
 1 349 710 RatchetFreak1 
 893 176 ArrayOfByteUTF8String 
 817 127 │ ArrayOfByteUTF8Const 
 778 089 │ StringBuilderChar 
 734 754 │ StringBuilderCodePoint 
 377 829 │ ArrayOfByteWindows1251 
 224 140 │ MatcherReplace 
 211 104 │ StringReplaceAll 
Sous forme de graphique: 
 Chaînes multiples, concentration 100% http://www.greycat.ru/img/os-chart-multi100.png

Plusieurs chaînes, 1% des chaînes contiennent des caractères de contrôle

Identique à la précédente, mais seulement 1% des chaînes ont été générées avec des caractères de contrôle. Les 99% restants ont été générés à l'aide du jeu de caractères [32..127]. Ils ne pouvaient donc pas contenir de caractères de contrôle. Cette charge synthétique est l’application la plus proche de la réalité de cet algorithme chez moi.

Ops/s │ Algorithme ──────────┼────────────────────────────── 3 711 952 │ Voo1 ──────────┼───────────────────────────── __ 2 .Ordre. 1 __. 1 922 707 │ RatchetFreak2EdStaub1GreyCat1 1 857 010 RatchetFreak2 1 023 751 ArrayOfByteUTF8String 939 055 │ StringBuilderChar 907 194 │ ArrayOfByteUTF8Const 841 963 │ StringBuilderCodePoint 606 465 │ MatcherReplace 501 555 │ StringReplaceAll 381 185 ArrayOfByteWindows1251

Sous forme de graphique: 
 Chaînes multiples, concentration de 1% http://www.greycat.ru/img/os-chart-multi1.png

Il m'est très difficile de décider qui a fourni la meilleure réponse, mais étant donné l'application réelle, la meilleure solution a été donnée/inspirée par Ed Staub, je suppose qu'il serait juste de marquer sa réponse. Merci à tous ceux qui ont pris part à cela, votre contribution a été très utile et inestimable. N'hésitez pas à lancer la suite de tests sur votre machine et à proposer des solutions encore meilleures (solution JNI opérationnelle, ça vous tente?).

Références.

76
GreyCat

S'il est raisonnable d'intégrer cette méthode dans une classe qui n'est pas partagée entre les threads, vous pouvez réutiliser le tampon: 

char [] oldChars = new char[5];

String stripControlChars(String s)
{
    final int inputLen = s.length();
    if ( oldChars.length < inputLen )
    {
        oldChars = new char[inputLen];
    }
    s.getChars(0, inputLen, oldChars, 0);

etc...

C'est une grosse victoire - environ 20%, si je comprends bien le meilleur des cas.

Si cela doit être utilisé sur des chaînes potentiellement volumineuses et que la "fuite" de mémoire pose problème, une référence faible peut être utilisée.

9
Ed Staub

en utilisant 1 tableau de caractères pourrait fonctionner un peu mieux

int length = s.length();
char[] oldChars = new char[length];
s.getChars(0, length, oldChars, 0);
int newLen = 0;
for (int j = 0; j < length; j++) {
    char ch = oldChars[j];
    if (ch >= ' ') {
        oldChars[newLen] = ch;
        newLen++;
    }
}
s = new String(oldChars, 0, newLen);

et j'ai évité les appels répétés à s.length();

une autre micro-optimisation qui pourrait fonctionner est

int length = s.length();
char[] oldChars = new char[length+1];
s.getChars(0, length, oldChars, 0);
oldChars[length]='\0';//avoiding explicit bound check in while
int newLen=-1;
while(oldChars[++newLen]>=' ');//find first non-printable,
                       // if there are none it ends on the null char I appended
for (int  j = newLen; j < length; j++) {
    char ch = oldChars[j];
    if (ch >= ' ') {
        oldChars[newLen] = ch;//the while avoids repeated overwriting here when newLen==j
        newLen++;
    }
}
s = new String(oldChars, 0, newLen);
24
ratchet freak

Eh bien, j'ai battu la meilleure méthode actuelle (la solution de freak avec le tableau préalloué) d'environ 30% selon mes mesures. Comment? En vendant mon âme.

Comme je suis sûr que tous ceux qui ont suivi la discussion jusqu'à présent savent que cela enfreint à peu près tous les principes de base de la programmation, mais bon. Quoi qu'il en soit, ce qui suit ne fonctionne que si le tableau de caractères utilisé de la chaîne n'est pas partagé entre d'autres chaînes. S'il le fait, celui qui doit le déboguer aura le droit de décider de vous tuer (sans appel à substring () et de l'utiliser avec des chaînes littérales). cela devrait fonctionner car je ne vois pas pourquoi la machine virtuelle Java internaliserait des chaînes uniques lues depuis une source externe). Cependant, n'oubliez pas de vous assurer que le code de référence ne le fait pas - c'est extrêmement probable et aiderait évidemment la solution de réflexion.

En tout cas on y va:

    // Has to be done only once - so cache those! Prohibitively expensive otherwise
    private Field value;
    private Field offset;
    private Field count;
    private Field hash;
    {
        try {
            value = String.class.getDeclaredField("value");
            value.setAccessible(true);
            offset = String.class.getDeclaredField("offset");
            offset.setAccessible(true);
            count = String.class.getDeclaredField("count");
            count.setAccessible(true);
            hash = String.class.getDeclaredField("hash");
            hash.setAccessible(true);               
        }
        catch (NoSuchFieldException e) {
            throw new RuntimeException();
        }

    }

    @Override
    public String strip(final String old) {
        final int length = old.length();
        char[] chars = null;
        int off = 0;
        try {
            chars = (char[]) value.get(old);
            off = offset.getInt(old);
        }
        catch(IllegalArgumentException e) {
            throw new RuntimeException(e);
        }
        catch(IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        int newLen = off;
        for(int j = off; j < off + length; j++) {
            final char ch = chars[j];
            if (ch >= ' ') {
                chars[newLen] = ch;
                newLen++;
            }
        }
        if (newLen - off != length) {
            // We changed the internal state of the string, so at least
            // be friendly enough to correct it.
            try {
                count.setInt(old, newLen - off);
                // Have to recompute hash later on
                hash.setInt(old, 0);
            }
            catch(IllegalArgumentException e) {
                e.printStackTrace();
            }
            catch(IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        // Well we have to return something
        return old;
    }

Pour mon teststring, 3477148.18ops/s vs 2616120.89ops/s pour l'ancienne variante. Je suis tout à fait sûr que la seule façon de battre cela pourrait être de l'écrire en C (probablement pas si) ou une approche complètement différente à laquelle personne n'a pensé jusqu'à présent. Bien que je ne sois absolument pas sûr que le timing soit stable sur différentes plates-formes, il produit au moins des résultats fiables sur ma machine (Java7, Win7 x64).

9
Voo

Vous pouvez diviser la tâche en plusieurs sous-tâches parallèles, en fonction de la quantité du processeur.

2
umbr

Junkie des performances Java bas niveau IANA, mais avez-vous essayé déroulant votre boucle principale ? Il semble que cela pourrait permettre à certains processeurs d'effectuer des contrôles en parallèle.

En outre, this a des idées amusantes d’optimisations.

1
Ryan Ransford

J'étais tellement libre et ai écrit un petit repère pour différents algorithmes. Ce n'est pas parfait, mais je prends le minimum de 1000 exécutions d'un algorithme donné 10000 fois sur une chaîne aléatoire (avec environ 32/200% de non imprimables par défaut). Cela devrait s’occuper de choses comme le GC, l’initialisation, etc. - il n’ya pas trop de frais généraux, aucun algorithme ne devrait avoir au moins une exécution sans trop de problèmes.

Pas particulièrement bien documenté, mais bon. C'est parti - J'ai inclus les deux algorithmes de Ratchet Freak et la version de base. Pour le moment, j'initialise de manière aléatoire une chaîne de 200 caractères avec des caractères uniformément répartis dans la plage [0, 200).

1
Voo

pourquoi utiliser "utf-8" nom de jeu de caractères donne directement de meilleures performances que d'utiliser stat statique pré-alloué Charset.forName ("utf-8")?

Si vous voulez dire String#getBytes("utf-8") etc .: Cela ne devrait pas être plus rapide - sauf pour une meilleure mise en cache - puisque Charset.forName("utf-8") est utilisé en interne, si le jeu de caractères n'est pas mis en cache.

Une chose peut être que vous utilisez différents jeux de caractères (ou peut-être que certains de vos codes fonctionnent de manière transparente), mais le jeu de caractères mis en cache dans StringCoding ne change pas.

0
Thomas