从Java String中删除所有不可打印字符的最快方法

Gre*_*Cat 80 java string optimization performance micro-optimization

StringJava中删除所有不可打印字符的最快方法是什么?

到目前为止,我已经尝试并测量了138字节,131个字符的字符串:

  • 字符串replaceAll()- 最慢的方法
    • 517009结果/秒
  • 预编译模式,然后使用匹配器 replaceAll()
    • 637836结果/秒
  • 使用StringBuffer,codepointAt()逐个获取代码点并附加到StringBuffer
    • 711946结果/秒
  • 使用StringBuffer,charAt()逐个获取字符并附加到StringBuffer
    • 1052964结果/秒
  • 预分配char[]缓冲区,charAt()逐个获取字符并填充此缓冲区,然后转换回String
    • 2022653结果/秒
  • 预分配2个char[]缓冲区 - 旧的和新的,一次获取现有String的所有字符,getChars()逐个迭代旧缓冲区并填充新缓冲区,然后将新缓冲区转换为String - 我自己的最快版本
    • 2502502结果/秒
  • 与2个缓冲区相同的东西 - 仅使用byte[],getBytes()并指定编码为"utf-8"
    • 857485结果/秒
  • 具有2个byte[]缓冲区的相同内容,但将编码指定为常量Charset.forName("utf-8")
    • 791076结果/秒
  • 与2个byte[]缓冲区相同的东西,但指定编码为1字节本地编码(几乎没有理智的事情要做)
    • 370164结果/秒

我最好的尝试如下:

    char[] oldChars = new char[s.length()];
    s.getChars(0, s.length(), oldChars, 0);
    char[] newChars = new char[s.length()];
    int newLen = 0;
    for (int j = 0; j < s.length(); j++) {
        char ch = oldChars[j];
        if (ch >= ' ') {
            newChars[newLen] = ch;
            newLen++;
        }
    }
    s = new String(newChars, 0, newLen);
Run Code Online (Sandbox Code Playgroud)

有关如何让它更快的任何想法?

回答一个非常奇怪的问题的奖励点:为什么直接使用"utf-8"字符集名称会比使用预先分配的静态const产生更好的性能Charset.forName("utf-8")

更新

  • 来自棘轮怪胎的建议令人印象深刻的3105590结果/秒表现,+ 24%的改善!
  • Ed Staub的建议产生了另一项改进 - 3471017结果/秒,比之前最好的+ 12%.

更新2

我已尽力收集所有提议的解决方案及其交叉突变,并将其作为github小型基准框架发布.目前它运动17种算法.其中一个是"特殊的" - Voo1算法(由SO用户Voo提供)采用复杂的反射技巧,从而实现了恒星速度,但它混淆了JVM字符串的状态,因此它是单独进行基准测试的.

欢迎您查看并运行它以确定您的盒子上的结果.这是我对我的结果的总结.它的规格如下:

  • Debian sid
  • Linux 2.6.39-2-amd64(x86_64)
  • 从包中安装Java sun-java6-jdk-6.24-1,JVM将自己标识为
    • Java(TM)SE运行时环境(版本1.6.0_24-b07)
    • Java HotSpot(TM)64位服务器VM(内置19.1-b02,混合模式)

给定不同的输入数据集,不同的算法最终显示出不同的结果.我在3种模式下运行了基准测试:

同一个字符串

此模式适用于由StringSource类提供的相同单个字符串作为常量.摊牌是:

 Ops / s  ? Algorithm
?????????????????????????????????????????
6 535 947 ? Voo1
?????????????????????????????????????????
5 350 454 ? RatchetFreak2EdStaub1GreyCat1
5 249 343 ? EdStaub1
5 002 501 ? EdStaub1GreyCat1
4 859 086 ? ArrayOfCharFromStringCharAt
4 295 532 ? RatchetFreak1
4 045 307 ? ArrayOfCharFromArrayOfChar
2 790 178 ? RatchetFreak2EdStaub1GreyCat2
2 583 311 ? RatchetFreak2
1 274 859 ? StringBuilderChar
1 138 174 ? StringBuilderCodePoint
  994 727 ? ArrayOfByteUTF8String
  918 611 ? ArrayOfByteUTF8Const
  756 086 ? MatcherReplace
  598 945 ? StringReplaceAll
  460 045 ? ArrayOfByteWindows1251

以图表形式: 相同的单个字符串图表http://www.greycat.ru/img/os-chart-single.png

多个字符串,100%的字符串包含控制字符

源字符串提供程序使用(0..127)字符集预先生成大量随机字符串 - 因此几乎所有字符串都包含至少一个控制字符.算法以循环方式从此预生成的数组接收字符串.

 Ops / s  ? Algorithm
?????????????????????????????????????????
2 123 142 ? Voo1
?????????????????????????????????????????
1 782 214 ? EdStaub1
1 776 199 ? EdStaub1GreyCat1
1 694 628 ? ArrayOfCharFromStringCharAt
1 481 481 ? ArrayOfCharFromArrayOfChar
1 460 067 ? RatchetFreak2EdStaub1GreyCat1
1 438 435 ? RatchetFreak2EdStaub1GreyCat2
1 366 494 ? RatchetFreak2
1 349 710 ? RatchetFreak1
  893 176 ? ArrayOfByteUTF8String
  817 127 ? ArrayOfByteUTF8Const
  778 089 ? StringBuilderChar
  734 754 ? StringBuilderCodePoint
  377 829 ? ArrayOfByteWindows1251
  224 140 ? MatcherReplace
  211 104 ? StringReplaceAll

以图表形式: 多个字符串,100%浓度http://www.greycat.ru/img/os-chart-multi100.png

多个字符串,1%的字符串包含控制字符

与之前相同,但只有1%的字符串是使用控制字符生成的 - 其他99%是使用[32..127]字符集生成的,因此它们根本不能包含控制字符.这个合成负载在我的位置最接近这个算法的实际应用.

 Ops / s  ? Algorithm
?????????????????????????????????????????
3 711 952 ? Voo1
?????????????????????????????????????????
2 851 440 ? EdStaub1GreyCat1
2 455 796 ? EdStaub1
2 426 007 ? ArrayOfCharFromStringCharAt
2 347 969 ? RatchetFreak2EdStaub1GreyCat2
2 242 152 ? RatchetFreak1
2 171 553 ? ArrayOfCharFromArrayOfChar
1 922 707 ? RatchetFreak2EdStaub1GreyCat1
1 857 010 ? RatchetFreak2
1 023 751 ? ArrayOfByteUTF8String
  939 055 ? StringBuilderChar
  907 194 ? ArrayOfByteUTF8Const
  841 963 ? StringBuilderCodePoint
  606 465 ? MatcherReplace
  501 555 ? StringReplaceAll
  381 185 ? ArrayOfByteWindows1251

以图表形式: 多个字符串,1%浓度http://www.greycat.ru/img/os-chart-multi1.png

我很难决定谁提供了最好的答案,但鉴于现实世界的应用程序最好的解决方案是由Ed Staub给出/启发的,我想我的答案是公平的.感谢所有参与此活动的人,您的意见非常有帮助且非常宝贵.随意在您的盒子上运行测试套件并提出更好的解决方案(工作JNI解决方案,任何人?).

参考

rat*_*eak 25

使用1个char数组可以更好地工作

int length = s.length();
char[] oldChars = new char[length];
s.getChars(0, length, oldChars, 0);
int newLen = 0;
for (int j = 0; j < length; j++) {
    char ch = oldChars[j];
    if (ch >= ' ') {
        oldChars[newLen] = ch;
        newLen++;
    }
}
s = new String(oldChars, 0, newLen);
Run Code Online (Sandbox Code Playgroud)

我避免一再打电话给 s.length();

另一个可能有用的微优化是

int length = s.length();
char[] oldChars = new char[length+1];
s.getChars(0, length, oldChars, 0);
oldChars[length]='\0';//avoiding explicit bound check in while
int newLen=-1;
while(oldChars[++newLen]>=' ');//find first non-printable,
                       // if there are none it ends on the null char I appended
for (int  j = newLen; j < length; j++) {
    char ch = oldChars[j];
    if (ch >= ' ') {
        oldChars[newLen] = ch;//the while avoids repeated overwriting here when newLen==j
        newLen++;
    }
}
s = new String(oldChars, 0, newLen);
Run Code Online (Sandbox Code Playgroud)

  • 哼!这是个好主意!我的生产环境中99.9%的字符串不会真正需要剥离 - 我可以进一步改进它以消除第一个`char []`分配并按原样返回String,如果没有发生剥离的话. (2认同)

Ed *_*aub 11

如果将此方法嵌入到不跨线程共享的类中是合理的,那么您可以重用缓冲区:

char [] oldChars = new char[5];

String stripControlChars(String s)
{
    final int inputLen = s.length();
    if ( oldChars.length < inputLen )
    {
        oldChars = new char[inputLen];
    }
    s.getChars(0, inputLen, oldChars, 0);
Run Code Online (Sandbox Code Playgroud)

等等...

这是一个很大的胜利 - 大约20%左右,因为我理解当前最好的情况.

如果要在潜在的大字符串上使用它并且内存"泄漏"是一个问题,可以使用弱引用.


Voo*_*Voo 11

根据我的措施,我已经击败了当前最好的方法(使用预分配阵列的怪胎解决方案)大约30%.怎么样?卖我的灵魂.

我相信到目前为止所有参与讨论的人都知道这违反了几乎任何基本的编程原则,但是哦.无论如何,只有当字符串的使用字符数组不在其他字符串之间共享时,以下内容才有效 - 如果它必须调试它的人将决定杀死你(不调用substring()并在文字字符串上使用它)这应该工作,因为我不明白为什么JVM会从外部源读取实际的唯一字符串).虽然不要忘记确保基准代码不这样做 - 这极有可能并且显然有助于反射解决方案.

无论如何我们去:

    // Has to be done only once - so cache those! Prohibitively expensive otherwise
    private Field value;
    private Field offset;
    private Field count;
    private Field hash;
    {
        try {
            value = String.class.getDeclaredField("value");
            value.setAccessible(true);
            offset = String.class.getDeclaredField("offset");
            offset.setAccessible(true);
            count = String.class.getDeclaredField("count");
            count.setAccessible(true);
            hash = String.class.getDeclaredField("hash");
            hash.setAccessible(true);               
        }
        catch (NoSuchFieldException e) {
            throw new RuntimeException();
        }

    }

    @Override
    public String strip(final String old) {
        final int length = old.length();
        char[] chars = null;
        int off = 0;
        try {
            chars = (char[]) value.get(old);
            off = offset.getInt(old);
        }
        catch(IllegalArgumentException e) {
            throw new RuntimeException(e);
        }
        catch(IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        int newLen = off;
        for(int j = off; j < off + length; j++) {
            final char ch = chars[j];
            if (ch >= ' ') {
                chars[newLen] = ch;
                newLen++;
            }
        }
        if (newLen - off != length) {
            // We changed the internal state of the string, so at least
            // be friendly enough to correct it.
            try {
                count.setInt(old, newLen - off);
                // Have to recompute hash later on
                hash.setInt(old, 0);
            }
            catch(IllegalArgumentException e) {
                e.printStackTrace();
            }
            catch(IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        // Well we have to return something
        return old;
    }
Run Code Online (Sandbox Code Playgroud)

对于我的TestString是获取3477148.18ops/s2616120.89ops/s旧的变体.我很确定唯一的方法就是用C语言编写它(可能不是)或者到目前为止没有人想过的完全不同的方法.虽然我绝对不确定不同平台的时序是否稳定 - 至少在我的盒子(Java7,Win7 x64)上产生可靠的结果.

  • @GreyCat是的,它肯定有一些附加的条件,说实话,我几乎只写了它,因为我敢肯定没有明显的方法可以进一步改善您当前的最佳解决方案。在某些情况下,我确定它可以很好地工作(在剥离它之前没有子字符串或intern调用),但这是因为对当前的某个Hotspot版本有所了解(即afaik,它不会从IO读取内部字符串-不会)尤其有用)。如果确实需要这些额外的x%,这可能会很有用,但是如果需要,您可以增加一个基准,以了解您仍然可以提高多少;) (3认同)