문자열의 모든 문자를 반복하는 가장 빠른 방법


163

Java에서 String의 모든 문자를 반복하는 가장 빠른 방법은 다음과 같습니다.

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 배열입니다.
Keagan Ladds 2012

Google은 종종 좋은 자료입니다 : mkyong.com/java/…
Johan Sjöberg

2
이 질문은 반복자를 사용하는 성능을 요구하지 않습니다. 내가 알고 싶은 것은 반복적 인 통화 charAt비용이 단일 통화를 수행하는 비용보다 적거나 큼toCharArray
Óscar López

1
누구든지 StringCharacterIterator로 분석을 했습니까?
bdrx

답변:


352

첫 번째 업데이트 : 프로덕션 환경에서이 작업을 시도하기 전에 (권장하지 않음) 먼저 다음을 읽으십시오. http://www.javaspecialists.eu/archive/Issue237.html Java 9부터는 설명 된 솔루션이 더 이상 작동하지 않습니다. Java는 기본적으로 문자열을 byte []로 저장하기 때문입니다.

두 번째 업데이트 : 2016-10-25 현재, AMDx64 8core 및 소스 1.8에서 'charAt'과 필드 액세스를 사용하는 것에는 차이가 없습니다. jvm은 'string.charAt (n)'호출을 인라인하고 간소화하기에 충분히 최적화 된 것으로 보입니다.

그것은 모두 String검사되는 길이에 달려 있습니다. 질문에서 알 수 있듯이 문자열에 대한 것이면 문자열을 검사하는 가장 빠른 방법은 리플렉션을 사용 char[]하여 문자열 의 뒷면 에 액세스하는 것 입니다.

9 개 가지 기술을 (두 클라이언트 모드와 서버 모드)를 64 AMD 페넘 II 4 코어에 JDK 8 (Win32 및 Win64를)와 완전히 무작위 벤치 마크 955 @ 3.2 GHZ (아래 참조!)를 사용하는 것을 알 수 String.charAt(n)있는 가장 빠른 작은을위한입니다 문자열 및 reflectionString 백업 배열에 액세스 하는 데 사용 하는 것이 큰 문자열의 경우 거의 두 배 빠릅니다.

실험

  • 9 가지 최적화 기법이 시도되었습니다.

  • 모든 문자열 내용은 무작위입니다

  • 테스트는 0,1,2,4,8,16 등으로 시작하는 2의 배수로 문자열 크기에 대해 수행됩니다.

  • 테스트는 문자열 크기 당 1,000 회 수행됩니다.

  • 테스트는 매번 무작위 순서로 섞입니다. 다시 말해, 테스트는 매번 1000 번 이상 수행 될 때마다 무작위 순서로 수행됩니다.

  • 전체 테스트 스위트는 JVM 워밍업이 최적화 및 시간에 미치는 영향을 보여주기 위해 앞뒤로 수행됩니다.

  • 전체 제품군은 한 번은 -client모드에서, 다른 하나는 -server모드 에서 두 번 수행 됩니다.

결론

클라이언트 모드 (32 비트)

문자열 의 길이는 1 256 자 호출, string.charAt(i)초당 13,400,000 만 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.charAt (i) 보다 거의 두 배 빠릅니다 (178 % 빠름). 이 범위의 평균 속도는 초당 11.111 억 자입니다.

필드는 미리 확보 한 다음 라이브러리에서 다른 문자열로 재사용 할 수 있습니다. 흥미롭게도, 위의 코드와 달리 필드 액세스를 사용하면 루프 검사에서 'chars.length'를 사용하는 것보다 로컬 최종 길이 변수를 갖는 것이 9 % 빠릅니다. 필드 액세스를 가장 빠르게 설정하는 방법은 다음과 같습니다.

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

서버 모드에 대한 특별 설명

AMD 64 시스템의 64 비트 Java 시스템에서 서버 모드에서 32 자 길이의 문자열 이후에 필드 액세스가 시작됩니다. 클라이언트 모드에서 512 자 길이까지 표시되지 않았습니다.

또한 서버 모드에서 JDK 8 (32 비트 빌드)을 실행할 때 전체 문자열이 작은 문자열과 작은 문자열 모두에서 7 % 느려 졌다고 생각합니다. 이것은 JDK 8 초기 릴리스의 2013 년 12 월 121 빌드입니다. 따라서 현재로서는 32 비트 서버 모드가 32 비트 클라이언트 모드보다 느린 것 같습니다.

말하자면 ... 호출 할 가치가있는 유일한 서버 모드는 64 비트 시스템에있는 것 같습니다. 그렇지 않으면 실제로 성능이 저하됩니다.

-server modeAMD64 에서 실행되는 32 비트 빌드의 경우 다음과 같이 말할 수 있습니다.

  1. String.charAt (i)는 전체적으로 확실한 승자입니다. 크기는 8-512 자 사이이지만 '신규' '재사용'및 '필드'에서 승자가있었습니다.
  2. 클라이언트 모드에서 String.charAt (i)가 45 % 빠릅니다.
  3. 클라이언트 모드에서 큰 문자열의 경우 필드 액세스가 두 배 빠릅니다.

또한 String.chars () (스트림 및 병렬 버전)는 흉상입니다. 다른 방법보다 속도가 느립니다. StreamsAPI는 일반 문자열 연산을 수행하는 다소 느린 방법입니다.

위시리스트

Java String은 contains (predicate), forEach (consumer), forEachWithIndex (consumer)와 같은 최적화 된 메소드를 허용하는 술어를 가질 수 있습니다. 따라서 사용자가 String 메서드의 길이 또는 반복 호출을 알 필요없이 라이브러리 구문 분석 beep-beep beep속도를 높이는 데 도움이 될 수 있습니다.

계속 꿈꾸세요 :)

행복한 줄!

~ SH

이 테스트에서는 공백이 있는지 문자열을 테스트하는 다음 9 가지 방법을 사용했습니다.

"charAt1"-STRING 콘텐츠를 점검하십시오. 일반적인 방법 :

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 만들기

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

"stream"-새로운 JAVA-8 String의 IntStream을 사용하고 점검을 위해 Predicate를 전달하십시오

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

"streamPara"-위와 동일하지만 OH-LA-LA-GO PARALLEL !!!

// 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"-문자열에서 새 문자를 가져옵니다 []

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"-위와 동일하지만 "FOR-EACH"사용

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

"field1"-팬시 !! 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"-위에서와 동일하지만 "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;
}

클라이언트 -client모드에 대한 컴포지트 결과 (앞뒤 테스트 결합)

참고 : Java 32 비트를 사용하는 -client 모드와 Java 64 비트를 사용하는 -server 모드는 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모드에 대한 컴포지트 결과 (앞뒤 테스트 결합)

참고 : 이것은 AMD64에서 서버 모드로 실행되는 Java 32 비트에 대한 테스트입니다. 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에서는). 그러나 인덱싱은 0을 기준으로합니다. 그러나 하위 문자열이 없다는 것을 알면 제대로 작동합니다.
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에서 추가 메모리를 생성하지 않습니다.


질문에 대답하지 않습니다. 이 질문은 성능에 관한 것입니다. 아시다시피 OP는 문자열을 반복하는 것이 응용 프로그램의 주요 비용이라는 것을 알았습니다.
rghome

9

호기심과 세인트 힐의 답변과 비교하십시오.

무거운 데이터를 처리해야하는 경우 클라이언트 모드에서 JVM을 사용하지 않아야합니다. 클라이언트 모드는 최적화를 위해 만들어지지 않았습니다.

클라이언트 모드 및 서버 모드에서 JVM을 사용하여 @Saint Hill 벤치 마크 결과를 비교해 보겠습니다.

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
게시 해 주셔서 감사합니다. 따라서 큰 문자열의 경우 필드 액세스는 charAt ()보다 여전히 2 배 빠릅니다. 실제로, 필드 액세스는 28 개의 길이 문자열 (미쳤습니다!) 이후로 전반적으로 훨씬 빨라졌습니다. 따라서 ... 서버 모드는 모든 것을 더 빠르게 만듭니다. 매우 흥미로운!
코디네이터

1
예, 반사 방법이 정말 빠릅니다. 흥미 롭군
ceklock

2
BTW : 새로운 JVM들은 자동으로 파악 최고 (보통) 작동 -server 또는 -client의 어느 : docs.oracle.com/javase/7/docs/technotes/guides/vm/...
jontejj

2
실제로 @jontejj는 그렇게 간단하지 않습니다. Windows에서 32 비트 JVM을 실행중인 경우 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에서 추가 인덱스 검사를 수행해야한다는 것은 바보입니다.
Ingo

1
8 살짜리 스레드를 부활시킬 위험이 있습니다 ... 문자열 뒤에있는 char 배열은 문자열 자체보다 클 수 있습니다. 즉, 문자열 "abcde"가 있고 하위 문자열을 사용하여 "bcd"를 새 문자열로 추출하면 새 문자열은 첫 번째 문자열과 정확히 동일한 char 배열로 백업됩니다. 이것이 문자열 클래스가 오프셋과 카운트를 유지하는 이유입니다. 따라서 배열의 어떤 문자가이 문자열을 나타내는 지 알고 있습니다. 따라서 범위 확인이 중요하지 않으면이 문자열의 끝을 넘어 문자에 액세스 할 수 있습니다.
dty

3

str.toCharArray () 의 시간 복잡성을 고려하면 @Saint Hill의 대답에도 불구하고 ,

첫 번째 문자열은 매우 큰 문자열에서도 더 빠릅니다. 아래 코드를 실행하여 직접 확인할 수 있습니다.

        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

JVM을 예열 시키려면 테스트를 10 번 반복하십시오.

// 3 수정

결론 :

우선 str.toCharArray();전체 문자열을 메모리에 복사합니다. 긴 문자열에는 메모리를 소비 할 수 있습니다. 메소드 String.charAt( )는 이전에 String 클래스 검사 색인에서 char 배열에서 char을 찾습니다. 충분히 짧은 문자열처럼 보입니다. 첫 번째 방법 (즉, chatAt방법) 은이 색인 확인으로 인해 조금 느립니다. 그러나 문자열이 충분히 길면 전체 문자 배열을 복사하는 속도가 느려지고 첫 번째 방법이 더 빠릅니다. 줄이 길수록 toCharArray연주 속도가 느려집니다 . for(int j = 0; j < 10000; j++)루프에서 제한을 변경하여보십시오 . JVM 워밍업 코드가 더 빨리 실행되도록하지만 비율은 동일합니다.

결국 그것은 단지 미세 최적화입니다.


for:in재미를 위해 옵션 을 사용해 볼 수 있습니까?
dasblinkenlight 2012 년

2
벤치 마크에 결함이 있습니다. JIT에서 최적화를 수행 할 수 없습니다. JIT는 아무것도하지 않기 때문에 루프를 완전히 제거 할 수 있습니다.
JB 니 제트

문자열은 Iterable배열도 아닙니다.
Piotr Gwiazda

2
이것은 유효한 테스트가 아니며 테스트 1로 JVM을 '워밍업'하여 테스트 2의 선호로 결과를 왜곡시킬 수 있습니다. OP의 모든 질문은 어쨌든 미세 최적화 냄새가납니다.
인식

1
진실. 워밍업 후 (편집 2 참조) 두 시간이 더 작아서 서로 가까이 있습니다. 내 예제에서 두 번째 테스트는 조금 더 빠릅니다. 그러나 문자열을 더 길게 만들면 첫 번째 문자열이 더 빠릅니다. 문자열이 길수록 문자 배열 복사로 인해 두 번째 테스트가 느려집니다. 첫 번째 방법으로하세요.
Piotr Gwiazda

2

String.toCharArray()새로운 문자 배열을 생성하고, 문자열 길이의 메모리 할당을 의미 한 다음 문자열을 사용하여 원래 문자 배열을 System.arraycopy()복사 한 다음이 사본을 호출자에게 반환합니다. String.charAt ()은 i원본 에서 위치에있는 문자를 반환 String.charAt()하므로 이보다 빠릅니다 String.toCharArray(). 그러나 String.toCharArray()원래 String 배열에서 char가 아닌 copy를 반환 하지만 여기서 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

두 번째 것은 새로운 문자 배열을 만들고 String의 모든 문자를이 새로운 문자 배열로 복사하므로 첫 번째 문자는 더 빠르며 메모리가 덜 부족합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.