JavaでString
からすべての非印刷文字を削除する最速の方法は何ですか?
これまでに、138バイト、131文字の文字列を試し、測定しました。
replaceAll()
-最も遅いメソッドreplaceAll()
を使用しますcodepointAt()
を1つずつ使用してコードポイントを取得し、StringBuffer に追加します。charAt()
を1つずつ使用して文字を取得し、StringBuffer に追加します。char[]
_バッファを事前に割り当て、charAt()
を使用して文字を1つずつ取得し、このバッファを埋めてから、String に変換します。char[]
_バッファの事前割り当て-getChars()
を使用して既存の文字列のすべての文字を一度に取得し、古いバッファを1つずつ繰り返し、新しいバッファを埋めてから、新しいバッファを文字列に変換します-自分の最速バージョンbyte[]
_、getBytes()
のみを使用し、「utf-8」としてエンコードを指定しますbyte[]
_バッファーと同じものですが、定数としてエンコードを指定しますCharset.forName("utf-8")
byte[]
_バッファを使用するものと同じですが、1バイトのローカルエンコーディングとしてエンコードを指定します(やることはほとんどありません)私の最善の試みは次のとおりでした:
_ 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")
を使用するよりもパフォーマンスが向上する理由
提案されたすべてのソリューションとそのクロスミューテーションを最大限に収集し、それを githubの小さなベンチマークフレームワーク として公開しました。現在、17のアルゴリズムを備えています。それらの1つは「特殊」です-Voo1アルゴリズム( SO user Voo によって提供される)複雑な反射トリックを使用します。優れた速度を達成しますが、JVM文字列の状態を台無しにします。したがって、個別にベンチマークされます。
あなたはそれをチェックアウトし、あなたの箱の結果を決定するためにそれを実行することを歓迎します。これが私が得た結果の要約です。それは仕様です:
Sun-Java6-jdk-6.24-1
_からインストールされたJava、JVMはとして自身を識別します異なるアルゴリズムは、異なる入力データのセットに対して、最終的に異なる結果を示します。 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
チャート形式:
(ソース: greycat.r )
ソース文字列プロバイダーは、(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
チャート形式:
(ソース: greycat.r )
前と同じですが、文字列の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
チャート形式:
(ソース: greycat.r )
誰がベストアンサーを提供したかを決めるのは非常に難しいですが、実際のアプリケーションのベストソリューションがエド・スタウブによって与えられ/インスパイアされたことを考えると、彼のアンサーをマークするのは公平だと思います。これに参加したすべての人に感謝します、あなたの入力は非常に有用で、非常に貴重でした。ボックスでテストスイートを実行し、さらに優れたソリューション(実用的なJNIソリューションをお持ちですか?)をご提案ください。
スレッド間で共有されていないクラスにこのメソッドを埋め込むのが妥当な場合は、バッファーを再利用できます。
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文字の配列を使用すると、少し良くなる可能性があります
_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);
_
さて、私の測定によると、現在の最良の方法(事前に割り当てられたアレイを使用したフリークのソリューション)を約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)で信頼できる結果が得られます。
プロセッサの量に応じて、タスクをいくつかの並列サブタスクに分割できます。
私はとても自由で、さまざまなアルゴリズムの小さなベンチマークを作成しました。完璧ではありませんが、ランダムな文字列に対して、指定されたアルゴリズムを10000回以上実行します(デフォルトでは約32/200%の非印刷物)。 GC、初期化などのようなものを処理する必要があります-オーバーヘッドがあまりないので、アルゴリズムが邪魔されずに少なくとも1回実行されるべきではありません。
特に十分に文書化されていませんが、まあまあです。 ここに行きます -ラチェットフリークのアルゴリズムと基本バージョンの両方を含めました。現時点では、[0、200]の範囲に均等に分布したcharを使用して、200文字の長い文字列をランダムに初期化します。
「utf-8」文字セット名を直接使用すると、事前に割り当てられた静的const Charset.forName( "utf-8")を使用するよりもパフォーマンスが向上するのはなぜですか。
String#getBytes("utf-8")
などを意味する場合:これは、文字セットがキャッシュされていない場合はCharset.forName("utf-8")
が内部で使用されるため、より高速なキャッシングを除いて高速であってはなりません。
1つは、異なる文字セットを使用している(またはコードの一部が透過的に実行している)場合もありますが、StringCoding
にキャッシュされている文字セットは変更されません。