Il modo più veloce per scorrere su tutti i caratteri in una stringa


163

In Java, quale sarebbe il modo più veloce per scorrere su tutti i caratteri in una stringa, questo:

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

O questo:

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

MODIFICARE :

Quello che vorrei sapere è se il costo di chiamare ripetutamente il charAtmetodo durante una lunga iterazione finisce per essere inferiore o superiore al costo di eseguire una singola chiamata atoCharArray all'inizio e quindi accedere direttamente all'array durante l'iterazione.

Sarebbe bello se qualcuno potesse fornire un benchmark solido per lunghezze di stringa diverse, tenendo presente il tempo di riscaldamento JIT, il tempo di avvio JVM, ecc. E non solo la differenza tra due chiamate a System.currentTimeMillis().


18
Che cosa è successo a for (char c : chars)?
dasblinkenlight,

Il primo dovrebbe essere più veloce, e comunque una stringa un array di caratteri, teoricamente.
Keagan Ladds,

Google è spesso una buona risorsa: mkyong.com/java/…
Johan Sjöberg

2
La domanda non richiede le prestazioni dell'utilizzo di iteratori, foreach. Quello che mi piacerebbe sapere è se il costo delle chiamate ripetute charAtfinisce per essere inferiore o superiore al costo di eseguire una singola chiamata atoCharArray
Óscar López,

1
Qualcuno ha fatto analisi con StringCharacterIterator ?
bdrx,

Risposte:


352

PRIMO AGGIORNAMENTO: prima di provare questo in un ambiente di produzione (non consigliato), leggi prima questo: http://www.javaspecialists.eu/archive/Issue237.html A partire da Java 9, la soluzione descritta non funzionerà più , perché ora Java memorizzerà le stringhe come byte [] per impostazione predefinita.

SECONDO AGGIORNAMENTO: a partire dal 25-10-2016, sul mio AMDx64 8core e la fonte 1.8, non vi è alcuna differenza tra l'utilizzo di 'charAt' e l'accesso al campo. Sembra che jvm sia sufficientemente ottimizzato per incorporare e semplificare qualsiasi chiamata "string.charAt (n)".

Tutto dipende dalla lunghezza Stringdell'ispezione. Se, come dice la domanda, è per stringhe lunghe , il modo più rapido per ispezionare la stringa è utilizzare la riflessione per accedere al supporto char[]della stringa.

Un benchmark completamente randomizzato con JDK 8 (win32 e win64) su un 64 AMD Phenom II 4 core 955 @ 3.2 GHZ (sia in modalità client che in modalità server) con 9 tecniche diverse (vedi sotto!) Mostra che l'utilizzo String.charAt(n)è il più veloce per i piccoli stringhe e che usandoreflection per accedere all'array di backing String è quasi due volte più veloce per stringhe di grandi dimensioni.

L'ESPERIMENTO

  • Vengono provate 9 diverse tecniche di ottimizzazione.

  • Tutti i contenuti delle stringhe sono randomizzati

  • Il test viene eseguito per dimensioni di stringa in multipli di due a partire da 0,1,2,4,8,16 ecc.

  • I test vengono eseguiti 1.000 volte per dimensione della stringa

  • I test vengono mescolati in ordine casuale ogni volta. In altre parole, i test vengono eseguiti in ordine casuale ogni volta che vengono eseguiti, oltre 1000 volte.

  • L'intera suite di test viene eseguita in avanti e all'indietro, per mostrare l'effetto del riscaldamento JVM sull'ottimizzazione e sui tempi.

  • L'intera suite viene eseguita due volte, una volta in -clientmodalità e l'altra in -servermodalità.

CONCLUSIONI

-client mode (32 bit)

Per le stringhe da 1 a 256 caratteri , la chiamata string.charAt(i)vince con un'elaborazione media di 13,4 milioni a 588 milioni di caratteri al secondo.

Inoltre, è complessivamente più veloce del 5,5% (client) e del 13,9% (server) in questo modo:

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

che in questo modo con una variabile di lunghezza finale locale:

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

Per stringhe lunghe, con una lunghezza compresa tra 512 e 256 KB , utilizzare la riflessione per accedere all'array di supporto della stringa è il più veloce. Questa tecnica è quasi due volte più veloce di String.charAt (i) (178% più veloce). La velocità media su questo intervallo era di 1,111 miliardi di caratteri al secondo.

Il campo deve essere ottenuto in anticipo e quindi può essere riutilizzato nella libreria su stringhe diverse. È interessante notare che, a differenza del codice sopra, con l'accesso al campo, è più veloce del 9% avere una variabile di lunghezza finale locale piuttosto che usare "chars.length" nel controllo del ciclo. Ecco come l'accesso al campo può essere impostato come più veloce:

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

Commenti speciali sulla modalità server

L'accesso al campo inizia vincendo dopo stringhe di 32 caratteri in modalità server su una macchina Java a 64 bit sulla mia macchina AMD 64. Ciò non è stato visto fino a 512 caratteri di lunghezza in modalità client.

Vale anche la pena notare che, quando eseguivo JDK 8 (build a 32 bit) in modalità server, le prestazioni complessive erano più lente del 7% sia per le stringhe grandi che per quelle piccole. Questo era con build 121 dicembre 2013 di JDK 8 rilascio anticipato. Quindi, per ora, sembra che la modalità server a 32 bit sia più lenta della modalità client a 32 bit.

Detto questo ... sembra che l'unica modalità server che vale la pena invocare sia su una macchina a 64 bit. Altrimenti ostacola effettivamente le prestazioni.

Per build a 32 bit in esecuzione -server modesu un AMD64, posso dire questo:

  1. String.charAt (i) è il chiaro vincitore assoluto. Sebbene tra le dimensioni da 8 a 512 caratteri ci fossero vincitori tra "nuovo" "riutilizzo" e "campo".
  2. String.charAt (i) è più veloce del 45% in modalità client
  3. L'accesso al campo è due volte più veloce per stringhe di grandi dimensioni in modalità client.

Vale anche la pena dire che String.chars () (Stream e la versione parallela) sono un busto. Molto più lento di qualsiasi altro modo. L' StreamsAPI è un modo piuttosto lento per eseguire operazioni di stringa generali.

Lista dei desideri

Java String potrebbe avere predicato che accetta metodi ottimizzati come contente (predicato), forEach (consumer), forEachWithIndex (consumer). Pertanto, senza la necessità per l'utente di conoscere la lunghezza o ripetere le chiamate ai metodi String, questi potrebbero aiutare ad analizzare le libreriebeep-beep beep accelerazione delle .

Continua a sognare :)

Corde felici!

~ SH

Il test ha utilizzato i seguenti 9 metodi per testare la stringa per la presenza di spazi bianchi:

"charAt1" - CONTROLLA IL CONTENUTO DELLA STRING IN MODO USUALE:

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" - STESSO COME SOPRA MA UTILIZZARE String.length () ALL'INTERNO DI EFFETTUARE UN LOCALE FINALE int PER LA LUNGHEZZA

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

"stream" - UTILIZZA IL NUOVO IntStream della stringa JAVA-8 E PASSA UN PREDICATO PER EFFETTUARE IL CONTROLLO

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

"streamPara" - STESSI COME SOPRA, MA OH-LA-LA - VAI PARALLELO !!!

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

"riutilizzo" - RICARICA un carattere riutilizzabile [] CON IL CONTENUTO DI STRING

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" - Ottieni una nuova copia del carattere [] DALLA STRINGA

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" - STESSO COME SOPRA, MA UTILIZZARE "FOR-EACH"

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

"field1" - FANTASIA !! OTTIENI CAMPO PER L'ACCESSO AL CARATTERE INTERNO DELLA 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" - STESSO COME SOPRA, MA UTILIZZARE "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;
}

RISULTATI COMPOSITI PER LA -clientMODALITÀ CLIENTE (test avanti e indietro combinati)

Nota: che la modalità client con Java a 32 bit e la modalità server con Java a 64 bit sono le stesse di seguito sulla mia macchina 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

RISULTATI COMPOSITI PER LA -serverMODALITÀ SERVER (test avanti e indietro combinati)

Nota: questo è il test per Java a 32 bit in esecuzione in modalità server su un AMD64. La modalità server per Java 64 bit era la stessa di Java 32 bit in modalità client, tranne per il fatto che l'accesso al campo iniziava vincendo dopo una dimensione di 32 caratteri.

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

CODICE PROGRAMMA COMPLETO RUNNABLE

(per testare su Java 7 e versioni precedenti, rimuovere i due test dei flussi)

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
Questo test è stato eseguito nel server JVM o nel client JVM? Le migliori ottimizzazioni vengono eseguite solo nel server JVM. Se hai eseguito utilizzando la JVM predefinita a 32 bit e nessun argomento, hai eseguito in modalità client.
Ceklock,

2
Ottenere il buffer di backup è problematico nel caso di sottostringhe o stringhe create usando String (char [], int, int), poiché si ottiene l'intero buffer (almeno su Android), ma l'indicizzazione sarà basata su zero. Tuttavia, se sai che non hai una sottostringa, funzionerà bene.
prewett

5
Qualche idea sul perché "for (int i = 0; i <data.length (); i ++)" è più veloce della definizione di data.length () come variabile locale finale?
skyin

2
La definizione di una variabile richiede affatto un'operazione di stack nel codice byte del metodo. Ma le ottimizzazioni, dal riconoscere il tuo algoritmo, potrebbero accelerare la ripetizione dell'operazione nel codice macchina effettivo, senza il sovraccarico di alocalizzazione variabile. Tali ottimizzazioni a volte esistono nei compilatori bytecode, a volte no. Tutto dipende dal fatto che la jvm sia abbastanza intelligente :-)
Il coordinatore

2
@DavidS i numeri sono il tasso (in nanosecondi) per carattere ispezionato. Più piccolo è meglio.
Il coordinatore,

14

Questa è solo una micro-ottimizzazione di cui non dovresti preoccuparti.

char[] chars = str.toCharArray();

ti restituisce una copia di strarray di caratteri (in JDK, restituisce una copia di caratteri chiamando System.arrayCopy).

Oltre a ciò, str.charAt()controlla solo se l'indice è effettivamente nei limiti e restituisce un carattere all'interno dell'indice dell'array.

Il primo non crea ulteriore memoria in JVM.


Non risponde alla domanda Questa domanda riguarda le prestazioni. Per quanto ne sai, l'OP potrebbe aver scoperto che l'iterazione su stringhe è un costo importante nella loro applicazione.
rghome,

9

Solo per curiosità e per confrontarsi con la risposta di Saint Hill.

Se è necessario elaborare dati pesanti, non utilizzare JVM in modalità client. La modalità client non viene creata per le ottimizzazioni.

Confrontiamo i risultati dei benchmark @Saint Hill usando una JVM in modalità Client e Server.

Core2Quad Q6600 G0 @ 2.4GHz
JavaSE 1.7.0_40

Vedi anche: Differenze reali tra "java -server" e "java -client"?


MODALITÀ CLIENTE:

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) 

MODALITÀ SERVER:

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) 

CONCLUSIONE:

Come puoi vedere, la modalità server è molto più veloce.


2
Grazie per la pubblicazione. Pertanto, per stringhe di grandi dimensioni, l'accesso al campo è ancora 2 volte più veloce di charAt (). In effetti, l'accesso al campo è diventato nel complesso ancora più veloce, portando avanti dopo 28 stringhe di lunghezza (folle !!) Quindi ... la modalità server rende tutto più veloce. Molto interessante!
Il coordinatore

1
Sì, il metodo riflettente è molto più veloce. Interessante.
Ceklock

2
btw: le nuove JVM individuano automaticamente quale client o server funziona meglio (di solito): docs.oracle.com/javase/7/docs/technotes/guides/vm/…
jontejj il

2
@jontejj in pratica non è così semplice. Se si esegue una JVM a 32 bit su Windows, la JVM sarà sempre predefinita sul client.
Ceklock,

7

Il primo utilizzo str.charAtdovrebbe essere più veloce.

Se scavi all'interno del codice sorgente della Stringclasse, possiamo vedere che charAtè implementato come segue:

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

Qui, tutto ciò che fa è indicizzare un array e restituire il valore.

Ora, se vediamo l'implementazione di toCharArray, troveremo di seguito:

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

Come vedi, sta facendo qualcosa System.arraycopyche sarà sicuramente un po 'più lento del non farlo.


2
È sciocco che String # charAt esegua un ulteriore controllo dell'indice, quando l'indice viene comunque verificato all'accesso all'array.
Ingo,

1
A rischio di rianimare un thread di 8 anni ... La matrice di caratteri dietro una stringa potrebbe essere più grande della stringa stessa. Cioè, se avevi una stringa "abcde" e poi hai usato la sottostringa per estrarre "bcd" in una nuova stringa, la nuova stringa sarà supportata dallo stesso array di caratteri della prima stringa. Ecco perché la classe string mantiene un offset e un conteggio, quindi sa quali caratteri nell'array sono quelli che rappresentano questa stringa. Quindi il controllo dell'intervallo è importante, altrimenti sarebbe possibile accedere ai caratteri oltre le estremità di questa stringa.
dty

3

Nonostante la risposta di @Saint Hill se si considera la complessità temporale di str.toCharArray () ,

il primo è più veloce anche per stringhe molto grandi. Puoi eseguire il codice qui sotto per vederlo da solo.

        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)");

produzione:

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

2

Sembra che il nether sia più veloce o più lento

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

Per lunghe corde sceglierò il primo. Perché copiare attorno a stringhe lunghe? La documentazione dice:

public char [] toCharArray () Converte questa stringa in un nuovo array di caratteri.

Restituisce: una matrice di caratteri appena allocata la cui lunghezza è la lunghezza di questa stringa e il cui contenuto è inizializzato per contenere la sequenza di caratteri rappresentata da questa stringa.

// Modifica 1

Ho modificato il test per ingannare l'ottimizzazione JIT.

// Modifica 2

Ripeti il ​​test 10 volte per far riscaldare JVM.

// Modifica 3

conclusioni:

Prima di tutto str.toCharArray();copia l'intera stringa in memoria. Può richiedere molta memoria per stringhe lunghe. Il metodo String.charAt( )cerca char in array di caratteri all'interno della classe String controllando prima l'indice. Sembra che il primo metodo Strings abbastanza breve (cioè il chatAtmetodo) sia un po 'più lento a causa di questo controllo dell'indice. Ma se la stringa è abbastanza lunga, la copia dell'intero array di caratteri diventa più lenta e il primo metodo è più veloce. Più lunga è la stringa, più lenta è l' toCharArrayesecuzione. Prova a cambiare il limite in for(int j = 0; j < 10000; j++)loop per vederlo. Se permettiamo a JVM di riscaldare il codice viene eseguito più velocemente, ma le proporzioni sono le stesse.

Dopotutto è solo una micro-ottimizzazione.


Potresti provare l' for:inopzione, solo per divertimento?
dasblinkenlight,

2
Il tuo benchmark è difettoso: non consente a JIT di effettuare le sue ottimizzazioni; JIT potrebbe rimuovere completamente i loop, dal momento che non fanno nulla.
JB Nizet,

String non è na Iterablené array.
Piotr Gwiazda,

2
Questo non è un test valido, hai "riscaldato" la tua JVM con Test 1, che può distorcere i risultati verso il favore di Test 2. L'intera domanda del PO puzza comunque di micro ottimizzazione.
Percezione

1
Vero. Dopo il riscaldamento (vedi Modifica 2) entrambe le volte sono più piccole ma ancora vicine l'una all'altra. Nel mio esempio il secondo test è un po 'più veloce. Ma se allungo la stringa, la prima è più veloce. La stringa più lunga, il secondo test più lento è dovuto alla copia dell'array char. Fallo solo nel primo modo.
Piotr Gwiazda,

2

String.toCharArray()crea un nuovo array di caratteri, significa allocazione della memoria della lunghezza della stringa, quindi copia l'array di caratteri originale della stringa utilizzando System.arraycopy()e quindi restituisce questa copia al chiamante. String.charAt () restituisce il carattere nella posizione idalla copia originale, ecco perché String.charAt()sarà più veloce di String.toCharArray(). Tuttavia, String.toCharArray()restituisce copia e non carattere dall'array String originale, dove String.charAt()restituisce il carattere dall'array char originale. Il codice seguente restituisce il valore nell'indice specificato di questa stringa.

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

il codice seguente restituisce un array di caratteri appena allocato la cui lunghezza è la lunghezza di questa stringa

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

Il secondo fa sì che venga creato un nuovo array di caratteri e che tutti i caratteri della stringa vengano copiati in questo nuovo array di caratteri, quindi immagino che il primo sia più veloce (e meno affamato di memoria).

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.