Найшвидший спосіб перебрати всі символи в рядку


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

Перший повинен бути швидшим, і в будь-якому разі, теоретично, це рядок "char char".
Keagan Ladds

Google часто є хорошим ресурсом: mkyong.com/java/…
Йохан

2
Питання не запитує про ефективність використання ітераторів, передбачити. Що я хотів би знати, якщо вартість багаторазового дзвінка виявиться charAtменшою або більшою, ніж вартість виконання одного дзвінкаtoCharArray
Óscar López

1
Хтось робив аналіз із StringCharacterIterator ?
bdrx

Відповіді:


352

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

ДРУГЕ ОНОВЛЕННЯ: Станом на 2016-10-25, на моєму AMDx64 8core та джерелі 1.8, різниці між використанням 'charAt' та доступу до поля немає. Здається, jvm достатньо оптимізований для вбудованого та впорядкування будь-яких викликів 'string.charAt (n)'.

Все залежить від тривалості Stringобстежуваного. Якщо, як йдеться в запитанні, це стосується довгих рядків, найшвидший спосіб перевірити рядок - це використовувати відображення для доступу до резервної копії char[]рядка.

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

ДОСВІД

  • Перевірено 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 до 256 К символів використовується швидке використання відображення для доступу до резервного масиву 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);
   }

Спеціальні коментарі до режиму -сервер

Доступ до поля починається з виграшу після 32 рядків довжини символів у режимі сервера на 64-бітній машині Java на моїй машині AMD 64. Це було помічено до 512 символів довжиною в режимі клієнта.

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

Але, мабуть, єдиний серверний режим, на який варто звернутися, - це 64-розрядна машина. Інакше це насправді гальмує продуктивність.

Для 32-бітної збірки, що працює -server modeна AMD64, я можу сказати так:

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

Також варто сказати, що String.chars () (Потік і паралельна версія) - це бюст. Шлях повільніше, ніж будь-який інший спосіб. TheStreamsAPI є досить повільним способом для виконання операцій над рядками взагалі.

Список бажань

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" - ТОБО ЯКЩО ПРО ВИКОРИСТАННЯ String.length () ВМЕСТИТЬ ЗРОБИТИ ЗАКОННИЙ МІСЦЕВИЙ ІНТЕРНЕТ ДЛЯ ДОВГИ

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

"повторне використання" - ПОВЕРНІТЬ ПОСТАВНИЙ знак [] ЗМІСТОМ СТОРІН

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" - Отримайте нову копію char [] ІЗ СТРУГУ

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 !! Отримати поле для доступу до внутрішнього символу СТРІНГ []

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 з Java 32-бітним режимом і -серверний режим з Java-64-бітними є такими ж, як нижче на моїй машині 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. Режим сервера для Java 64 біт був таким самим, як Java 32 біт у режимі клієнта, за винятком того, що доступ до поля починається вигравати після розміру 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 () як кінцевої локальної змінної?
skyin

2
Визначення змінної взагалі вимагає операції стека в байтовому коді методу. Але оптимізація, розпізнаючи ваш алгоритм, може відстежувати цю повторювану операцію у фактичному машинному коді, без накладних витрат змінної алокації. Такі оптимізації іноді існують у компіляторах байт-кодів, іноді - ні. Все залежить від того, чи jvm досить розумний :-)
Координатор

2
@DavidS номери - це швидкість (в наносекундах) на перевірений символ. Менше - краще.
Координатор

14

Це лише мікрооптимізація, про яку ви не повинні турбуватися.

char[] chars = str.toCharArray();

повертає вам копію strмасивів символів (у JDK він повертає копію символів за допомогою викликуSystem.arrayCopy ).

Крім цього, str.charAt()лише перевіряє, чи справді індекс знаходиться в межах, і повертає символ в індексі масиву.

Перший не створює додаткової пам'яті в JVM.


Не відповідає на запитання. Це питання стосується продуктивності. Наскільки ви знаєте, ОП, можливо, виявила, що перегляд рядків є головною вартістю в їх застосуванні.
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
btw: нові JVM автоматично визначають, який із -server або -client найкраще працює (як правило): 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" в нову рядок, нова рядок буде підкріплена таким же масивом char, що і перша рядок. Ось чому клас рядків підтримує зсув і кількість - тому він знає, які символи в масиві є тими, які представляють цей рядок. Тому важлива перевірка діапазону, інакше можна було б отримати доступ до символів поза межами кінців цього рядка.
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 Nizet

Рядок не є ні Iterableні масив.
Piotr Gwiazda

2
Це неправдивий тест, ви «розігріли» свій JVM тестом 1, який може перекосити результати на користь тесту 2. Все питання ОП в будь-якому випадку пахне мікрооптимізацією.
Сприйняття

1
Правда. Після прогрівання (див. Редагування 2) обидва рази бувають меншими, але все ще близько один до одного. У моєму прикладі другий тест трохи швидший. Але якщо я зроблю струну довше, перша швидша. Чим довший рядок, тим повільніше другий тест, завдяки копії масиву char. Просто зробіть це першим способом.
Piotr Gwiazda

2

String.toCharArray()створює новий масив char, означає розподіл пам'яті довжини рядка, потім копіює оригінальний масив char із рядка за допомогою, 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.