【问题标题】:Why is String.toUpperCase() so slow?为什么 String.toUpperCase() 这么慢?
【发布时间】:2016-02-11 09:47:18
【问题描述】:

这段代码比标准的 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 ms - 差不多 10 秒

** 更新

我添加了“拉丁”检查并将其用作基准(对于那些不相信我的人):

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% 左右。

【问题讨论】:

  • 您的代码不适用于拉丁字母以外的符号;不注意语言环境等。
  • 你是怎么发现这段代码快了 3 倍的?
  • 因为String#toUpperCase() 必须处理整个 Unicode,而不仅仅是标准的 latin a-z
  • Ěščřžýáíéöťďň 等。也许 Java 9 会有一条快速路径(其中 ASCII 字符串将被更有效地编码),但目前还没有。您只能将代码用于 ASCII 编码。如果您知道可以使用它,请这样做,有支持这种情况的库:Guava's Ascii#toUpperCase()
  • 此代码在土耳其语区域设置中是错误的。 'i'(带点的 i)的大写在土耳其语中不是 'I'(不带点的 I),而是 Unicode U+0130(上面带点的拉丁文大写字母 I)

标签: java string performance uppercase lowercase


【解决方案1】:

检查source code 中的java.lang.String 是有益的:

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

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

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

“正确操作”的惩罚是toUppercase 的标准版本比您的版本慢1。但如果您的版本不会,它会给出正确的答案。

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


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

【讨论】:

  • 感谢您提供如此完整的答案,我确实检查了 UTF-8 和 UTF-16,两者都应该适用于我的版本。所以它似乎与 ASCII、UTF-8、UTF-16 “兼容”,这应该是我想使用它的 99% 的时间。我将不得不检查内存使用情况,因为每次转换都会生成 2 个副本。
  • 如果您使用代码平面零之外的字符,则它与 UTF-8 或 UTF-16 不兼容。
【解决方案2】:

我运行了简单的 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倍。

【讨论】:

  • 抱歉,但在我的基准测试中,运行 NextSessionId() 2,000,000 次我得到了这些:toUpperString(String) = 26957.152 ms 和 String.toUpperCase() = 32568.200 ms。这意味着您的随机字符串的生成大部分时间都用完了,并且正在使您的结果复杂化。将它们放在一个预先生成的数组中,这将增加很少的时间。
  • 您能分享一下您正在执行的测试吗?
【解决方案3】:

换句话说,是否存在此代码不起作用的情况?

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

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

我相信在其他语言中还有很多类似的特殊情况。

【讨论】:

猜你喜欢
  • 2021-09-03
  • 2016-09-28
  • 2020-02-08
  • 2012-07-17
  • 2011-11-07
  • 2015-08-24
  • 2013-08-06
  • 2014-07-16
  • 2011-01-02
相关资源
最近更新 更多