Le moyen le plus rapide d'itérer tous les caractères d'une chaîne


163

En Java, quel serait le moyen le plus rapide d'itérer sur tous les caractères d'une chaîne, ceci:

String str = "a really, really long string";
for (int i = 0, n = str.length(); i < n; i++) {
    char c = str.charAt(i);
}

Ou ca:

char[] chars = str.toCharArray();
for (int i = 0, n = chars.length; i < n; i++) {
    char c = chars[i];
}

ÉDITER :

Ce que j'aimerais savoir, c'est si le coût d'appels répétés de la charAtméthode pendant une longue itération finit par être inférieur ou supérieur au coût d'un seul appel toCharArrayau début, puis d'accéder directement au tableau pendant l'itération.

Ce serait formidable si quelqu'un pouvait fournir une référence robuste pour différentes longueurs de chaîne, en tenant compte du temps de préchauffage JIT, du temps de démarrage de la JVM, etc. et pas seulement de la différence entre deux appels à System.currentTimeMillis().


18
Qu'est-il arrivé for (char c : chars)?
dasblinkenlight

Le premier devrait être plus rapide, et de toute façon une chaîne un tableau de caractères, théoriquement.
Keagan Ladds

Google est souvent une bonne ressource: mkyong.com/java/…
Johan Sjöberg

2
La question ne demande pas la performance de l'utilisation des itérateurs, pour chacun. Ce que j'aimerais savoir, c'est si le coût des appels répétés charAtfinit par être inférieur ou supérieur au coût d'un seul appel àtoCharArray
Óscar López

1
Quelqu'un a-t-il fait une analyse avec StringCharacterIterator ?
bdrx

Réponses:


352

PREMIÈRE MISE À JOUR: Avant d'essayer cela dans un environnement de production (déconseillé), lisez d'abord ceci: http://www.javaspecialists.eu/archive/Issue237.html À partir de Java 9, la solution décrite ne fonctionnera plus , car maintenant Java stockera les chaînes sous forme d'octet [] par défaut.

DEUXIÈME MISE À JOUR: À partir du 25/10/2016, sur mon AMDx64 8core et la source 1.8, il n'y a aucune différence entre l'utilisation de 'charAt' et l'accès au champ. Il semble que le jvm soit suffisamment optimisé pour intégrer et rationaliser tous les appels 'string.charAt (n)'.

Tout dépend de la durée de l' Stringinspection. Si, comme le dit la question, il s'agit de longues chaînes, le moyen le plus rapide d'inspecter la chaîne est d'utiliser la réflexion pour accéder au support char[]de la chaîne.

Un benchmark entièrement randomisé avec JDK 8 (win32 et win64) sur un AMD Phenom II 4 core 955 @ 3.2 GHZ (en mode client et en mode serveur) avec 9 techniques différentes (voir ci-dessous!) Montre que l'utilisation String.charAt(n)est la plus rapide pour les petits strings et que l'utilisation reflectionpour accéder au tableau de sauvegarde String est presque deux fois plus rapide pour les grandes chaînes.

L'EXPÉRIENCE

  • 9 techniques d'optimisation différentes sont essayées.

  • Tout le contenu de la chaîne est aléatoire

  • Les tests sont effectués pour des tailles de chaîne par multiples de deux commençant par 0,1,2,4,8,16 etc.

  • Les tests sont effectués 1000 fois par taille de chaîne

  • Les tests sont mélangés dans un ordre aléatoire à chaque fois. En d'autres termes, les tests sont effectués dans un ordre aléatoire à chaque fois qu'ils sont effectués, plus de 1000 fois.

  • L'ensemble de la suite de tests est effectué en avant et en arrière pour montrer l'effet du préchauffage de la JVM sur l'optimisation et les temps.

  • L'ensemble de la suite se fait deux fois, une fois en -clientmode et l'autre en -servermode.

CONCLUSIONS

-mode client (32 bits)

Pour les chaînes de 1 à 256 caractères , l'appel l' string.charAt(i)emporte avec un traitement moyen de 13,4 millions à 588 millions de caractères par seconde.

En outre, il est globalement 5,5% plus rapide (client) et 13,9% (serveur) comme ceci:

    for (int i = 0; i < data.length(); i++) {
        if (data.charAt(i) <= ' ') {
            doThrow();
        }
    }

que comme ça avec une variable de longueur finale locale:

    final int len = data.length();
    for (int i = 0; i < len; i++) {
        if (data.charAt(i) <= ' ') {
            doThrow();
        }
    }

Pour les chaînes longues, d'une longueur de 512 à 256K caractères , l'utilisation de la réflexion pour accéder au tableau de sauvegarde de String est la plus rapide. Cette technique est presque deux fois plus rapide que String.charAt (i) (178% plus rapide). La vitesse moyenne sur cette plage était de 1,111 milliard de caractères par seconde.

Le champ doit être obtenu à l'avance, puis il peut être réutilisé dans la bibliothèque sur différentes chaînes. Fait intéressant, contrairement au code ci-dessus, avec l'accès au champ, il est 9% plus rapide d'avoir une variable de longueur finale locale que d'utiliser 'chars.length' dans la vérification de la boucle. Voici comment l'accès au champ peut être configuré le plus rapidement:

   final Field field = String.class.getDeclaredField("value");
   field.setAccessible(true);

   try {
       final char[] chars = (char[]) field.get(data);
       final int len = chars.length;
       for (int i = 0; i < len; i++) {
           if (chars[i] <= ' ') {
               doThrow();
           }
       }
       return len;
   } catch (Exception ex) {
       throw new RuntimeException(ex);
   }

Commentaires spéciaux sur le mode -server

L'accès aux champs commence à gagner après des chaînes de 32 caractères en mode serveur sur une machine Java 64 bits sur ma machine AMD 64. Cela n'a pas été vu avant une longueur de 512 caractères en mode client.

Il convient également de noter que lorsque j'exécutais JDK 8 (version 32 bits) en mode serveur, les performances globales étaient 7% plus lentes pour les grandes et les petites chaînes. C'était avec la build 121 décembre 2013 de la version anticipée du JDK 8. Donc, pour l'instant, il semble que le mode serveur 32 bits soit plus lent que le mode client 32 bits.

Cela étant dit ... il semble que le seul mode serveur qui mérite d'être invoqué soit sur une machine 64 bits. Sinon, cela nuit en fait aux performances.

Pour une version 32 bits fonctionnant -server modesur un AMD64, je peux dire ceci:

  1. String.charAt (i) est clairement le vainqueur. Bien qu'entre 8 et 512 caractères, il y avait des gagnants parmi les «nouvelles» «réutilisation» et «champ».
  2. String.charAt (i) est 45% plus rapide en mode client
  3. L'accès aux champs est deux fois plus rapide pour les grandes chaînes en mode client.

Il convient également de dire que String.chars () (Stream et la version parallèle) sont un buste. Bien plus lent que tout autre moyen. L' StreamsAPI est un moyen plutôt lent d'effectuer des opérations générales sur les chaînes.

Liste de souhaits

Java String peut avoir un prédicat acceptant des méthodes optimisées telles que contains (prédicat), forEach (consommateur), forEachWithIndex (consumer). Ainsi, sans que l'utilisateur ait besoin de connaître la longueur ou de répéter les appels aux méthodes String, celles-ci pourraient beep-beep beepaccélérer l' analyse des bibliothèques .

Continue de rêver :)

Joyeuses cordes!

~ SH

Le test a utilisé les 9 méthodes suivantes pour tester la chaîne pour la présence d'espaces blancs:

"charAt1" - VÉRIFIEZ LE CONTENU DE LA CHAÎNE DE LA MANIÈRE HABITUELLE:

int charAtMethod1(final String data) {
    final int len = data.length();
    for (int i = 0; i < len; i++) {
        if (data.charAt(i) <= ' ') {
            doThrow();
        }
    }
    return len;
}

"charAt2" - MÊME QUE CI-DESSUS, MAIS UTILISEZ String.length () AU LIEU DE FAIRE UN INT LOCAL FINAL POUR LA LONGUEUR

int charAtMethod2(final String data) {
    for (int i = 0; i < data.length(); i++) {
        if (data.charAt(i) <= ' ') {
            doThrow();
        }
    }
    return data.length();
}

"stream" - UTILISEZ LE NOUVEAU JAVA-8 String's IntStream ET PASSEZ-LE UN PRÉDICAT POUR FAIRE LA VÉRIFICATION

int streamMethod(final String data, final IntPredicate predicate) {
    if (data.chars().anyMatch(predicate)) {
        doThrow();
    }
    return data.length();
}

"streamPara" - MÊME QUE CI-DESSUS, MAIS OH-LA-LA - ALLEZ PARALLÈLE !!!

// avoid this at all costs
int streamParallelMethod(final String data, IntPredicate predicate) {
    if (data.chars().parallel().anyMatch(predicate)) {
        doThrow();
    }
    return data.length();
}

"réutiliser" - REMPLIR UN CARACTERISTIQUE RÉUTILISABLE [] AVEC LES CHAÎNES CONTENU

int reuseBuffMethod(final char[] reusable, final String data) {
    final int len = data.length();
    data.getChars(0, len, reusable, 0);
    for (int i = 0; i < len; i++) {
        if (reusable[i] <= ' ') {
            doThrow();
        }
    }
    return len;
}

"new1" - OBTENEZ UNE NOUVELLE COPIE DU caractère [] DE LA CHAÎNE

int newMethod1(final String data) {
    final int len = data.length();
    final char[] copy = data.toCharArray();
    for (int i = 0; i < len; i++) {
        if (copy[i] <= ' ') {
            doThrow();
        }
    }
    return len;
}

"new2" - MÊME QUE CI-DESSUS, MAIS UTILISEZ "FOR-EACH"

int newMethod2(final String data) {
    for (final char c : data.toCharArray()) {
        if (c <= ' ') {
            doThrow();
        }
    }
    return data.length();
}

"field1" - FANTAISIE !! OBTENIR UN CHAMP POUR ACCÉDER AU caractère INTERNE DE LA STRING []

int fieldMethod1(final Field field, final String data) {
    try {
        final char[] chars = (char[]) field.get(data);
        final int len = chars.length;
        for (int i = 0; i < len; i++) {
            if (chars[i] <= ' ') {
                doThrow();
            }
        }
        return len;
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

"field2" - MÊME QUE CI-DESSUS, MAIS UTILISEZ "FOR-EACH"

int fieldMethod2(final Field field, final String data) {
    final char[] chars;
    try {
        chars = (char[]) field.get(data);
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
    for (final char c : chars) {
        if (c <= ' ') {
            doThrow();
        }
    }
    return chars.length;
}

RÉSULTATS COMPOSITES POUR LE -clientMODE CLIENT (tests avant et arrière combinés)

Remarque: que le mode -client avec Java 32 bits et le mode -server avec Java 64 bits sont les mêmes que ci-dessous sur ma machine AMD64.

Size     WINNER  charAt1 charAt2  stream streamPar   reuse    new1    new2  field1  field2
1        charAt    77.0     72.0   462.0     584.0   127.5    89.5    86.0   159.5   165.0
2        charAt    38.0     36.5   284.0   32712.5    57.5    48.3    50.3    89.0    91.5
4        charAt    19.5     18.5   458.6    3169.0    33.0    26.8    27.5    54.1    52.6
8        charAt     9.8      9.9   100.5    1370.9    17.3    14.4    15.0    26.9    26.4
16       charAt     6.1      6.5    73.4     857.0     8.4     8.2     8.3    13.6    13.5
32       charAt     3.9      3.7    54.8     428.9     5.0     4.9     4.7     7.0     7.2
64       charAt     2.7      2.6    48.2     232.9     3.0     3.2     3.3     3.9     4.0
128      charAt     2.1      1.9    43.7     138.8     2.1     2.6     2.6     2.4     2.6
256      charAt     1.9      1.6    42.4      90.6     1.7     2.1     2.1     1.7     1.8
512      field1     1.7      1.4    40.6      60.5     1.4     1.9     1.9     1.3     1.4
1,024    field1     1.6      1.4    40.0      45.6     1.2     1.9     2.1     1.0     1.2
2,048    field1     1.6      1.3    40.0      36.2     1.2     1.8     1.7     0.9     1.1
4,096    field1     1.6      1.3    39.7      32.6     1.2     1.8     1.7     0.9     1.0
8,192    field1     1.6      1.3    39.6      30.5     1.2     1.8     1.7     0.9     1.0
16,384   field1     1.6      1.3    39.8      28.4     1.2     1.8     1.7     0.8     1.0
32,768   field1     1.6      1.3    40.0      26.7     1.3     1.8     1.7     0.8     1.0
65,536   field1     1.6      1.3    39.8      26.3     1.3     1.8     1.7     0.8     1.0
131,072  field1     1.6      1.3    40.1      25.4     1.4     1.9     1.8     0.8     1.0
262,144  field1     1.6      1.3    39.6      25.2     1.5     1.9     1.9     0.8     1.0

RÉSULTATS COMPOSITES POUR LE -serverMODE SERVEUR (tests avant et arrière combinés)

Remarque: il s'agit du test de Java 32 bits fonctionnant en mode serveur sur un AMD64. Le mode serveur pour Java 64 bits était le même que pour Java 32 bits en mode client, sauf que l'accès aux champs commençait à gagner après une taille de 32 caractères.

Size     WINNER  charAt1 charAt2  stream streamPar   reuse    new1    new2  field1  field2
1        charAt     74.5    95.5   524.5     783.0    90.5   102.5    90.5   135.0   151.5
2        charAt     48.5    53.0   305.0   30851.3    59.3    57.5    52.0    88.5    91.8
4        charAt     28.8    32.1   132.8    2465.1    37.6    33.9    32.3    49.0    47.0
8          new2     18.0    18.6    63.4    1541.3    18.5    17.9    17.6    25.4    25.8
16         new2     14.0    14.7   129.4    1034.7    12.5    16.2    12.0    16.0    16.6
32         new2      7.8     9.1    19.3     431.5     8.1     7.0     6.7     7.9     8.7
64        reuse      6.1     7.5    11.7     204.7     3.5     3.9     4.3     4.2     4.1
128       reuse      6.8     6.8     9.0     101.0     2.6     3.0     3.0     2.6     2.7
256      field2      6.2     6.5     6.9      57.2     2.4     2.7     2.9     2.3     2.3
512       reuse      4.3     4.9     5.8      28.2     2.0     2.6     2.6     2.1     2.1
1,024    charAt      2.0     1.8     5.3      17.6     2.1     2.5     3.5     2.0     2.0
2,048    charAt      1.9     1.7     5.2      11.9     2.2     3.0     2.6     2.0     2.0
4,096    charAt      1.9     1.7     5.1       8.7     2.1     2.6     2.6     1.9     1.9
8,192    charAt      1.9     1.7     5.1       7.6     2.2     2.5     2.6     1.9     1.9
16,384   charAt      1.9     1.7     5.1       6.9     2.2     2.5     2.5     1.9     1.9
32,768   charAt      1.9     1.7     5.1       6.1     2.2     2.5     2.5     1.9     1.9
65,536   charAt      1.9     1.7     5.1       5.5     2.2     2.4     2.4     1.9     1.9
131,072  charAt      1.9     1.7     5.1       5.4     2.3     2.5     2.5     1.9     1.9
262,144  charAt      1.9     1.7     5.1       5.1     2.3     2.5     2.5     1.9     1.9

CODE DE PROGRAMME COMPLET EXÉCUTABLE

(pour tester sur Java 7 et versions antérieures, supprimez les tests des deux flux)

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.function.IntPredicate;

/**
 * @author Saint Hill <http://stackoverflow.com/users/1584255/saint-hill>
 */
public final class TestStrings {

    // we will not test strings longer than 512KM
    final int MAX_STRING_SIZE = 1024 * 256;

    // for each string size, we will do all the tests
    // this many times
    final int TRIES_PER_STRING_SIZE = 1000;

    public static void main(String[] args) throws Exception {
        new TestStrings().run();
    }

    void run() throws Exception {

        // double the length of the data until it reaches MAX chars long
        // 0,1,2,4,8,16,32,64,128,256 ... 
        final List<Integer> sizes = new ArrayList<>();
        for (int n = 0; n <= MAX_STRING_SIZE; n = (n == 0 ? 1 : n * 2)) {
            sizes.add(n);
        }

        // CREATE RANDOM (FOR SHUFFLING ORDER OF TESTS)
        final Random random = new Random();

        System.out.println("Rate in nanoseconds per character inspected.");
        System.out.printf("==== FORWARDS (tries per size: %s) ==== \n", TRIES_PER_STRING_SIZE);

        printHeadings(TRIES_PER_STRING_SIZE, random);

        for (int size : sizes) {
            reportResults(size, test(size, TRIES_PER_STRING_SIZE, random));
        }

        // reverse order or string sizes
        Collections.reverse(sizes);

        System.out.println("");
        System.out.println("Rate in nanoseconds per character inspected.");
        System.out.printf("==== BACKWARDS (tries per size: %s) ==== \n", TRIES_PER_STRING_SIZE);

        printHeadings(TRIES_PER_STRING_SIZE, random);

        for (int size : sizes) {
            reportResults(size, test(size, TRIES_PER_STRING_SIZE, random));

        }
    }

    ///
    ///
    ///  METHODS OF CHECKING THE CONTENTS
    ///  OF A STRING. ALWAYS CHECKING FOR
    ///  WHITESPACE (CHAR <=' ')
    ///  
    ///
    // CHECK THE STRING CONTENTS
    int charAtMethod1(final String data) {
        final int len = data.length();
        for (int i = 0; i < len; i++) {
            if (data.charAt(i) <= ' ') {
                doThrow();
            }
        }
        return len;
    }

    // SAME AS ABOVE BUT USE String.length()
    // instead of making a new final local int 
    int charAtMethod2(final String data) {
        for (int i = 0; i < data.length(); i++) {
            if (data.charAt(i) <= ' ') {
                doThrow();
            }
        }
        return data.length();
    }

    // USE new Java-8 String's IntStream
    // pass it a PREDICATE to do the checking
    int streamMethod(final String data, final IntPredicate predicate) {
        if (data.chars().anyMatch(predicate)) {
            doThrow();
        }
        return data.length();
    }

    // OH LA LA - GO PARALLEL!!!
    int streamParallelMethod(final String data, IntPredicate predicate) {
        if (data.chars().parallel().anyMatch(predicate)) {
            doThrow();
        }
        return data.length();
    }

    // Re-fill a resuable char[] with the contents
    // of the String's char[]
    int reuseBuffMethod(final char[] reusable, final String data) {
        final int len = data.length();
        data.getChars(0, len, reusable, 0);
        for (int i = 0; i < len; i++) {
            if (reusable[i] <= ' ') {
                doThrow();
            }
        }
        return len;
    }

    // Obtain a new copy of char[] from String
    int newMethod1(final String data) {
        final int len = data.length();
        final char[] copy = data.toCharArray();
        for (int i = 0; i < len; i++) {
            if (copy[i] <= ' ') {
                doThrow();
            }
        }
        return len;
    }

    // Obtain a new copy of char[] from String
    // but use FOR-EACH
    int newMethod2(final String data) {
        for (final char c : data.toCharArray()) {
            if (c <= ' ') {
                doThrow();
            }
        }
        return data.length();
    }

    // FANCY!
    // OBTAIN FIELD FOR ACCESS TO THE STRING'S
    // INTERNAL CHAR[]
    int fieldMethod1(final Field field, final String data) {
        try {
            final char[] chars = (char[]) field.get(data);
            final int len = chars.length;
            for (int i = 0; i < len; i++) {
                if (chars[i] <= ' ') {
                    doThrow();
                }
            }
            return len;
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    // same as above but use FOR-EACH
    int fieldMethod2(final Field field, final String data) {
        final char[] chars;
        try {
            chars = (char[]) field.get(data);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        for (final char c : chars) {
            if (c <= ' ') {
                doThrow();
            }
        }
        return chars.length;
    }

    /**
     *
     * Make a list of tests. We will shuffle a copy of this list repeatedly
     * while we repeat this test.
     *
     * @param data
     * @return
     */
    List<Jobber> makeTests(String data) throws Exception {
        // make a list of tests
        final List<Jobber> tests = new ArrayList<Jobber>();

        tests.add(new Jobber("charAt1") {
            int check() {
                return charAtMethod1(data);
            }
        });

        tests.add(new Jobber("charAt2") {
            int check() {
                return charAtMethod2(data);
            }
        });

        tests.add(new Jobber("stream") {
            final IntPredicate predicate = new IntPredicate() {
                public boolean test(int value) {
                    return value <= ' ';
                }
            };

            int check() {
                return streamMethod(data, predicate);
            }
        });

        tests.add(new Jobber("streamPar") {
            final IntPredicate predicate = new IntPredicate() {
                public boolean test(int value) {
                    return value <= ' ';
                }
            };

            int check() {
                return streamParallelMethod(data, predicate);
            }
        });

        // Reusable char[] method
        tests.add(new Jobber("reuse") {
            final char[] cbuff = new char[MAX_STRING_SIZE];

            int check() {
                return reuseBuffMethod(cbuff, data);
            }
        });

        // New char[] from String
        tests.add(new Jobber("new1") {
            int check() {
                return newMethod1(data);
            }
        });

        // New char[] from String
        tests.add(new Jobber("new2") {
            int check() {
                return newMethod2(data);
            }
        });

        // Use reflection for field access
        tests.add(new Jobber("field1") {
            final Field field;

            {
                field = String.class.getDeclaredField("value");
                field.setAccessible(true);
            }

            int check() {
                return fieldMethod1(field, data);
            }
        });

        // Use reflection for field access
        tests.add(new Jobber("field2") {
            final Field field;

            {
                field = String.class.getDeclaredField("value");
                field.setAccessible(true);
            }

            int check() {
                return fieldMethod2(field, data);
            }
        });

        return tests;
    }

    /**
     * We use this class to keep track of test results
     */
    abstract class Jobber {

        final String name;
        long nanos;
        long chars;
        long runs;

        Jobber(String name) {
            this.name = name;
        }

        abstract int check();

        final double nanosPerChar() {
            double charsPerRun = chars / runs;
            long nanosPerRun = nanos / runs;
            return charsPerRun == 0 ? nanosPerRun : nanosPerRun / charsPerRun;
        }

        final void run() {
            runs++;
            long time = System.nanoTime();
            chars += check();
            nanos += System.nanoTime() - time;
        }
    }

    // MAKE A TEST STRING OF RANDOM CHARACTERS A-Z
    private String makeTestString(int testSize, char start, char end) {
        Random r = new Random();
        char[] data = new char[testSize];
        for (int i = 0; i < data.length; i++) {
            data[i] = (char) (start + r.nextInt(end));
        }
        return new String(data);
    }

    // WE DO THIS IF WE FIND AN ILLEGAL CHARACTER IN THE STRING
    public void doThrow() {
        throw new RuntimeException("Bzzzt -- Illegal Character!!");
    }

    /**
     * 1. get random string of correct length 2. get tests (List<Jobber>) 3.
     * perform tests repeatedly, shuffling each time
     */
    List<Jobber> test(int size, int tries, Random random) throws Exception {
        String data = makeTestString(size, 'A', 'Z');
        List<Jobber> tests = makeTests(data);
        List<Jobber> copy = new ArrayList<>(tests);
        while (tries-- > 0) {
            Collections.shuffle(copy, random);
            for (Jobber ti : copy) {
                ti.run();
            }
        }
        // check to make sure all char counts the same
        long runs = tests.get(0).runs;
        long count = tests.get(0).chars;
        for (Jobber ti : tests) {
            if (ti.runs != runs && ti.chars != count) {
                throw new Exception("Char counts should match if all correct algorithms");
            }
        }
        return tests;
    }

    private void printHeadings(final int TRIES_PER_STRING_SIZE, final Random random) throws Exception {
        System.out.print("  Size");
        for (Jobber ti : test(0, TRIES_PER_STRING_SIZE, random)) {
            System.out.printf("%9s", ti.name);
        }
        System.out.println("");
    }

    private void reportResults(int size, List<Jobber> tests) {
        System.out.printf("%6d", size);
        for (Jobber ti : tests) {
            System.out.printf("%,9.2f", ti.nanosPerChar());
        }
        System.out.println("");
    }
}

1
Ce test a-t-il été exécuté dans la JVM serveur ou la JVM client? Les meilleures optimisations ne sont effectuées que dans la JVM serveur. Si vous avez exécuté la JVM 32 bits par défaut et aucun argument, vous avez exécuté en mode client.
ceklock

2
Obtenir le tampon de sauvegarde est problématique dans le cas de sous-chaînes ou de chaînes créées à l'aide de String (char [], int, int), car vous obtenez le tampon entier (au moins sur Android), mais votre indexation sera basée sur zéro. Cependant, si vous savez que vous n'avez pas de sous-chaîne, cela fonctionnera correctement.
prewett

5
Une idée pourquoi "for (int i = 0; i <data.length (); i ++)" est plus rapide que de définir data.length () comme variable locale finale?
skyin

2
La définition d'une variable, du tout, nécessite une opération de pile dans le code d'octet de la méthode. Mais les optimisations, issues de la reconnaissance de votre algorithme, pourraient accélérer cette opération de répétition dans le code machine réel, sans la surcharge de la répartition des variables. De telles optimisations existent parfois dans les compilateurs de bytecode, parfois non. Tout dépend si le jvm est assez intelligent :-)
The Coordinator

2
@DavidS les nombres sont le taux (en nanosecondes) par caractère inspecté. Plus petit est meilleur.
The Coordinator

14

Il ne s'agit que d'une micro-optimisation dont vous ne devriez pas vous inquiéter.

char[] chars = str.toCharArray();

vous renvoie une copie des strtableaux de caractères (en JDK, il renvoie une copie des caractères en appelant System.arrayCopy).

En dehors de cela, str.charAt()vérifie uniquement si l'index est effectivement dans les limites et renvoie un caractère dans l'index du tableau.

Le premier ne crée pas de mémoire supplémentaire dans JVM.


Ne répond pas à la question. Cette question concerne les performances. Pour autant que vous sachiez, l'OP a peut-être découvert que l'itération sur des chaînes est un coût majeur dans leur application.
rghome

9

Juste pour la curiosité et pour comparer avec la réponse de Saint Hill.

Si vous avez besoin de traiter des données lourdes, vous ne devez pas utiliser JVM en mode client. Le mode client n'est pas fait pour les optimisations.

Comparons les résultats des benchmarks @Saint Hill en utilisant une JVM en mode Client et en mode Serveur.

Core2Quad Q6600 G0 @ 2.4GHz
JavaSE 1.7.0_40

Voir aussi: De vraies différences entre "java -server" et "java -client"?


MODE CLIENT:

len =      2:    111k charAt(i),  105k cbuff[i],   62k new[i],   17k field access.   (chars/ms) 
len =      4:    285k charAt(i),  166k cbuff[i],  114k new[i],   43k field access.   (chars/ms) 
len =      6:    315k charAt(i),  230k cbuff[i],  162k new[i],   69k field access.   (chars/ms) 
len =      8:    333k charAt(i),  275k cbuff[i],  181k new[i],   85k field access.   (chars/ms) 
len =     12:    342k charAt(i),  342k cbuff[i],  222k new[i],  117k field access.   (chars/ms) 
len =     16:    363k charAt(i),  347k cbuff[i],  275k new[i],  152k field access.   (chars/ms) 
len =     20:    363k charAt(i),  392k cbuff[i],  289k new[i],  180k field access.   (chars/ms) 
len =     24:    375k charAt(i),  428k cbuff[i],  311k new[i],  205k field access.   (chars/ms) 
len =     28:    378k charAt(i),  474k cbuff[i],  341k new[i],  233k field access.   (chars/ms) 
len =     32:    376k charAt(i),  492k cbuff[i],  340k new[i],  251k field access.   (chars/ms) 
len =     64:    374k charAt(i),  551k cbuff[i],  374k new[i],  367k field access.   (chars/ms) 
len =    128:    385k charAt(i),  624k cbuff[i],  415k new[i],  509k field access.   (chars/ms) 
len =    256:    390k charAt(i),  675k cbuff[i],  436k new[i],  619k field access.   (chars/ms) 
len =    512:    394k charAt(i),  703k cbuff[i],  439k new[i],  695k field access.   (chars/ms) 
len =   1024:    395k charAt(i),  718k cbuff[i],  462k new[i],  742k field access.   (chars/ms) 
len =   2048:    396k charAt(i),  725k cbuff[i],  471k new[i],  767k field access.   (chars/ms) 
len =   4096:    396k charAt(i),  727k cbuff[i],  459k new[i],  780k field access.   (chars/ms) 
len =   8192:    397k charAt(i),  712k cbuff[i],  446k new[i],  772k field access.   (chars/ms) 

MODE SERVEUR:

len =      2:     86k charAt(i),   41k cbuff[i],   46k new[i],   80k field access.   (chars/ms) 
len =      4:    571k charAt(i),  250k cbuff[i],   97k new[i],  222k field access.   (chars/ms) 
len =      6:    666k charAt(i),  333k cbuff[i],  125k new[i],  315k field access.   (chars/ms) 
len =      8:    800k charAt(i),  400k cbuff[i],  181k new[i],  380k field access.   (chars/ms) 
len =     12:    800k charAt(i),  521k cbuff[i],  260k new[i],  545k field access.   (chars/ms) 
len =     16:    800k charAt(i),  592k cbuff[i],  296k new[i],  640k field access.   (chars/ms) 
len =     20:    800k charAt(i),  666k cbuff[i],  408k new[i],  800k field access.   (chars/ms) 
len =     24:    800k charAt(i),  705k cbuff[i],  452k new[i],  800k field access.   (chars/ms) 
len =     28:    777k charAt(i),  736k cbuff[i],  368k new[i],  933k field access.   (chars/ms) 
len =     32:    800k charAt(i),  780k cbuff[i],  571k new[i],  969k field access.   (chars/ms) 
len =     64:    800k charAt(i),  901k cbuff[i],  800k new[i],  1306k field access.   (chars/ms) 
len =    128:    1084k charAt(i),  888k cbuff[i],  633k new[i],  1620k field access.   (chars/ms) 
len =    256:    1122k charAt(i),  966k cbuff[i],  729k new[i],  1790k field access.   (chars/ms) 
len =    512:    1163k charAt(i),  1007k cbuff[i],  676k new[i],  1910k field access.   (chars/ms) 
len =   1024:    1179k charAt(i),  1027k cbuff[i],  698k new[i],  1954k field access.   (chars/ms) 
len =   2048:    1184k charAt(i),  1043k cbuff[i],  732k new[i],  2007k field access.   (chars/ms) 
len =   4096:    1188k charAt(i),  1049k cbuff[i],  742k new[i],  2031k field access.   (chars/ms) 
len =   8192:    1157k charAt(i),  1032k cbuff[i],  723k new[i],  2048k field access.   (chars/ms) 

CONCLUSION:

Comme vous pouvez le voir, le mode serveur est beaucoup plus rapide.


2
Merci d'avoir posté. Ainsi, pour les grandes chaînes, l'accès aux champs est toujours 2x plus rapide que charAt (). En fait, l'accès aux champs est devenu encore plus rapide dans l'ensemble avec une avance de 28 chaînes de longueur (fou !!) Donc ... le mode serveur rend tout plus rapide. Très intéressant!
The Coordinator

1
Ouais, la méthode réfléchissante est vraiment plus rapide. Intéressant.
ceklock

2
btw: les JVM les plus récentes déterminent automatiquement lequel des -serveur ou -client qui fonctionne le mieux (généralement): docs.oracle.com/javase/7/docs/technotes/guides/vm/…
jontejj

2
@jontejj en pratique ce n'est pas si simple. Si vous exécutez une JVM 32 bits sous Windows, la JVM sera toujours par défaut client.
ceklock

7

Le premier à utiliser str.charAtdevrait être plus rapide.

Si vous creusez à l'intérieur du code source de la Stringclasse, nous pouvons voir qu'il charAtest implémenté comme suit:

public char charAt(int index) {
    if ((index < 0) || (index >= count)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return value[index + offset];
}

Ici, il ne fait qu'indexer un tableau et renvoyer la valeur.

Maintenant, si nous voyons l'implémentation de toCharArray, nous trouverons ci-dessous:

public char[] toCharArray() {
    char result[] = new char[count];
    getChars(0, count, result, 0);
    return result;
}

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    if (srcBegin < 0) {
        throw new StringIndexOutOfBoundsException(srcBegin);
    }
    if (srcEnd > count) {
        throw new StringIndexOutOfBoundsException(srcEnd);
    }
    if (srcBegin > srcEnd) {
        throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
    }
    System.arraycopy(value, offset + srcBegin, dst, dstBegin,
         srcEnd - srcBegin);
}

Comme vous le voyez, il fait un System.arraycopyqui va certainement être un peu plus lent que de ne pas le faire.


2
C'est idiot que le String # charAt fasse une vérification d'index supplémentaire, quand l'index est vérifié de toute façon lors de l'accès au tableau.
Ingo

1
Au risque de relancer un thread vieux de 8 ans ... Le tableau char derrière une chaîne peut être plus grand que la chaîne elle-même. Autrement dit, si vous aviez une chaîne "abcde" et que vous utilisiez ensuite une sous-chaîne pour extraire "bcd" dans une nouvelle chaîne, la nouvelle chaîne sera sauvegardée par le même tableau de caractères que la première chaîne. C'est pourquoi la classe string maintient un offset et un compte - afin de savoir quels caractères du tableau sont ceux qui représentent cette chaîne. La vérification de la plage est donc importante sinon il serait possible d'accéder aux caractères au-delà des extrémités de cette chaîne.
dty

3

Malgré la réponse de @Saint Hill si vous considérez la complexité temporelle de str.toCharArray () ,

le premier est plus rapide même pour les très grosses cordes. Vous pouvez exécuter le code ci-dessous pour le voir par vous-même.

        char [] ch = new char[1_000_000_00];
    String str = new String(ch); // to create a large string

    // ---> from here
    long currentTime = System.nanoTime();
    for (int i = 0, n = str.length(); i < n; i++) {
        char c = str.charAt(i);
    }
    // ---> to here
    System.out.println("str.charAt(i):"+(System.nanoTime()-currentTime)/1000000.0 +" (ms)");

    /**
     *   ch = str.toCharArray() itself takes lots of time   
     */
    // ---> from here
    currentTime = System.nanoTime();
    ch = str.toCharArray();
    for (int i = 0, n = str.length(); i < n; i++) {
        char c = ch[i];
    }
    // ---> to  here
    System.out.println("ch = str.toCharArray() + c = ch[i] :"+(System.nanoTime()-currentTime)/1000000.0 +" (ms)");

production:

str.charAt(i):5.492102 (ms)
ch = str.toCharArray() + c = ch[i] :79.400064 (ms)

2

On dirait que niether est plus rapide ou plus lent

    public static void main(String arguments[]) {


        //Build a long string
        StringBuilder sb = new StringBuilder();
        for(int j = 0; j < 10000; j++) {
            sb.append("a really, really long string");
        }
        String str = sb.toString();
        for (int testscount = 0; testscount < 10; testscount ++) {


            //Test 1
            long start = System.currentTimeMillis();
            for(int c = 0; c < 10000000; c++) {
                for (int i = 0, n = str.length(); i < n; i++) {
                    char chr = str.charAt(i);
                    doSomethingWithChar(chr);//To trick JIT optimistaion
                }
            }

            System.out.println("1: " + (System.currentTimeMillis() - start));

            //Test 2
            start = System.currentTimeMillis();
            char[] chars = str.toCharArray();
            for(int c = 0; c < 10000000; c++) {
                for (int i = 0, n = chars.length; i < n; i++) {
                    char chr = chars[i];
                    doSomethingWithChar(chr);//To trick JIT optimistaion
                }
            }
            System.out.println("2: " + (System.currentTimeMillis() - start));
            System.out.println();
        }


    }


    public static void doSomethingWithChar(char chr) {
        int newInt = chr << 2;
    }

Pour les longues chaînes, j'ai choisi le premier. Pourquoi copier autour de longues chaînes? Documentations dit:

public char [] toCharArray () Convertit cette chaîne en un nouveau tableau de caractères.

Renvoie: un tableau de caractères nouvellement alloué dont la longueur est la longueur de cette chaîne et dont le contenu est initialisé pour contenir la séquence de caractères représentée par cette chaîne.

// Modifier 1

J'ai changé le test pour tromper l'optimisation JIT.

// Modifier 2

Répétez le test 10 fois pour laisser la JVM se réchauffer.

// Modifier 3

Conclusions:

Tout d'abord str.toCharArray();copie la chaîne entière en mémoire. Cela peut consommer de la mémoire pour les longues chaînes. La méthode String.charAt( )recherche avant le char dans le tableau char à l'intérieur de la classe String vérifiant l'index. Il semble que pour les chaînes assez courtes, la première méthode (c'est-à-dire la chatAtméthode) est un peu plus lente à cause de cette vérification d'index. Mais si la chaîne est suffisamment longue, la copie du tableau entier de caractères devient plus lente et la première méthode est plus rapide. Plus la corde est longue, plus les performances sont lentes toCharArray. Essayez de changer la limite en for(int j = 0; j < 10000; j++)boucle pour le voir. Si nous laissons JVM préchauffer, le code s'exécute plus rapidement, mais les proportions sont les mêmes.

Après tout, ce n'est que de la micro-optimisation.


Pourriez-vous essayer l' for:inoption, juste pour le plaisir?
dasblinkenlight

2
Votre benchmark est imparfait: il ne laisse pas le JIT faire ses optimisations; le JIT pourrait supprimer complètement les boucles, car elles ne font rien.
JB Nizet

La chaîne n'est ni na Iterableni array.
Piotr Gwiazda

2
Ce n'est pas un test valide, vous avez «réchauffé» votre JVM avec le test 1, ce qui peut fausser les résultats en faveur du test 2. Toute la question de l'OP sent de toute façon la micro-optimisation.
Perception le

1
Vrai. Après l'échauffement (voir Edit 2), les deux temps sont plus petits mais toujours proches l'un de l'autre. Dans mon exemple, le deuxième test est un peu plus rapide. Mais si je rallonge la chaîne, la première est plus rapide. La chaîne la plus longue, le deuxième test le plus lent est dû à la copie du tableau de caractères. Faites-le simplement de la première manière.
Piotr Gwiazda

2

String.toCharArray()crée un nouveau tableau de caractères, signifie l'allocation de mémoire de longueur de chaîne, puis copie le tableau de caractères d'origine de la chaîne en utilisant System.arraycopy(), puis renvoie cette copie à l'appelant. String.charAt () renvoie le caractère à la position ide la copie originale, c'est pourquoi String.charAt()sera plus rapide que String.toCharArray(). Bien que, String.toCharArray()renvoie une copie et non un caractère du tableau String d'origine, où String.charAt()renvoie le caractère du tableau de caractères d'origine. Le code ci-dessous renvoie la valeur à l'index spécifié de cette chaîne.

public char charAt(int index) {
    if ((index < 0) || (index >= value.length)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return value[index];
}

code ci-dessous renvoie un tableau de caractères nouvellement alloué dont la longueur est la longueur de cette chaîne

public char[] toCharArray() {
    // Cannot use Arrays.copyOf because of class initialization order issues
    char result[] = new char[value.length];
    System.arraycopy(value, 0, result, 0, value.length);
    return result;
}

1

Le second provoque la création d'un nouveau tableau de caractères et tous les caractères de la chaîne sont copiés dans ce nouveau tableau de caractères, donc je suppose que le premier est plus rapide (et moins gourmand en mémoire).

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.