从Java字符串中剥离所有不可打印字符的最快方法


81

String用Java剥离所有不可打印字符的最快方法是什么?

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

  • 字符串的replaceAll()-最慢的方法
    • 517009个结果/秒
  • 预编译模式,然后使用Matcher的 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);

关于如何使其更快的想法?

回答一个非常奇怪的问题的好处是:为什么直接使用“ 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类提供的作为常量的同一单个字符串。摊牌是:

 操作数│算法
──────────┼────────────────────────
6535947│Voo1
──────────┼────────────────────────
5350454│RatchetFreak2EdStaub1GreyCat1
5249343│EdStaub1
5002501│EdStaub1GreyCat1
4859 086│ArrayOfCharFromStringCharAt
4295532│RatchetFreak1
4045307│ArrayOfCharFromArrayOfChar
2 790 178│RatchetFreak2EdStaub1GreyCat2
2583311│RatchetFreak2
1274859│StringBuilderChar
1138174│StringBuilderCodePoint
  994727│ArrayOfByteUTF8String
  918611│ArrayOfByteUTF8Const
  756 086│MatcherReplace
  598945│StringReplaceAll
  460045│ArrayOfByteWindows1251

以图表形式:( 来源:greycat.ru相同的单字符串图表

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

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

 操作数│算法
──────────┼────────────────────────
2123142│Voo1
──────────┼────────────────────────
1782214│EdStaub1
1776199│EdStaub1GreyCat1
1694628│ArrayOfCharFromStringCharAt
1481481│ArrayOfCharFromArrayOfChar
1460067│RatchetFreak2EdStaub1GreyCat1
1438435│RatchetFreak2EdStaub1GreyCat2
1366494│棘轮怪2
1349710│棘轮怪胎1
  893176│ArrayOfByteUTF8String
  817127│ArrayOfByteUTF8Const
  778089│StringBuilderChar
  734754│StringBuilderCodePoint
  377829│ArrayOfByteWindows1251
  224140│MatcherReplace
  211 104│StringReplaceAll

以图表形式:( 来源:greycat.ru多串,浓度100%

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

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

 操作数│算法
──────────┼────────────────────────
3711952│Voo1
──────────┼────────────────────────
2851440│EdStaub1GreyCat1
2455796│EdStaub1
2426007│ArrayOfCharFromStringCharAt
2347969│RatchetFreak2EdStaub1GreyCat2
2242152│RatchetFreak1
2171553│ArrayOfCharFromArrayOfChar
1922707│RatchetFreak2EdStaub1GreyCat1
1857010│棘轮怪2
1 023 751│ArrayOfByteUTF8String
  939 055│StringBuilderChar
  907194│ArrayOfByteUTF8Const
  841963│StringBuilderCodePoint
  606465│MatcherReplace
  501555│StringReplaceAll
  381185│ArrayOfByteWindows1251

以图表形式:( 来源:greycat.ru多串,浓度1%

我很难决定谁提供了最佳答案,但是鉴于实际应用中最佳解决方案是由Ed Staub提供/启发的,我想标记他的答案是很公平的。感谢所有参与此活动的人,您的意见非常有帮助且非常宝贵。随时在您的设备上运行测试套件,并提出更好的解决方案(正在使用JNI解决方案,有人吗?)。

参考文献


21
“这个问题说明了研究工作” –嗯,通过。+1
古斯塔夫·巴克福斯

7
StringBuilder会比StringBuffer不同步的速度快一点,我刚才提到这是因为您标记了此micro-optimization

2
@Jarrod罗伯逊:好了,让我们所有的只读字段决赛,并提取s.length()了出来for,以及:-)环

3
空格下方的某些字符可以打印,例如\t\n。127以上的许多字符在您的字符集中都是不可打印的。
彼得·劳里

1
您是否初始化了容量为的字符串缓冲区s.length()
棘轮怪胎

Answers:


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

等等...

据我所知,这是一个巨大的胜利-20%左右。

如果要在可能较大的字符串上使用它,并且担心内存“泄漏”,则可以使用弱引用。


很好的主意!到目前为止,它使每秒计数达到3471017字符串-即比以前的最佳版本提高了12%。
GreyCat 2011年

25

使用1个字符数组可能会更好

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

而且我避免了多次致电 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);

1
谢谢!您的版本每秒产生3105590字符串-极大的改进!
GreyCat

newLen++;:使用预增量++newLen;怎么样?-(++j也在循环中)。在这里看看:stackoverflow.com/questions/1546981/…–
托马斯(Thomas)

添加final到该算法并使用oldChars[newLen++]++newLen是一个错误-整个字符串将减少1!)不会产生可测量的性能提升(即,我得到±2..3%的差异,可与不同运行的差异进行比较)
GreyCat

@grey我使用其他一些优化功能制作了另一个版本
棘手怪胎

2
嗯!那是一个绝妙的主意!在生产环境中99.9%的字符串实际上并不需要剥离-char[]如果没有剥离,我可以进一步改进它以消除甚至第一个分配并按原样返回String。
GreyCat 2011年

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

对于我的测试字符串,3477148.18ops/s2616120.89ops/s旧版本相比。我敢肯定,击败它的唯一方法可能是用C编写(可能不是),或者到目前为止还没有人想到过的完全不同的方法。尽管我绝对不确定在不同平台上的计时是否稳定-至少在我的机器(Java7,Win7 x64)上产生可靠的结果。


感谢您提供解决方案,请查看问题更新-我已经发布了我的测试框架,并为17种算法添加了3个测试运行结果。您的算法始终处于最重要的位置,但是它会更改Java String的内部状态,从而破坏了“不可变的String”协定=>在实际应用中很难使用它。测试方面,是的,这是最好的结果,但是我想我会宣布它是一个单独的提名:)
GreyCat 2011年

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

1
虽然我尝试在有时间的情况下尝试JNI版本-到目前为止从未使用过它,所以会很有趣。但是我可以肯定它会变慢,因为有更多的调用开销(字符串太小了),而且JIT应该不费劲地优化函数。只是不要使用new String()以防万一您的字符串没有更改,但是我想您已经知道了。
Voo

我已经尝试过在纯C语言中做完全相同的事情-而且,与基于反射的版本相比,它并没有显示出太多的改进。C版本的运行速度快了+ 5..10%,并不是那么好-我认为它至少会像1.5x-1.7x ...
GreyCat 2011年

2

您可以根据处理器的数量将任务分为几个并行的子任务。


是的,我也想到了,但是在我的情况下它不会产生任何性能提升-这种剥离算法将在已经非常大规模的并行系统中调用。
GreyCat 2011年

2
而且,此外,我可能会猜测,为每50-100个字节的字符串分派几个线程进行处理将是一个巨大的过大杀伤力。
GreyCat 2011年

是的,为每个小字符串分叉线程不是一个好主意。但是负载均衡器可以提高性能。顺便说一句,您是否使用StringBuilder而不是StringBuffer来测试性能,而StringBuffer由于同步而缺乏性能。
2011年

我的生产设置运行时会产生几个单独的进程,并尽可能利用尽可能多的并行CPU和内核,因此我可以StringBuilder在任何地方自由使用而根本没有任何问题。
GreyCat 2011年

2

我是如此自由,为不同的算法编写了一个小型基准测试。这并不完美,但是我在给定的字符串上最少进行了1000次给定算法的10000次运行(默认情况下约有32/200%的不可打印内容)。这应该处理诸如GC,初始化之类的工作-没有太多的开销,因此任何算法都不应在没有太大阻碍的情况下至少运行一次。

没有特别有据可查的,但是很好。在这里-我同时介绍了棘轮怪胎的算法和基本版本。目前,我随机地初始化了一个200个字符长的字符串,其均匀分布的字符范围为[0,200)。


+1的努力-但您应该已经问过我-我已经有一个类似的基准测试套件-那是我测试算法的地方;)
GreyCat 2011年

@GreyCat好吧,我可以,但是将它们放在一起(无论如何,不​​使用现有代码)可能会更快;)
Voo

1

IANA低级Java性能迷,但您是否尝试展开主循环?看来它可以允许某些CPU并行执行检查。

此外,还有一些有趣的优化想法。


我怀疑是否可以在此处完成任何展开操作,因为(a)依赖于先前步骤中算法的后续步骤,(b)我什至没有听说有人在Java中进行手动循环展开以产生任何出色的结果;JIT通常会很好地展开与任务相匹配的内容。不过,感谢您的建议和链接:)
GreyCat 2011年

0

为什么直接使用“ utf-8”字符集名称会比使用预分配的静态const Charset.forName(“ utf-8”)直接产生更好的性能?

如果您是这样的话String#getBytes("utf-8"):不应更快-除了更好的缓存-由于Charset.forName("utf-8")内部使用了字符集(如果未缓存字符集),因此应该更快。

一件事可能是您使用的是不同的字符集(或者您的某些代码透明地使用了),但是缓存的字符集StringCoding没有改变。

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.