Java文字列からすべての印刷不可能な文字を取り除く最速の方法


81

StringJavaで印刷できない文字をすべて削除する最速の方法は何ですか?

これまでのところ、138バイト、131文字の文字列で試して測定しました。

  • 文字列replaceAll()-最も遅いメソッド
    • 517009結果/秒
  • パターンをプリコンパイルしてから、Matcherを使用します replaceAll()
    • 637836結果/秒
  • StringBufferを使用し、codepointAt()1つずつ使用してコードポイントを取得し、StringBufferに追加します
    • 711946結果/秒
  • StringBufferを使用し、charAt()1つずつ使用して文字を取得し、StringBufferに追加します
    • 1052964結果/秒
  • char[]バッファを事前に割り当て、charAt()1つずつ使用して文字を取得し、このバッファを埋めてから、文字列に変換し直します
    • 2022653結果/秒
  • char[]古いバッファと新しいバッファの2つのバッファを事前に割り当て、を使用して既存の文字列のすべての文字を一度に取得しgetChars()、古いバッファを1つずつ繰り返し、新しいバッファを埋めてから、新しいバッファを文字列に変換します-私自身の最速バージョン
    • 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」文字セット名を使用すると、事前に割り当てられたstaticconstを使用するよりもパフォーマンスが直接向上するのはなぜCharset.forName("utf-8")ですか。

更新

  • ラチェットフリークからの提案は、印象的な3105590の結果/秒のパフォーマンス、+ 24%の改善をもたらします!
  • Ed Staubからの提案により、さらに別の改善がもたらされます-3471017の結果/秒、以前のベストよりも+ 12%。

アップデート2

私は、提案されたすべてのソリューションとその相互変異を収集するために最善を尽くし、それをgithubで小さなベンチマークフレームワークとして公開しました。現在、17のアルゴリズムを備えています。それらの1つは「特別」です-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│アルゴリズム
──────────┼──────────────────────────────
6535947│Voo1
──────────┼──────────────────────────────
5350454│RatchetFreak2EdStaub1GreyCat1
5249343│EdStaub1
50002501│EdStaub1GreyCat1
4859086│ArrayOfCharFromStringCharAt
4295532│RatchetFreak1
4045307│ArrayOfCharFromArrayOfChar
2790178│RatchetFreak2EdStaub1GreyCat2
2583311│RatchetFreak2
1274859│StringBuilderChar
1138174│StringBuilderCodePoint
  994727│ArrayOfByteUTF8String
  918611│ArrayOfByteUTF8Const
  756086│MatcherReplace
  598945│StringReplaceAll
  460045│ArrayOfByteWindows1251

グラフ形式:( 出典:greycat.ru同じ単一の文字列チャート

複数の文字列、文字列の100%に制御文字が含まれています

ソース文字列プロバイダーは、(0..127)文字セットを使用して大量のランダム文字列を事前に生成しました。したがって、ほとんどすべての文字列に少なくとも1つの制御文字が含まれていました。アルゴリズムは、この事前生成された配列からラウンドロビン方式で文字列を受け取りました。

 Ops /s│アルゴリズム
──────────┼──────────────────────────────
2123142│Voo1
──────────┼──────────────────────────────
1782214│EdStaub1
1776199│EdStaub1GreyCat1
1694628│ArrayOfCharFromStringCharAt
1481481│ArrayOfCharFromArrayOfChar
1460067│RatchetFreak2EdStaub1GreyCat1
1438435│RatchetFreak2EdStaub1GreyCat2
1366494│RatchetFreak2
1349710│RatchetFreak1
  893176│ArrayOfByteUTF8String
  817127│ArrayOfByteUTF8Const
  778089│StringBuilderChar
  734754│StringBuilderCodePoint
  377829│ArrayOfByteWindows1251
  224140│MatcherReplace
  211104│StringReplaceAll

グラフ形式:( 出典:greycat.ru複数の弦、100%濃度

複数の文字列、文字列の1%に制御文字が含まれています

以前と同じですが、文字列の1%のみが制御文字で生成されました。他の99%は、[32..127]文字セットを使用して生成されたため、制御文字をまったく含めることができませんでした。この合成負荷は、私の場所でこのアルゴリズムの実際のアプリケーションに最も近いものです。

 Ops /s│アルゴリズム
──────────┼──────────────────────────────
3711952│Voo1
──────────┼──────────────────────────────
2851440│EdStaub1GreyCat1
2455796│EdStaub1
2426007│ArrayOfCharFromStringCharAt
2347969│RatchetFreak2EdStaub1GreyCat2
2242152│RatchetFreak1
2171553│ArrayOfCharFromArrayOfChar
1922707│RatchetFreak2EdStaub1GreyCat1
1857010│RatchetFreak2
1023751│ArrayOfByteUTF8String
  939055│StringBuilderChar
  907194│ArrayOfByteUTF8Const
  841963│StringBuilderCodePoint
  606465│MatcherReplace
  501555│StringReplaceAll
  381185│ArrayOfByteWindows1251

グラフ形式:( 出典:greycat.ru複数のストリング、1%の濃度

誰が最良の答えを提供したかを決めるのは非常に難しいですが、実際のアプリケーションを考えると、Ed Staubによって最良の解決策が与えられ、触発されたので、彼の答えをマークするのは公平だと思います。これに参加してくれたすべての人に感謝します。あなたの意見は非常に役に立ち、非常に貴重でした。ボックスでテストスイートを自由に実行して、さらに優れたソリューションを提案してください(実用的なJNIソリューション、誰か?)。

参考文献


21
「この質問は研究努力を示しています」-うーん...ええ、合格。+1
Gustav Barkefors 2011

7
StringBuilderStringBuffer同期されていない場合よりもわずかに高速になります。これにタグを付けたので、これについて言及しますmicro-optimization

2
@Jarrod Roberson:わかりました。では、すべての読み取り専用フィールドをfinals.length()にして、forループからも抽出しましょう:-)
ホーム

3
スペースの下の一部の文字は印刷可能です(例:\tおよび)\n。127を超える多くの文字は、文字セットで印刷できません。
Peter Lawrey 2011

1
容量の文字列バッファを初期化しましたs.length()か?
ラチェットフリーク

回答:


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%程度です。

これが潜在的に大きな文字列で使用され、メモリ「リーク」が懸念される場合は、弱参照を使用できます。


いい案!これまでのところ、カウントは1秒あたり最大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 2011

newLen++;:プリインクリメントを使用するのはどう++newLen;ですか?-(++jループ内でも)。こちらをご覧ください:stackoverflow.com/questions/1546981/…–
Thomas

finalこのアルゴリズムに追加して使用するとoldChars[newLen++]++newLenエラーです-文字列全体が1ずれます!)、測定可能なパフォーマンスの向上は得られません(つまり、異なる実行の違いに匹敵する±2..3%の違いが得られます)
GreyCat

@grey私は他のいくつかの最適化で別のバージョンを作成しました
ラチェットフリーク

2
うーん!それは素晴らしいアイデアです!私の本番環境の文字列の99.9%は、実際にはストリッピングを必要としません-char[]ストリッピングが発生しなかった場合は、最初の割り当てを排除し、文字列をそのまま返すようにさらに改善できます。
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;
    }

取得私たTestStringための3477148.18ops/s2616120.89ops/s古いバリアントのために。それを打ち負かす唯一の方法は、Cで書くこと(おそらくそうではないかもしれません)か、これまで誰も考えていなかったまったく異なるアプローチであると確信しています。タイミングが異なるプラットフォーム間で安定しているかどうかは絶対にわかりませんが、少なくとも私のボックス(Java7、Win7 x64)で信頼できる結果が得られます。


ソリューションをありがとう、質問の更新をチェックしてください-私はテストフレームワークを公開し、17のアルゴリズムに対して3つのテスト実行結果を追加しました。アルゴリズムは常に最上位にありますが、Java Stringの内部状態が変更されるため、「不変のString」コントラクトが破られます=>実際のアプリケーションで使用するのはかなり困難です。テストに関しては、ええ、それは最良の結果ですが、私はそれを別の指名として発表すると思います:)
GreyCat 2011

3
@GreyCatええ、確かにいくつかの大きな文字列が添付されています。正直なところ、現在の最良のソリューションをさらに改善する目立った方法はないと確信しているので、私はほとんどそれを書いただけです。それがうまくいくと確信している状況があります(それを取り除く前に部分文字列やインターンの呼び出しはありません)が、それは1つの現在のホットスポットバージョンに関する知識のためです(つまり、IOから読み取られた文字列をインターンしません-そうしません」特に便利です)。これらの追加のx%が本当に必要な場合は便利かもしれませんが、それ以外の場合は、さらに改善できるかどうかを確認するためのベースラインが増えます;)
Voo 2011

1
時間があればJNIバージョンを試してみましたが、今のところ使用したことがないので面白いと思います。しかし、追加の呼び出しオーバーヘッド(文字列が小さすぎる)とJITが関数を最適化するのにそれほど苦労するべきではないという事実のために、それはかなり遅くなると確信しています。new String()文字列が変更されていない場合は使用しないでください。ただし、すでに取得されていると思います。
Voo 2011

私はすでに純粋なCでまったく同じことをしようとしました-そして、まあ、それはあなたのリフレクションベースのバージョンに比べて実際にはあまり改善を示していません。私は...それは少なくとも1.5倍、1.7倍のようなものだろうと思った- + 5..10%が速く、本当に素晴らしいではないということのようなCのバージョンは何かを実行します
GreyCat

2

プロセッサの数に応じて、タスクをいくつかの並列サブタスクに分割できます。


ええ、私も考えましたが、私の状況ではパフォーマンスが向上しません。このストリッピングアルゴリズムは、すでに超並列システムで呼び出されます。
GreyCat 2011

2
さらに、50〜100バイトの文字列ごとに処理するためにいくつかのスレッドをフォークするのは非常にやり過ぎだと思うかもしれません。
GreyCat 2011

はい、すべての小さな文字列にスレッドをフォークすることはお勧めできません。ただし、ロードバランサーはパフォーマンスを向上させる可能性があります。ところで、同期されたためにパフォーマンスが不足しているStringBufferの代わりにStringBuilderを使用してパフォーマンスをテストしましたか?
umbr 2011

私の本番セットアップの実行では、いくつかの個別のプロセスが生成され、可能な限り多くの並列CPUとコアが使用されるため、StringBuilderどこでも問題なく自由に使用できます。
GreyCat 2011

2

私はとても自由で、さまざまなアルゴリズムの小さなベンチマークを作成しました。完璧ではありませんが、ランダムな文字列に対して、特定のアルゴリズムを最低1000回実行します(デフォルトでは約32/200%の印刷不可)。これで、GC、初期化などの処理が必要になります。オーバーヘッドがそれほど多くないため、どのアルゴリズムでも、障害なく少なくとも1回実行する必要はありません。

特に十分に文書化されていませんが、まあ。ここで私達は行く-私は、ラチェットフリークのアルゴリズムと基本的なバージョンの両方が含まれていました。現時点では、200文字の長さの文字列を[0、200)の範囲で均一に分散された文字でランダムに初期化します。


努力のために+
1-

@GreyCatええと、私はできましたが、それを(とにかく既存のコードから)一緒に投げる方がおそらく速かったです;)
Voo 2011

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")文字セットがキャッシュされていない場合、これは内部で使用されるため、より良いキャッシュを除いて、より速くなるべきではありません。

1つは、異なる文字セットを使用している(または、コードの一部が透過的に使用している)が、キャッシュされStringCodingている文字セットが変更されていないことです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.