在一个字符串中迭代所有字符的最快方法

187

在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()两次调用之间的差异,那将会很棒。


24
for (char c : chars) 发生了什么? - Sergey Kalinichenko
第一个应该更快,而且字符串本质上就是字符数组。 - Keagan Ladds
2
这个问题并不询问使用迭代器、foreach的性能。我想知道的是,重复调用 charAt 的成本是否比执行单个调用 toCharArray 的成本要小或大。 - Óscar López
@dasblinkenlight,我问题中的第二种形式等同于 for (char c : chars) - Óscar López
1
有人使用过StringCharacterIterator进行分析吗? - bdrx
9个回答

387

FIRST UPDATE: 在生产环境中(不建议),在尝试此方法之前,请先阅读此文:http://www.javaspecialists.eu/archive/Issue237.html。自Java 9开始,其描述的解决方案将无法再起作用,因为现在Java默认会将字符串存储为byte[]。

SECOND UPDATE: 我在AMDx64 8core和源1.8上测试发现,在2016-10-25时,使用'charAt'和字段访问没有区别。似乎jvm已经足够优化了,以内联和流线型任何'string.charAt(n)'调用。

THIRD UPDATE: 我在Ryzen 1950-X 16 core和源1.14上测试发现,“charAt1”比字段访问慢9倍,“charAt2”比字段访问慢4倍。字段访问是明显的赢家。请注意,程序需要使用byte[]访问Java 9+版本的jvms。

这完全取决于正在检查的String的长度。如果像问题所述,它是用于长字符串,则检查字符串的最快方法是使用反射来访问字符串的后备char[]。

使用JDK 8(win32和win64)对64位AMD Phenom II 4核955 @ 3.2 GHZ进行完全随机化基准测试(在客户端模式和服务器模式下),使用9种不同技术(见下文!)展示了对小字符串使用String.charAt(n)是最快的,而对于大字符串使用反射访问字符串后备数组几乎快两倍。

实验

  • 尝试9种不同的优化技术。
  • 所有字符串内容都是随机的
  • 测试以两个字符的倍数开始的字符串大小进行,0,1,2,4,8,16等。
  • 每个字符串大小测试1000次
  • 每次测试都会随机洗牌。换句话说,这些测试每次都以随机顺序进行测试,超过1000次。
  • 整个测试套件正向和反向完成,以显示JVM预热对优化和时间的影响。
  • 整个套件完成两次,一次在-client模式下,另一次在-server模式下。

结论

-客户端模式(32位)

对于长度为1到256个字符的字符串,调用string.charAt(i)平均处理13.4百万到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,然后可以在库中在不同的字符串上重复使用它。有趣的是,与上面的代码不同,使用Field访问时,在循环检查中使用本地final长度变量比使用'chars.length'快9%。以下是如何设置 Field 访问为最快:

   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模式的特殊注释

64位Java机器上,在服务器模式下,从32个字符长度的字符串开始,字段访问会在AMD64机器上获胜。在客户端模式下直到512个字符长度才能看到这种情况。

另外值得注意的是,我认为,在运行JDK 8(32位版本)的服务器模式时,无论是对于大字符串还是小字符串,整体性能都比较慢,相比之下,客户端模式要快7%。这是在2013年12月发布的JDK 8早期版本121中出现的。因此,目前似乎32位服务器模式比32位客户端模式慢。

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

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

  1. String.charAt(i)总体上表现最好。虽然在8到512个字符之间,“new”、“reuse”和“field”之间也有赢家。
  2. String.charAt(i)在客户端模式下快45%。
  3. 对于大字符串,在客户端模式下字段访问速度是服务器模式的两倍。

还值得一提的是,String.chars()(流和并行版本)表现不佳。比其他任何方法都要慢。 Streams API是执行常规字符串操作的一种相当缓慢的方式。

愿望清单

Java String可能会有接受优化方法的谓词,比如contains(predicate)、forEach(consumer)、forEachWithIndex(consumer)。这样,用户就不需要知道长度或重复调用String方法,这些可以帮助解析库提高速度。

继续梦想吧 :)

快乐字符串!

~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()而不是创建一个final local 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字符串IntStream,并传递一个谓词来进行检查

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

"重用" -- 用字符串内容重新填充可重用的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" -- 从字符串中获取一个新的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" -- 获取用于访问字符串内部 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);
    }
}

"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模式的复合结果(正向和反向测试结合)

注意:在我的AMD64机器上,使用Java 32位的-client模式和使用Java 64位的-server模式与下面的结果相同。

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 32位相同,Java 64位的服务器模式除了字段访问从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
7
为什么“for (int i = 0; i < data.length(); i++)”比将data.length()定义为一个final局部变量更快呢? - skyin
2
定义变量,无论如何,都需要在方法字节码中进行堆栈操作。但是,通过识别您的算法,优化可以在实际机器代码中快速跟踪该重复操作,而无需进行变量分配的开销。这样的优化有时存在于字节码编译器中,有时不存在。这完全取决于jvm是否足够聪明 :-) - The Coordinator
2
@DavidS 这些数字代表每个字符检查的速率(以纳秒为单位)。数字越小,效果越好。 - The Coordinator
显示剩余20条评论

15

这只是一种微小的优化,你不需要担心。

char[] chars = str.toCharArray();

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

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

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


6
未回答问题。这个问题是关于性能的。你不知道,提问者可能已经发现在他们的应用程序中迭代字符串是一个主要的开销。 - rghome

10

只是出于好奇,与圣希尔的答案进行比较。

如果需要处理大量数据,则不应在客户端模式下使用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个字符长度的字符串之后,字段访问变得更加快速(太疯狂了!!)。因此...服务器模式可以使所有事情都更快。非常有趣! - The Coordinator
1
是的,反射方法确实更快。有趣。 - ceklock
3
顺便提一下:较新的JVM会自动确定哪个选项(通常是-server或-client)最适合使用:http://docs.oracle.com/javase/7/docs/technotes/guides/vm/server-class.html - 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
2
冒着重新激活一个8年前的帖子的风险...字符串背后的字符数组可能比字符串本身更大。也就是说,如果你有一个字符串"abcde",然后你使用子字符串提取"bcd"到一个新的字符串中,那么新的字符串将由与第一个字符串完全相同的字符数组支持。这就是为什么字符串类维护偏移量和计数-以便它知道数组中哪些字符是表示此字符串的字符。因此,范围检查很重要,否则可能会访问超出此字符串末尾的字符。 - dty

4

String.toCharArray()方法创建了一个新的char数组,分配了与字符串长度相等的内存空间,然后使用System.arraycopy()复制原始char数组,并将其返回给调用者。 String.charAt()方法从原始复制品中返回位置i处的字符,这就是为什么String.charAt()String.toCharArray()快的原因。 虽然String.toCharArray()返回的是副本而不是原始字符串数组中的字符,但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;
}

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()方法在String类内部的char数组中查找字符并检查索引。 如果字符串足够短,则第一种方法(即charAt方法)由于此索引检查而稍微慢一些。但是,如果字符串足够长,则复制整个字符数组变得更慢,第一种方法更快。字符串越长,toCharArray的性能越慢。尝试更改for(int j=0;j<10000;j++)循环中的限制即可看到结果。 如果让JVM预热,代码运行得更快,但比例是相同的。
总之,这只是微小的优化。

2
你的基准测试有缺陷:它不允许JIT进行优化; JIT可以完全删除循环,因为它们没有任何作用。 - JB Nizet
字符串既不是可迭代对象也不是数组。 - Piotr Gwiazda
2
这不是一个有效的测试,你已经通过Test 1“热身”了JVM,这可能会偏向于Test 2的结果。此外,该OP的整个问题都带有微小的优化味道。 - Perception
1
正确。经过热身(请参见Edit 2),两次测试时间都更短,但仍相近。在我的示例中,第二个测试略快一些。但如果我让字符串变长,第一个测试就会更快。由于字符数组复制,字符串越长,第二个测试就越慢。建议采用第一种方式完成操作。 - Piotr Gwiazda
@PeterGwiazda 请编辑您的答案,并将最后一条评论作为结论添加进去,我会接受它作为正确答案。 - Óscar López
显示剩余2条评论

1

在许多实际场景中,字符值和原始字符串中的索引都是必要的。这里是另一组使用 openjdk 版本 "19.0.1" 2022-10-18 和 JMH 1.35 进行测试的 String 迭代基准。

源代码

/**
 * Iterates over a string, much like {@link CharacterIterator}, but faster.
 * <p>
 * <strong>Caution:</strong> This class offers minimal bounds checking to eke
 * out some efficiency.
 * </p>
 */
public final class FastCharacterIterator {

  private final String mS;
  private final int mLen;

  /**
   * Starts at 0, not guaranteed to be within bounds.
   */
  private int mPos;

  /**
   * Constructs a new iterator that can advance through the given string
   * one character at a time.
   *
   * @param s The string to iterate.
   */
  public FastCharacterIterator( final String s ) {
    assert s != null;

    mS = s;
    mLen = s.length();
  }

  /**
   * Returns the iterated index. The return value is not guaranteed to be
   * within the string bounds.
   *
   * @return The iterated index.
   */
  public int index() {
    return mPos;
  }

  /**
   * Returns the character at the currently iterated position in the string.
   * This method performs bounds checking by catching an exception because
   * usually parsing is complete when there are no more characters to iterate,
   * meaning that 99.99% of the time, explicit bounds checking is superfluous.
   *
   * @return {@link CharacterIterator#DONE} if there are no more characters.
   */
  public char current() {
    try {
      return mS.charAt( mPos );
    } catch( final Exception ex ) {
      return DONE;
    }
  }

  /**
   * Returns the next character in the string and consumes it.
   *
   * @return {@link CharacterIterator#DONE} if there are no more characters.
   */
  public char advance() {
    try {
      return mS.charAt( ++mPos );
    } catch( final Exception ex ) {
      return DONE;
    }
  }

  /**
   * Returns the next character in the string without consuming it. Multiple
   * consecutive calls to this method will return the same value.
   *
   * @return {@link CharacterIterator#DONE} if there are no more characters.
   */
  public char peek() {
    try {
      return mS.charAt( mPos + 1 );
    } catch( final Exception ex ) {
      return DONE;
    }
  }

  /**
   * Advances to the next character in the string, without bounds checking.
   */
  public void next() {
    mPos++;
  }

  /**
   * Advances to the previous character in the string, without bounds checking.
   */
  public void prev() {
    mPos--;
  }

  /**
   * Answers whether {@link #next()} followed by {@link #current()} is safe.
   *
   * @return {@code true} if there are more characters to be iterated.
   */
  public boolean hasNext() {
    return mPos < mLen;
  }

  /**
   * Parse all characters that match a given function.
   *
   * @param f The function that determines when skipping stops.
   */
  @SuppressWarnings( "StatementWithEmptyBody" )
  public void skip( final Function<Character, Boolean> f ) {
    assert f != null;

    while( f.apply( advance() ) ) ;

    // The loop always overshoots by one character.
    prev();
  }

  /**
   * Creates a string from a subset of consecutive characters in the string
   * being iterated. The calling class is responsible for bounds-checking.
   *
   * @param began The starting index, must be greater than or equal to zero
   *              and less than or equal to {@code ended}.
   * @param ended The ending index, must be less than the string length.
   * @return A substring of the iterated string.
   * @throws IndexOutOfBoundsException Either or both parameters exceed the
   *                                   string's boundaries.
   */
  public String substring( final int began, final int ended ) {
    assert began >= 0;
    assert began <= ended;
    assert ended < mLen;

    return mS.substring( began, ended );
  }
}

测试代码

用于测试的完整源代码:

import com.whitemagicsoftware.keenquotes.util.FastCharacterIterator;
import org.openjdk.jmh.annotations.Benchmark;

import java.text.StringCharacterIterator;
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicInteger;

import static java.lang.Math.random;
import static java.text.CharacterIterator.DONE;

/**
 * Higher scores mean faster code:
 *
 * <pre>
 * Benchmark                     Mode   Cnt    Score    Error  Units
 * test_CharArrayIterator        thrpt   25  753.960 ±  0.972  ops/s
 * test_CharAtIterator           thrpt   25  878.016 ±  0.884  ops/s
 * test_FastCharacterIterator    thrpt   25  803.041 ± 48.422  ops/s
 * test_StreamIterator           thrpt   25  101.416 ±  0.053  ops/s
 * test_StringCharacterIterator  thrpt   25  580.341 ±  0.432  ops/s
 * test_StringTokenizer          thrpt   25  174.121 ±  8.282  ops/s
 * </pre>
 */
@SuppressWarnings( "unused" )
public class StringIterationBenchmark {
  private static final int STRLEN = 1024 * 1024;
  private static final String CHARSET =
    "ABCDEFGHIJKLM NOPQRSTUVWXYZ abcdefghijklm nopqrstuvxyz 01234 5 6789";

  private static final String sText;

  static {
    final var len = CHARSET.length();
    final var buffer = new StringBuilder( STRLEN );

    for( var i = 0; i < STRLEN; i++ ) {
      buffer.append( CHARSET.charAt( (int) (len * random()) ) );
    }

    sText = buffer.toString();
  }

  private static String getText() {
    return sText;
  }

  @Benchmark
  public void test_FastCharacterIterator() {
    final var s = getText();
    final var i = new FastCharacterIterator( s );
    var spaces = 0;

    char ch = ' ';

    while( (ch = i.advance()) != DONE ) {
      if( ch == ' ' ) {
        spaces++;
      }
    }

    fail( i.index(), s.length() );
  }

  @Benchmark
  public void test_CharAtIterator() {
    final var s = getText();
    final var length = s.length();
    var index = 0;
    var spaces = 0;

    while( index < length ) {
      final var ch = s.charAt( index );

      if( ch == ' ' ) {
        spaces++;
      }

      index++;
    }

    fail( index, length );
  }

  @Benchmark
  public void test_StringCharacterIterator() {
    final var s = getText();
    final var i = new StringCharacterIterator( s );
    var index = 0;
    var spaces = 0;

    char ch = ' ';

    while( ch != DONE ) {
      ch = i.next();

      if( ch == ' ' ) {
        spaces++;
      }

      index++;
    }

    fail( index, s.length() );
  }

  @Benchmark
  public void test_CharArrayIterator() {
    final var s = getText();
    final var i = s.toCharArray();
    var index = 0;
    var spaces = 0;

    for( final var ch : i ) {
      if( ch == ' ' ) {
        spaces++;
      }

      index++;
    }

    fail( index, s.length() );
  }

  @Benchmark
  public void test_StringTokenizer() {
    final var s = getText();
    final var i = new StringTokenizer( s, " ", true );
    var index = 0;
    var spaces = 0;

    while( i.hasMoreTokens() ) {
      final var token = i.nextToken();

      if( token.isBlank() ) {
        spaces++;
      }

      index += token.length();
    }

    fail( index, s.length() );
  }

  @Benchmark
  public void test_StreamIterator() {
    final var s = getText();
    final var index = new AtomicInteger();
    final var spaces = new AtomicInteger();

    s.chars().forEach( codepoint -> {
      final var ch = Character.valueOf( (char) codepoint );

      if( ch == ' ' ) {
        spaces.incrementAndGet();
      }

      index.incrementAndGet();
    } );

    fail( index.get(), s.length() );
  }

  private static void fail( final int index, final int length ) {
    if( index != length ) {
      throw new RuntimeException( "Fail" );
    }
  }
}

评论

欢迎改进代码并重新运行基准测试。投诉将被发送到/dev/null。 StackOverflow是一个维基。


1
第二个会创建一个新的字符数组,并将字符串中的所有字符复制到这个新的字符数组中,因此我猜第一个更快(并且占用更少的内存)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接