web-dev-qa-db-ja.com

Java文字列からすべての非印刷文字を削除する最速の方法

JavaでStringからすべての非印刷文字を削除する最速の方法は何ですか?

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

  • 文字列のreplaceAll()-最も遅いメソッド
    • 517009結果/秒
  • パターンをプリコンパイルしてから、MatcherのreplaceAll() を使用します
    • 637836結果/秒
  • StringBufferを使用し、codepointAt()を1つずつ使用してコードポイントを取得し、StringBuffer に追加します。
    • 711946結果/秒
  • StringBufferを使用し、charAt()を1つずつ使用して文字を取得し、StringBuffer に追加します。
    • 1052964結果/秒
  • _char[]_バッファを事前に割り当て、charAt()を使用して文字を1つずつ取得し、このバッファを埋めてから、String に変換します。
    • 2022653結果/秒
  • 2つの_char[]_バッファの事前割り当て-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」文字セット名を使用すると、事前に割り当てられた静的const Charset.forName("utf-8")を使用するよりもパフォーマンスが向上する理由

更新

  • ratchet freakからの提案により、毎秒3105590という驚くべき結果が得られ、+ 24%の改善が見られます。
  • Ed Staubからの提案は、さらに別の改善をもたらします-3471017結果/秒、前のベストよりも+ 12%。

更新2

提案されたすべてのソリューションとそのクロスミューテーションを最大限に収集し、それを githubの小さなベンチマークフレームワーク として公開しました。現在、17のアルゴリズムを備えています。それらの1つは「特殊」です-Voo1アルゴリズム( SO user Voo によって提供される)複雑な反射トリックを使用します。優れた速度を達成しますが、JVM文字列の状態を台無しにします。したがって、個別にベンチマークされます。

あなたはそれをチェックアウトし、あなたの箱の結果を決定するためにそれを実行することを歓迎します。これが私が得た結果の要約です。それは仕様です:

  • Debian sid
  • Linux 2.6.39-2-AMD64(x86_64)
  • パッケージ_Sun-Java6-jdk-6.24-1_からインストールされたJava、JVMはとして自身を識別します
    • Java(TM)SEランタイム環境(ビルド1.6.0_24-b07)
    • Java HotSpot(TM)64-Bit Server VM(ビルド19.1-b02、混合モード)

異なるアルゴリズムは、異なる入力データのセットに対して、最終的に異なる結果を示します。 3つのモードでベンチマークを実行しました。

同じ単一の文字列

このモードは、StringSourceクラスによって定数として提供される同じ単一の文字列で機能します。ショーダウンは次のとおりです。

 Ops/s│アルゴリズム
──────────┼───────────────────────── ──────
 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_。 │StringBuilderChar 
 1 138 174│StringBuilderCodePoint 
 994 727│ArrayOfByteUTF8String 
 918 611│ArrayOfByteUTF8Const 
 756 086│MatcherReplace 
 598 945│StringReplaceAll 
 460 045│ArrayOfByteWindows1251 

チャート形式: Same single string chart
(ソース: greycat.r

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

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

 Ops/s│アルゴリズム
──────────┼───────────────────────── ──────
 2 123 142│Voo1 
──────────┼─────────────────── ────────────
 1 782 214│EdStaub1 
 1 776 199│EdStaub1GreyCat1 
 1 694 628│ArrayOfCharFromStringCharAt 
 1 481 481 │ArrayOfCharFromArrayOfChar [。 ArrayOfByteUTF8String 
 817 127│ArrayOfByteUTF8Const 
 778 089│StringBuilderChar 
 734 754│StringBuilderCodePoint 
 377 829│ArrayOfByteWindows1251 
 224 140│MatcherReplace 
 211 104│StringReplaceAll 

チャート形式: Multiple strings, 100% concentration
(ソース: greycat.r

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

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

 Ops/s│アルゴリズム
──────────┼───────────────────────── ──────
 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 │ArrayOfByteUTF8String 
 939 055│StringBuilderChar 
 907 194│ArrayOfByteUTF8Const 
 841 963│StringBuilderCodePoint 
 606 465│MatcherReplace 
 501 555│StringReplaceAll 
 381 185│ArrayOfByteWindows1251 

チャート形式: Multiple strings, 1% concentration
(ソース: greycat.r

誰がベストアンサーを提供したかを決めるのは非常に難しいですが、実際のアプリケーションのベストソリューションがエド・スタウブによって与えられ/インスパイアされたことを考えると、彼のアンサーをマークするのは公平だと思います。これに参加したすべての人に感謝します、あなたの入力は非常に有用で、非常に貴重でした。ボックスでテストスイートを実行し、さらに優れたソリューション(実用的なJNIソリューションをお持ちですか?)をご提案ください。

参照資料

78
GreyCat

スレッド間で共有されていないクラスにこのメソッドを埋め込むのが妥当な場合は、バッファーを再利用できます。

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

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

10
Ed Staub

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);
_
25
ratchet freak

さて、私の測定によると、現在の最良の方法(事前に割り当てられたアレイを使用したフリークのソリューション)を約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/s vs. 2616120.89ops/sを取得するテスト文字列の場合。 Cで書く(おそらくそうではない)か、これまで誰も考えたことのない完全に異なるアプローチをすることが唯一の方法であると確信しています。タイミングが異なるプラットフォーム間で安定しているかどうかは絶対にわかりませんが、少なくとも私のボックス(Java7、Win7 x64)で信頼できる結果が得られます。

10
Voo

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

2
umbr

私はとても自由で、さまざまなアルゴリズムの小さなベンチマークを作成しました。完璧ではありませんが、ランダムな文字列に対して、指定されたアルゴリズムを10000回以上実行します(デフォルトでは約32/200%の非印刷物)。 GC、初期化などのようなものを処理する必要があります-オーバーヘッドがあまりないので、アルゴリズムが邪魔されずに少なくとも1回実行されるべきではありません。

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

2
Voo

IANAの低レベルJavaパフォーマンスジャンキー、しかし、あなたは メインループの展開 ?を試してみましたか?.

また、 this には最適化のための楽しいアイデアがあります。

1
Ryan Ransford

「utf-8」文字セット名を直接使用すると、事前に割り当てられた静的const Charset.forName( "utf-8")を使用するよりもパフォーマンスが向上するのはなぜですか。

String#getBytes("utf-8")などを意味する場合:これは、文字セットがキャッシュされていない場合はCharset.forName("utf-8")が内部で使用されるため、より高速なキャッシングを除いて高速であってはなりません。

1つは、異なる文字セットを使用している(またはコードの一部が透過的に実行している)場合もありますが、StringCodingにキャッシュされている文字セットは変更されません。

0
Thomas