Самый быстрый способ перебрать все символы в строке


163

В Java, какой бы самый быстрый способ перебрать все символы в строке, это:

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

Или это:

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

РЕДАКТИРОВАТЬ :

Что я хотел бы знать, так это то, что стоимость повторного вызова charAtметода во время длинной итерации будет меньше или равна стоимости выполнения одного вызова toCharArrayв начале и последующего прямого доступа к массиву во время итерации.

Было бы замечательно, если бы кто-то мог обеспечить надежный тест для разных длин строк, имея в виду время прогрева JIT, время запуска JVM и т. Д., А не только разницу между двумя вызовами System.currentTimeMillis().


18
Что случилось с for (char c : chars)?
dasblinkenlight

Первый должен быть быстрее, и в любом случае это строка с массивом символов, теоретически.
Киган Лэддс

Google часто является хорошим ресурсом: mkyong.com/java/…
Йохан Шёберг

2
Вопрос не касается производительности использования итераторов, foreach. Что я хотел бы знать, так это то, что стоимость повторных звонков окажется charAtменьше или больше стоимости выполнения одного звонкаtoCharArray
Оскар Лопес

1
Кто-нибудь делал анализ с StringCharacterIterator ?
bdrx

Ответы:


353

ПЕРВОЕ ОБНОВЛЕНИЕ: прежде чем пытаться делать это когда-либо в производственной среде (не рекомендуется), сначала прочтите это: http://www.javaspecialists.eu/archive/Issue237.html Начиная с Java 9, описанное решение больше не будет работать потому что теперь Java будет хранить строки как byte [] по умолчанию.

ВТОРОЕ ОБНОВЛЕНИЕ: По состоянию на 2016-10-25, на моем 8xore AMDx64 и источнике 1.8 нет никакой разницы между использованием 'charAt' и доступом к полю. Похоже, что jvm достаточно оптимизирован, чтобы встроить и оптимизировать любые вызовы 'string.charAt (n)'.

Все зависит от длины Stringпроверяемого. Если, как говорится в вопросе, это для длинных строк, самый быстрый способ проверить строку - это использовать отражение для доступа к поддержке char[]строки.

Полностью рандомизированный эталонный тест с JDK 8 (win32 и win64) на 64-ядерном ядре Phenom II 4 AMD 955 @ 3,2 ГГц (как в режиме клиента, так и в режиме сервера) с 9 различными методами (см. Ниже!) Показывает, что использование String.charAt(n)является самым быстрым для малых строк и что использование reflectionдля доступа к массиву поддержки строк почти вдвое быстрее для больших строк.

ЭКСПЕРИМЕНТ

  • Испробованы 9 различных методов оптимизации.

  • Все содержимое строки рандомизировано

  • Тест проводится для размеров строк, кратных двум, начиная с 0,1,2,4,8,16 и т. Д.

  • Тесты проводятся 1000 раз на размер строки

  • Тесты перемешиваются в случайном порядке каждый раз. Другими словами, тесты проводятся в случайном порядке каждый раз, более 1000 раз.

  • Весь набор тестов выполняется вперед и назад, чтобы показать влияние прогрева JVM на оптимизацию и время.

  • Весь набор выполняется дважды, один в -clientрежиме, а другой в -serverрежиме.

ВЫВОДЫ

-клиентский режим (32 бита)

Для строк длиной от 1 до 256 символовstring.charAt(i) выигрывает вызов со средней обработкой от 13,4 до 588 миллионов символов в секунду.

Кроме того, это в целом на 5,5% быстрее (клиент) и 13,9% (сервер), например:

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

чем это с локальной конечной переменной длины:

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

Для длинных строк длиной от 512 до 256K символов наиболее быстрым является использование отражения для доступа к массиву поддержки String. Этот метод почти в два раза быстрее String.charAt (i) (на 178% быстрее). Средняя скорость в этом диапазоне составила 1,111 миллиарда символов в секунду.

Поле должно быть получено заранее, а затем оно может быть повторно использовано в библиотеке в разных строках. Интересно, что в отличие от кода выше, с доступом к полю на 9% быстрее иметь локальную конечную переменную длины, чем использовать 'chars.length' в проверке цикла. Вот как доступ к полю можно настроить максимально быстро:

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

Специальные комментарии по режиму -server

Доступ к полю начинает выигрывать после 32-х символьных строк в режиме сервера на 64-битной машине Java на моей машине AMD 64. Это не было видно до 512 символов в режиме клиента.

Также стоит отметить, что, когда я запускал JDK 8 (32-битная сборка) в режиме сервера, общая производительность была на 7% ниже как для больших, так и для маленьких строк. Это было в сборке 121 декабря 2013 года JDK 8 раннего выпуска. Итак, на данный момент кажется, что 32-битный режим сервера медленнее, чем 32-битный режим клиента.

При этом ... похоже, что единственный режим сервера, который стоит использовать, находится на 64-битной машине. В противном случае это фактически снижает производительность.

Для 32-битной сборки, работающей -server modeна AMD64, я могу сказать следующее:

  1. String.charAt (i) - явный победитель в целом. Хотя между размерами от 8 до 512 символов были победители среди «новых», «повторного использования» и «поля».
  2. String.charAt (i) на 45% быстрее в режиме клиента
  3. Доступ к полю в два раза быстрее для больших строк в режиме клиента.

Также стоит сказать, что String.chars () (Stream и параллельная версия) - это перебор. Намного медленнее, чем любой другой способ. StreamsAPI является довольно медленным способом для выполнения операций над строками вообще.

Список желаний

Java String может иметь предикат, принимающий оптимизированные методы, такие как содержит (предикат), forEach (потребитель), forEachWithIndex (потребитель). Таким образом, если пользователю не нужно знать длину или повторять вызовы методов String, это может помочь в beep-beep beepускорении анализа библиотек .

Мечтай дальше :)

Счастливые Струны!

~ SH

В тесте использовались следующие 9 методов проверки строки на наличие пробелов:

"charAt1" - ПРОВЕРИТЬ СОДЕРЖАНИЕ СОДЕРЖАНИЕ ОБЫЧНЫМ ПУТИ:

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" - ЖЕ, КАК ВЫШЕ, НО ИСПОЛЬЗОВАТЬ

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

"stream" - ИСПОЛЬЗУЙТЕ НОВУЮ СТРОКУ JAVA-8 и ПРОЙДИТЕ ЭТО ПРЕДИКАТ ДЛЯ ПРОВЕРКИ

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

"streamPara" - ЖЕ, КАК ВЫШЕ, НО ОХ-ЛА-ЛА - ПУТЕШЕСТВУЙ!

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

"reuse" - ЗАПОЛНИТЬ ОБРАЗУЮЩИЙСЯ ЧАР

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" - ПОЛУЧИТЕ НОВУЮ КОПИЮ ЧЕРНОГО [] ИЗ СТРУНЫ

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" - ТАК ЖЕ ВЫШЕ, НО ИСПОЛЬЗУЙТЕ "ДЛЯ КАЖДОГО"

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

"field1" - FANCY !! ПОЛУЧИТЕ ПОЛЕ для доступа к внутреннему символу 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" - ТАК ЖЕ ВЫШЕ, НО ИСПОЛЬЗУЙТЕ "ДЛЯ КАЖДОГО"

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

КОМПОЗИЦИОННЫЕ РЕЗУЛЬТАТЫ ДЛЯ -clientРЕЖИМА КЛИЕНТА (комбинированные тесты вперед и назад)

Обратите внимание: режим -client с 32-битным Java и режим -server с 64-битным Java такой же, как показано ниже на моей машине с 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

КОМПОЗИЦИОННЫЕ РЕЗУЛЬТАТЫ ДЛЯ -serverРЕЖИМА СЕРВЕРА (комбинированные тесты вперед и назад)

Примечание: это тест для 32-битной Java, работающей в режиме сервера на AMD64. Режим сервера для 64-битной Java был таким же, как 32-битная Java в режиме клиента, за исключением того, что доступ к полю начинался с выигрыша после 32-символьного размера.

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

ПОЛНЫЙ РАБОТАЮЩИЙ КОД ПРОГРАММЫ

(для тестирования на Java 7 и более ранних версиях удалите тесты двух потоков)

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
Был ли этот тест запущен на сервере JVM или клиентской JVM? Наилучшие оптимизации выполняются только в серверной JVM. Если вы работали с использованием 32-битной JVM по умолчанию и без аргументов, то вы работали в режиме клиента.
ceklock

2
Получение резервного буфера проблематично в случае подстрок или строк, созданных с использованием String (char [], int, int), так как вы получаете весь буфер (по крайней мере в Android), но ваша индексация будет основана на нуле. Однако, если вы знаете, что у вас нет подстроки, она будет работать нормально.
prewett

5
Любая идея, почему "for (int i = 0; i <data.length (); i ++)" быстрее, чем определение data.length () в качестве конечной локальной переменной?
Скайн

2
Для определения переменной вообще требуется операция стека в байт-коде метода. Но оптимизация, от распознавания вашего алгоритма, может быстро отследить эту повторяющуюся операцию в реальном машинном коде без дополнительных затрат на распределение переменных. Такие оптимизации иногда существуют в компиляторах байт-кода, иногда нет. Все зависит от того, достаточно ли умен JVM :-)
Координатор

2
@DavidS числа - это скорость (в наносекундах) на проверяемый символ. Меньше, тем лучше.
Координатор

14

Это просто микрооптимизация, о которой вам не стоит беспокоиться.

char[] chars = str.toCharArray();

возвращает вам копию strсимвольных массивов (в JDK он возвращает копию символов путем вызова System.arrayCopy).

Кроме этого, str.charAt()только проверяет, действительно ли индекс находится в границах, и возвращает символ в индексе массива.

Первый не создает дополнительной памяти в JVM.


Не отвечает на вопрос. Этот вопрос о производительности. Насколько вы знаете, OP, возможно, обнаружил, что итерации по строкам являются основной ценой в их применении.
rghome

9

Просто для любопытства и для сравнения с ответом Сент-Хилла.

Если вам нужно обрабатывать тяжелые данные, вы не должны использовать JVM в режиме клиента. Клиентский режим не предназначен для оптимизации.

Давайте сравним результаты тестов @Saint Hill с использованием JVM в режиме клиента и режиме сервера.

Core2Quad Q6600 G0 @ 2.4GHz
JavaSE 1.7.0_40

Смотрите также: Реальные различия между "java -server" и "java -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) 

РЕЖИМ СЕРВЕРА:

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) 

ВЫВОД:

Как видите, режим сервера намного быстрее.


2
Спасибо за публикацию. Таким образом, для больших строк доступ к полям все еще в 2 раза быстрее, чем charAt (). На самом деле, доступ к полям стал еще быстрее, после того как он стал лидером после 28 длинных строк (безумно !!) Итак ... режим сервера делает все быстрее. Очень интересно!
Координатор

1
Да, рефлексивный метод действительно быстрее. Интересный.
ceklock

2
Кстати: более новые JVM автоматически выясняют, какой из -серверов или -клиентов работает лучше (обычно): docs.oracle.com/javase/7/docs/technotes/guides/vm/…
jontejj

2
@jontejj на практике это не так просто. Если вы используете 32-битную JVM в Windows, то JVM всегда будет по умолчанию для клиента.
ceklock

7

Первое использование str.charAtдолжно быть быстрее.

Если вы покопаетесь в исходном коде Stringкласса, мы увидим, что charAtэто реализовано следующим образом:

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

Здесь все, что он делает, это индексирует массив и возвращает значение.

Теперь, если мы увидим реализацию toCharArray, мы найдем следующее:

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

Как вы видите, он делает, System.arraycopyчто, безусловно, будет медленнее, чем не делает.


2
Глупо, что String # charAt должен выполнить дополнительную проверку индекса, когда индекс все равно проверяется при доступе к массиву.
Инго

1
На риск возрождения 8-летнего потока ... Массив char за строкой может быть больше, чем сама строка. То есть, если у вас была строка «abcde», а затем вы использовали подстроку для извлечения «bcd» в новую строку, новая строка будет поддерживаться точно таким же массивом символов, что и первая строка. Вот почему строковый класс поддерживает смещение и счетчик - поэтому он знает, какие символы в массиве являются теми, которые представляют эту строку. Поэтому проверка диапазона важна, иначе можно было бы получить доступ к символам за пределами этой строки.
DTY

3

Несмотря на ответ @Saint Hill, если учесть временную сложность str.toCharArray () ,

первый быстрее даже для очень больших струн. Вы можете запустить код ниже, чтобы увидеть его для себя.

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

вывод:

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

2

Похоже, быстрее или медленнее

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

Для длинных струн я выберу первый. Зачем копировать длинные строки? Документация гласит:

public char [] toCharArray () Преобразует эту строку в новый массив символов.

Возвращает: вновь распределенный массив символов, длина которого равна длине этой строки и содержимое которого инициализируется, чтобы содержать последовательность символов, представленную этой строкой.

// Редактировать 1

Я изменил тест, чтобы обмануть JIT-оптимизацию.

// Редактировать 2

Повторите тест 10 раз, чтобы дать JVM прогреться.

// Редактировать 3

Выводы:

Прежде всего str.toCharArray();копирует всю строку в память. Это может занять много памяти для длинных строк. Метод String.charAt( )ищет char в массиве char внутри String, проверяя индекс раньше. Похоже, что для достаточно коротких строк первый метод (т. Е. chatAtМетод) немного медленнее из-за этой проверки индекса. Но если String достаточно длинный, копирование всего массива char происходит медленнее, а первый метод - быстрее. Чем длиннее строка, тем медленнее toCharArrayработает. Попробуйте изменить предел в for(int j = 0; j < 10000; j++)цикле, чтобы увидеть его. Если мы дадим JVM прогреться, код будет работать быстрее, но пропорции будут такими же.

Ведь это просто микрооптимизация.


Не могли бы вы попробовать for:inвариант, просто для удовольствия?
dasblinkenlight

2
Ваш бенчмарк некорректен: он не позволяет JIT оптимизировать; JIT может полностью удалить петли, поскольку они ничего не делают.
JB Низет

Строка не является Iterableни массивом.
Петр Гвязда

2
Это недействительный тест, вы «подогрели» свою JVM с помощью теста 1, который может исказить результаты в пользу теста 2. В любом случае, весь вопрос ОП пахнет микрооптимизацией.
Восприятие

1
Правда. После прогрева (см. Редактирование 2) оба раза меньше, но все еще близко друг к другу. В моем примере второй тест немного быстрее. Но если я сделаю String длиннее, первый будет быстрее. Чем длиннее строка, тем медленнее второй тест из-за копирования массива символов. Просто сделай это первым способом.
Петр Гвязда

2

String.toCharArray()создает новый массив символов, означает выделение памяти длины строки, затем копирует исходный массив символов строки с помощью System.arraycopy()и затем возвращает эту копию вызывающей стороне . String.charAt () возвращает символ в позиции iиз оригинальной копии, поэтому String.charAt()будет быстрее, чем String.toCharArray(). Хотя, String.toCharArray()возвращает копию, а не char из исходного массива String, где String.charAt()возвращает символ из исходного массива char. Код ниже возвращает значение по указанному индексу этой строки.

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

приведенный ниже код возвращает вновь распределенный массив символов, длина которого равна длине этой строки

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

Второй вызывает создание нового массива char, и все символы из String копируются в этот новый массив char, так что я бы предположил, что первый будет быстрее (и менее требователен к памяти).

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.