为什么 String.toUpperCase() 这么慢?

Why is String.toUpperCase() so slow?

此代码比标准 String.toUpperCase() 函数快约 3 倍:

public static String toUpperString(String pString) {
    if (pString != null) {
        char[] retChar = pString.toCharArray();
        for (int idx = 0; idx < pString.length(); idx++) {
            char c = retChar[idx];
            if (c >= 'a' && c <= 'z') {
                retChar[idx] = (char) (c & -33);
            }
        }
        return new String(retChar);
    } else {
        return null;
    }
}

为什么这么快? String.toUpperCase() 还做了哪些其他工作? 换句话说,是否存在此代码不起作用的情况?

执行 2,000,000 次随机长字符串(纯文本)的基准测试结果:

toUpperString(String):3514.339 毫秒 - 大约 3.5 秒
String.toUpperCase():9705.397 毫秒 - 将近 10 秒

** 更新

我已经添加了 "latin" 检查并将其用作基准(对于那些不相信我的人):

public class BenchmarkUpperCase {

    public static String[] randomStrings;

    public static String nextRandomString() {
        SecureRandom random = new SecureRandom();
        return new BigInteger(500, random).toString(32);
    }

    public static String customToUpperString(String pString) {
        if (pString != null) {
            char[] retChar = pString.toCharArray();
            for (int idx = 0; idx < pString.length(); idx++) {
                char c = retChar[idx];
                if (c >= 'a' && c <= 'z') {
                    retChar[idx] = (char) (c & -33);
                } else if (c >= 192) { // now catering for other than latin...
                    retChar[idx] = Character.toUpperCase(c);
                }
            }
            return new String(retChar);
        } else {
            return null;
        }
    }

    public static void main(String... args) {
        long timerStart, timePeriod = 0;
        randomStrings = new String[1000];
        for (int idx = 0; idx < 1000; idx++) {
            randomStrings[idx] = nextRandomString();
        }
        String dummy = null;

        for (int count = 1; count <= 5; count++) {
            timerStart = System.nanoTime();
            for (int idx = 0; idx < 20000000; idx++) {
                dummy = randomStrings[idx % 1000].toUpperCase();
            }
            timePeriod = System.nanoTime() - timerStart;
            System.out.println(count + " String.toUpper() : " + (timePeriod / 1000000));
        }

        for (int count = 1; count <= 5; count++) {
            timerStart = System.nanoTime();
            for (int idx = 0; idx < 20000000; idx++) {
                dummy = customToUpperString(randomStrings[idx % 1000]);
            }
            timePeriod = System.nanoTime() - timerStart;
            System.out.println(count + " customToUpperString() : " + (timePeriod / 1000000));
        }
    }

}

我得到这些结果:

1 String.toUpper() : 10724
2 String.toUpper() : 10551
3 String.toUpper() : 10551
4 String.toUpper() : 10660
5 String.toUpper() : 10575
1 customToUpperString() : 6687
2 customToUpperString() : 6684
3 customToUpperString() : 6686
4 customToUpperString() : 6693
5 customToUpperString() : 6710

仍然快了大约 60%。

我 运行 简单的 jmh 基准测试来比较两种方法 #toUpperString 和默认 j8 #toUpperCase 结果如下:

Benchmark                    Mode  Cnt     Score    Error  Units
MyBenchmark.customToString   avgt   20  3307.137 ± 81.192  ns/op
MyBenchmark.defaultToString  avgt   20  3384.921 ± 75.357  ns/op

测试实现是:

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(value = 1, warmups = 1)
@Threads(1)
public class MyBenchmark {

    public static String toUpperString(String pString) {
        if (pString != null) {
            char[] retChar = pString.toCharArray();
            for (int idx = 0; idx < pString.length(); idx++) {
                char c = retChar[idx];
                if (c >= 'a' && c <= 'z') {
                    retChar[idx] = (char) (c & -33);
                }
            }
            return new String(retChar);
        } else {
            return null;
        }
    }

    private SecureRandom random = new SecureRandom();

    public String nextSessionId() {
        return new BigInteger(130, random).toString(32);
    }


    @Setup
    public void init() {

    }

    @Benchmark
    public Object customToString() {
        return toUpperString(nextSessionId());
    }

    @Benchmark
    public String defaultToString() {
        return nextSessionId().toUpperCase();
    }

}

根据本次测试的score,此方法比默认快3倍。

In other words, are there cases in which this code will not work?

是的。即使您更新后的代码也无法在德语中正常工作,因为它没有涵盖“ß”的特殊情况。 此字母仅以小写形式存在,并转换为大写形式的 double s:

String bla = "blöße";
System.out.println(customToUpperString(bla)); // BLÖßE <- wrong
System.out.println(bla.toUpperCase(Locale.GERMANY)); // BLÖSSE <- right

我敢肯定在其他语言中还有很多这样的特殊情况。

针对 java.lang.String 检查 source code 很有启发性:

  1. 标准版本使用了相当长的篇幅来避免在不必要时创建新字符串。这需要对字符串进行两次传递。

  2. 标准版使用语言环境对象对所有字符进行大小写转换。您只对大于 192 的字符执行此操作。虽然这可能适用于常见的语言环境,但某些语言环境(当前或未来......或自定义)可能会有 "interesting" 大写规则适用于小于 192 的字符192也是。

  3. 标准版本通过 Unicode 代码点而不是代码单元正确地转换为大写。 (如果字符串包含代理字符,则按代码单元转换可能会中断或给出错误答案。)

"doing it correctly" 的惩罚是 toUppercase 的标准版本比您的 1 版本慢。但在您的版本不会给出正确答案的情况下,它会给出正确答案。

请注意,由于您正在测试 ASCII 字符串,因此您不会遇到 toUppercase 版本给出错误答案的情况。


1 - 根据您的基准...但请参阅其他答案!