遍历字符串中所有字符的最快方法


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


17
发生了什么事for (char c : chars)
dasblinkenlight 2012年

从理论上讲,第一个应该更快,并且无论如何都将一个字符串作为char数组。
Keagan Ladds

谷歌往往是一个很好的资源:mkyong.com/java/...
约翰·斯乔贝格

2
这个问题并不要求使用迭代器foreach的性能。我想知道的是,如果反复调用的成本charAt最终是比任何小于或大于以执行单一通话费用toCharArray
奥斯卡·洛佩斯

1
有人用StringCharacterIterator做过分析吗?
bdrx

Answers:


351

最新更新:在生产环境中尝试(不建议使用)之前,请先阅读以下内容: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[]字符串的支持。

在64种AMD Phenom II 4核心955 @ 3.2 GHZ(在客户端模式和服务器模式下)上使用9种不同技术(参见下文!)对JDK 8(win32和win64)进行的完全随机基准测试表明,使用String.charAt(n)这种方法对于小型计算机而言是最快的字符串和那个reflection用于访问String支持数组的字符串的速度几乎是大型字符串的两倍。

本实验

  • 尝试了9种不同的优化技术。

  • 所有字符串内容都是随机的

  • 测试以0、1、2、4、8、16等开头的字符串大小为2的倍数进行。

  • 每个字符串大小进行1000次测试

  • 每次将测试改组为随机顺序。换句话说,每次测试都以随机顺序进行,超过1000次。

  • 整个测试套件都向前和向后进行,以显示JVM预热对优化和时间的影响。

  • 整个套件执行两次,一次在-client模式下,另一个在-server模式下。

结论

-客户端模式(32位)

对于长度1到256个字符的字符串,呼叫string.charAt(i)胜出率是平均每秒处理1340万到5.88亿个字符。

此外,总体来说,它(客户端)和客户端(服务器)的速度分别提高了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%)。在此范围内的平均速度为每秒11.11亿个字符。

必须提前获取字段,然后可以在库中的不同字符串上重新使用它。有趣的是,与上面的代码不同,对于Field访问,使用本地最终长度变量比在循环检查中使用'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);
   }

-server模式下的特殊注释

在我的AMD 64机器上的64位Java机器上,在服务器模式下以服务器模式获得32个字符长的字符串之后,开始进行现场访问。在客户端模式下,只有512个字符的长度才能看到。

同样值得一提的是,当我在服务器模式下运行JDK 8(32位构建)时,无论大小字符串,整体性能都降低了7%。这是JDK 8早期版本的内部版本121 Dec 2013。因此,目前看来,32位服务器模式比32位客户端模式慢。

话虽如此……似乎唯一值得调用的服务器模式是在64位计算机上。否则实际上会影响性能。

对于在-server modeAMD64上运行的32位版本,我可以这样说:

  1. 总的来说,String.charAt(i)是明显的赢家。尽管介于8到512个字符之间,但在“新”,“重用”和“领域”中还是赢家。
  2. 在客户端模式下,String.charAt(i)的速度提高了45%
  3. 在客户端模式下,大型字符串的字段访问速度是后者的两倍。

同样值得一提的是,String.chars()(Stream和并行版本)是破产。比其他任何方式都要慢。该StreamsAPI是执行一般的字符串操作一个相当缓慢的方式。

愿望清单

Java String可以使谓词接受优化的方法,例如contains(predicate),forEach(consumer),forEachWithIndex(consumer)。因此,无需用户知道长度或重复调用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

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

“流”-使用新的JAVA-8 String的IntStream并通过它进行检查

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

“ streamPara”-同样,但是OH-LA-LA-并行!

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

“重用”-使用字符串内容重用可重复使用的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;
}

“ new1” –从STRING获得新的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”-相同,但使用“ FOR-EACH”

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

“ field1”-幻想!可获取字符串内部字符的参考字段[]

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位相同,除了Field访问在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 2014年

2
定义变量根本需要在方法字节码中进行堆栈操作。但是,从识别您的算法开始,优化可以快速跟踪实际机器代码中的重复操作,而不会产生变量分配的开销。此类优化有时存在于字节码编译器中,有时则不存在。这完全取决于jvm是否足够聪明:-)
协调员

2
@DavidS,数字是每个被检查字符的速率(以纳秒为单位)。越小越好。
协调员

14

这只是您不应该担心的微观优化。

char[] chars = str.toCharArray();

返回一个str字符数组的副本(在JDK中,它通过调用返回一个字符的副本System.arrayCopy)。

除此之外,str.charAt()仅检查索引是否确实在范围内,并在数组索引内返回一个字符。

第一个不会在JVM中创建额外的内存。


不回答问题。这个问题与性能有关。众所周知,OP可能已经发现遍历字符串是应用程序中的主要成本。
rghome

9

出于好奇并与Saint Hill的答案进行比较。

如果需要处理大量数据,则不应在客户端模式下使用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
顺便说一句:更新的JVM自动找出哪些-server或-client时工作最佳(通常)的:docs.oracle.com/javase/7/docs/technotes/guides/vm/...
jontejj

2
@jontejj实际上不是那么简单。如果您在Windows上运行32位JVM,则JVM将始终默认为客户端。
ceklock 2013年

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 2012年

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

看起来niether更快或更慢

    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( )之前在String类检查索引中的char数组中查找char。看起来足够短的Strings first方法(即chatAtmethod)由于此索引检查而变慢了。但是,如果String足够长,则复制整个char数组会变慢,并且第一种方法会更快。字符串越长,执行速度越慢toCharArray。尝试更改for(int j = 0; j < 10000; j++)循环限制以查看它。如果我们让JVM预热,则代码运行速度会更快,但是比例是相同的。

毕竟,这只是微优化。


您可以试一试这个for:in选项,只是为了好玩吗?
dasblinkenlight 2012年

2
您的基准测试有缺陷:它不允许JIT进行优化;JIT可以完全删除循环,因为它们不执行任何操作。
JB Nizet 2012年

字符串既不Iterable是数组也不是数组。
Piotr Gwiazda 2012年

2
这不是有效的测试,您已经用测试1“预热”了JVM,这可能会使结果偏向于测试2。无论如何,OP的整个问题都有微优化的味道。
感知2012年

1
真正。预热后(请参见编辑2),两个时间均较小,但仍彼此靠近。在我的示例中,第二次测试要快一些。但是,如果我将字符串变长,第一个会更快。字符串较长,第二次测试速度较慢,这归因于char数组副本。只是做第一种方式。
Piotr Gwiazda 2012年

2

String.toCharArray()创建一个新的char数组,表示分配字符串长度的内存,然后使用System.arraycopy()复制该字符串的原始char数组,然后将此副本返回给调用方。String.charAt()返回i原始副本位置的字符,这就是为什么String.charAt()它比更快的原因String.toCharArray()。虽然,String.toCharArray()从原始String数组返回copy而不是char,而String.charAt()从原始char数组返回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都复制到此新的char数组,因此我想第一个会更快(并且更少的内存消耗)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.