Javaで2つの文字列を連結する最速の方法は何ですか?
すなわち
String ccyPair = ccy1 + ccy2;
cyPair
のキーとしてHashMap
を使用しており、値を取得するために非常にタイトなループで呼び出されています。
プロファイルすると、これがボトルネックになります
Java.lang.StringBuilder.append(StringBuilder.Java:119)
Java.lang.StringBuilder.(StringBuilder.Java:93)
これらのルーチンがベンチマークに表示される理由は、コンパイラが内部で「+」を実装する方法だからです。
連結文字列が本当に必要な場合は、コンパイラに「+」を使用して魔法をかける必要があります。マップルックアップのキーがすべて必要な場合は、適切なequals
およびhashMap
実装の両方の文字列を保持するキークラスは、コピー手順を回避するので良い考えかもしれません。
たくさんの理論-練習の時間!
private final String s1 = new String("1234567890");
private final String s2 = new String("1234567890");
Intel Mac OSのウォームアップされた64ビットHotspot 1.6.0_22で、10,000,000のforループを使用します。
例えば
@Test public void testConcatenation() {
for (int i = 0; i < COUNT; i++) {
String s3 = s1 + s2;
}
}
ループ内に次のステートメントがある場合
String s3 = s1 + s2;
1.33秒
String s3 = new StringBuilder(s1).append(s2).toString();
1.28秒
String s3 = new StringBuffer(s1).append(s2).toString();
1.92秒
String s3 = s1.concat(s2);
0.70秒
String s3 = "1234567890" + "1234567890";
0.0秒
そのため、静的文字列がない限り、concatが明確な勝者になります。静的文字列を使用する場合は、コンパイラがすでに面倒を見てくれます。
答えは既に決まっていると思いますが、コードを共有するために投稿します。
短い答えは、純粋な連結があなたが探しているすべてである場合、です:String.concat(...)
出力:
ITERATION_LIMIT1: 1
ITERATION_LIMIT2: 10000000
s1: STRING1-1111111111111111111111
s2: STRING2-2222222222222222222222
iteration: 1
null: 1.7 nanos
s1.concat(s2): 106.1 nanos
s1 + s2: 251.7 nanos
new StringBuilder(s1).append(s2).toString(): 246.6 nanos
new StringBuffer(s1).append(s2).toString(): 404.7 nanos
String.format("%s%s", s1, s2): 3276.0 nanos
Tests complete
サンプルコード:
package net.fosdal.scratch;
public class StringConcatenationPerformance {
private static final int ITERATION_LIMIT1 = 1;
private static final int ITERATION_LIMIT2 = 10000000;
public static void main(String[] args) {
String s1 = "STRING1-1111111111111111111111";
String s2 = "STRING2-2222222222222222222222";
String methodName;
long startNanos, durationNanos;
int iteration2;
System.out.println("ITERATION_LIMIT1: " + ITERATION_LIMIT1);
System.out.println("ITERATION_LIMIT2: " + ITERATION_LIMIT2);
System.out.println("s1: " + s1);
System.out.println("s2: " + s2);
int iteration1 = 0;
while (iteration1++ < ITERATION_LIMIT1) {
System.out.println();
System.out.println("iteration: " + iteration1);
// method #0
methodName = "null";
iteration2 = 0;
startNanos = System.nanoTime();
while (iteration2++ < ITERATION_LIMIT2) {
method0(s1, s2);
}
durationNanos = System.nanoTime() - startNanos;
System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));
// method #1
methodName = "s1.concat(s2)";
iteration2 = 0;
startNanos = System.nanoTime();
while (iteration2++ < ITERATION_LIMIT2) {
method1(s1, s2);
}
durationNanos = System.nanoTime() - startNanos;
System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));
// method #2
iteration2 = 0;
startNanos = System.nanoTime();
methodName = "s1 + s2";
while (iteration2++ < ITERATION_LIMIT2) {
method2(s1, s2);
}
durationNanos = System.nanoTime() - startNanos;
System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));
// method #3
iteration2 = 0;
startNanos = System.nanoTime();
methodName = "new StringBuilder(s1).append(s2).toString()";
while (iteration2++ < ITERATION_LIMIT2) {
method3(s1, s2);
}
durationNanos = System.nanoTime() - startNanos;
System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));
// method #4
iteration2 = 0;
startNanos = System.nanoTime();
methodName = "new StringBuffer(s1).append(s2).toString()";
while (iteration2++ < ITERATION_LIMIT2) {
method4(s1, s2);
}
durationNanos = System.nanoTime() - startNanos;
System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));
// method #5
iteration2 = 0;
startNanos = System.nanoTime();
methodName = "String.format(\"%s%s\", s1, s2)";
while (iteration2++ < ITERATION_LIMIT2) {
method5(s1, s2);
}
durationNanos = System.nanoTime() - startNanos;
System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));
}
System.out.println();
System.out.println("Tests complete");
}
public static String method0(String s1, String s2) {
return "";
}
public static String method1(String s1, String s2) {
return s1.concat(s2);
}
public static String method2(String s1, String s2) {
return s1 + s2;
}
public static String method3(String s1, String s2) {
return new StringBuilder(s1).append(s2).toString();
}
public static String method4(String s1, String s2) {
return new StringBuffer(s1).append(s2).toString();
}
public static String method5(String s1, String s2) {
return String.format("%s%s", s1, s2);
}
}
コンパイル時( "my string"など)ではなく、実行時に生成されたString(UUID.randomUUID()。toString()など)でテストする必要があります。私の結果は
plus: 118 ns
concat: 52 ns
builder1: 102 ns
builder2: 66 ns
buffer1: 119 ns
buffer2: 87 ns
この実装では:
private static long COUNT = 10000000;
public static void main(String[] args) throws Exception {
String s1 = UUID.randomUUID().toString();
String s2 = UUID.randomUUID().toString();
for(String methodName : new String[] {
"none", "plus", "concat", "builder1", "builder2", "buffer1", "buffer2"
}) {
Method method = ConcatPerformanceTest.class.getMethod(methodName, String.class, String.class);
long time = System.nanoTime();
for(int i = 0; i < COUNT; i++) {
method.invoke((Object) null, s1, s2);
}
System.out.println(methodName + ": " + (System.nanoTime() - time)/COUNT + " ns");
}
}
public static String none(String s1, String s2) {
return null;
}
public static String plus(String s1, String s2) {
return s1 + s2;
}
public static String concat(String s1, String s2) {
return s1.concat(s2);
}
public static String builder1(String s1, String s2) {
return new StringBuilder(s1).append(s2).toString();
}
public static String builder2(String s1, String s2) {
return new StringBuilder(s1.length() + s2.length()).append(s1).append(s2).toString();
}
public static String buffer1(String s1, String s2) {
return new StringBuffer(s1).append(s2).toString();
}
public static String buffer2(String s1, String s2) {
return new StringBuffer(s1.length() + s2.length()).append(s1).append(s2).toString();
}
タイトルの質問:String.concat
は、通常、2つのString
sを連結する最も速い方法です(ただし、null
sに注意してください)。 [特大]中間バッファまたは他のオブジェクトは含まれません。奇妙な+
は、StringBuilder
を含む比較的非効率的なコードにコンパイルされます。
しかし、あなたの身体は他の問題を指し示しています。マップのキーを生成する文字列連結は、一般的な「反イディオム」です。これはハッキングであり、エラーが発生しやすいものです。生成されたキーは一意であると確信していますか?まだ未知の要件のためにコードが維持された後、それは一意のままですか?最良のアプローチは、キーの不変値クラスを作成することです。 List
と汎用のTupleクラスを使用するのはずさんなハックです。
私にとって、以下のconcat3メソッドは、WindowsおよびリモートLinuxマシンでベンチマークを実行した後の最速の方法です。
public class StringConcat {
public static void main(String[] args) {
int run = 100 * 100 * 1000;
long startTime, total = 0;
final String a = "a";
final String b = "assdfsaf";
final String c = "aasfasfsaf";
final String d = "afafafdaa";
final String e = "afdassadf";
startTime = System.currentTimeMillis();
concat1(run, a, b, c, d, e);
total = System.currentTimeMillis() - startTime;
System.out.println(total);
startTime = System.currentTimeMillis();
concat2(run, a, b, c, d, e);
total = System.currentTimeMillis() - startTime;
System.out.println(total);
startTime = System.currentTimeMillis();
concat3(run, a, b, c, d, e);
total = System.currentTimeMillis() - startTime;
System.out.println(total);
}
private static void concat3(int run, String a, String b, String c, String d, String e) {
for (int i = 0; i < run; i++) {
String str = new StringBuilder(a.length() + b.length() + c.length() + d.length() + e.length()).append(a)
.append(b).append(c).append(d).append(e).toString();
}
}
private static void concat2(int run, String a, String b, String c, String d, String e) {
for (int i = 0; i < run; i++) {
String str = new StringBuilder(a).append(b).append(c).append(d).append(e).toString();
}
}
private static void concat1(int run, String a, String b, String c, String d, String e) {
for (int i = 0; i < run; i++) {
String str = a + b + c + d + e;
}
}
}
おそらく連結の代わりに、Pairクラスを作成する必要がありますか?
_public class Pair<T1, T2> {
private T1 first;
private T2 second;
public static <U1,U2> Pair<U1,U2> create(U1 first, U2 second) {
return new Pair<U1,U2>(U1,U2);
}
public Pair( ) {}
public Pair( T1 first, T2 second ) {
this.first = first;
this.second = second;
}
public T1 getFirst( ) {
return first;
}
public void setFirst( T1 first ) {
this.first = first;
}
public T2 getSecond( ) {
return second;
}
public void setSecond( T2 second ) {
this.second = second;
}
@Override
public String toString( ) {
return "Pair [first=" + first + ", second=" + second + "]";
}
@Override
public int hashCode( ) {
final int prime = 31;
int result = 1;
result = prime * result + ((first == null)?0:first.hashCode());
result = prime * result + ((second == null)?0:second.hashCode());
return result;
}
@Override
public boolean equals( Object obj ) {
if ( this == obj )
return true;
if ( obj == null )
return false;
if ( getClass() != obj.getClass() )
return false;
Pair<?, ?> other = (Pair<?, ?>) obj;
if ( first == null ) {
if ( other.first != null )
return false;
}
else if ( !first.equals(other.first) )
return false;
if ( second == null ) {
if ( other.second != null )
return false;
}
else if ( !second.equals(other.second) )
return false;
return true;
}
}
_
これをHashMapのキーとして使用します
_HashMap<String,Whatever>
_の代わりに_HashMap<Pair<String,String>,Whatever>
_を使用します
map.get( str1 + str2 )
の代わりにタイトループでmap.get( Pair.create(str1,str2) )
を使用します。
ThorbjørnRavn Andersensの提案を試すことをお勧めします。
2つの部分の長さに応じて、連結された文字列が必要な場合、再割り当てを回避するために必要なサイズのStringBuilderインスタンスを作成すると、パフォーマンスがわずかに向上する場合があります。デフォルトのStringBuilderコンストラクターは、少なくとも私のマシンでは、現在の実装で16文字を予約しています。したがって、連結された文字列が初期バッファサイズよりも長い場合、StringBuilderは再割り当てする必要があります。
これを試して、プロファイラーがそれについて言っていることを教えてください:
StringBuilder ccyPair = new StringBuilder(ccy1.length()+ccy2.length());
ccyPair.append(ccy1);
ccyPair.append(ccy2);
Java仕様 ( およびJavaの最初のバージョン )によると、「文字列連結演算子+」セクションでは次のように記述されています。
繰り返し文字列の連結のパフォーマンスを向上させるために、JavaコンパイラはStringBufferクラスまたは同様の手法を使用して、式の評価によって作成される中間Stringオブジェクトの数を減らすことができます
基本的に、+ operator
またはStringBuilder.append
変数の場合は基本的に同じです。
他にも、あなたの質問で2つの文字列のみを追加すると述べましたが、3つ以上の文字列を追加すると異なる結果につながることを念頭に置いています:
少し変更した@Duncan McGregorの例を使用しました。 concatを使用して2〜6個の文字列を連結する5つのメソッドと、StringBuilderを使用して2〜6個の文字列を連結する5つのメソッドがあります。
// Initialization
private final String s1 = new String("1234567890");
private final String s2 = new String("1234567890");
private final String s3 = new String("1234567890");
private final String s4 = new String("1234567890");
private final String s5 = new String("1234567890");
private final String s6 = new String("1234567890");
// testing the concat
public void testConcatenation2stringsConcat(int count) {
for (int i = 0; i < count; i++) {
String s100 = s1.concat(s2);
}
}
public void testConcatenation3stringsConcat(int count) {
for (int i = 0; i < count; i++) {
String s100 = s1.concat(s2).concat(s3);
}
}
public void testConcatenation4stringsConcat(int count) {
for (int i = 0; i < count; i++) {
String s100 = s1.concat(s2).concat(s3).concat(s4);
}
}
public void testConcatenation5stringsConcat(int count) {
for (int i = 0; i < count; i++) {
String s100 = s1.concat(s2).concat(s3).concat(s4).concat(s5);
}
}
public void testConcatenation6stringsConcat(int count) {
for (int i = 0; i < count; i++) {
String s100 = s1.concat(s2).concat(s3).concat(s4).concat(s5).concat(s6);
}
}
//testing the StringBuilder
public void testConcatenation2stringsSB(int count) {
for (int i = 0; i < count; i++) {
String s100 = new StringBuilder(s1).append(s2).toString();
}
}
public void testConcatenation3stringsSB(int count) {
for (int i = 0; i < count; i++) {
String s100 = new StringBuilder(s1).append(s2).append(s3).toString();
}
}
public void testConcatenation4stringsSB(int count) {
for (int i = 0; i < count; i++) {
String s100 = new StringBuilder(s1).append(s2).append(s3).append(s4).toString();
}
}
public void testConcatenation5stringsSB(int count) {
for (int i = 0; i < count; i++) {
String s100 = new StringBuilder(s1).append(s2).append(s3).append(s4).append(s5).toString();
}
}
public void testConcatenation6stringsSB(int count) {
for (int i = 0; i < count; i++) {
String s100 = new StringBuilder(s1).append(s2).append(s3).append(s4).append(s5).append(s6).toString();
}
}
私はこれらの結果を(秒単位で)得ました:
testConcatenation2stringsConcat:0.018 ||||||||||||||||| testConcatenation2stringsSB:0.2testConcatenation3stringsConcat:0.35 |||||||| |||||||||||| testConcatenation3stringsSB:0.25testConcatenation4stringsConcat:0.5 |||||||| ||||||||||||||| testConcatenation4stringsSB:0.3testConcatenation5stringsConcat:0.67 |||||||| |||||||||||| testConcatenation5stringsSB:0.38testConcatenation5stringsConcat:0.9 |||||||| ||||||||||||||| testConcatenation5stringsSB:0.43
これは、ダブルキー、単一値を備えた線形プローブマップの完全な実装です。 Java.util.HashMapよりも優れたパフォーマンスを発揮するはずです。
警告、1日の非常に早い時間にゼロから書かれているため、バグが含まれている可能性があります。自由に編集してください。
ソリューションは、どのラッパーよりも優れており、いつでも連結できます。 get/putの割り当てがないため、汎用マップも迅速になります。
これで問題が解決することを願っています。 (コードは不要ないくつかの簡単なテストで提供されます)
package bestsss.util;
@SuppressWarnings("unchecked")
public class DoubleKeyMap<K1, K2, V> {
private static final int MAX_CAPACITY = 1<<29;
private static final Object TOMBSTONE = new String("TOMBSTONE");
Object[] kvs;
int[] hashes;
int count = 0;
final int rehashOnProbes;
public DoubleKeyMap(){
this(8, 5);
}
public DoubleKeyMap(int capacity, int rehashOnProbes){
capacity = nextCapacity(Math.max(2, capacity-1));
if (rehashOnProbes>capacity){
throw new IllegalArgumentException("rehashOnProbes too high");
}
hashes = new int[capacity];
kvs = new Object[kvsIndex(capacity)];
count = 0;
this.rehashOnProbes = rehashOnProbes;
}
private static int nextCapacity(int c) {
int n = Integer.highestOneBit(c)<<1;
if (n<0 || n>MAX_CAPACITY){
throw new Error("map too large");
}
return n;
}
//alternatively this method can become non-static, protected and overriden, the perfoamnce can drop a little
//but if better spread of the lowest bit is possible, all good and proper
private static<K1, K2> int hash(K1 key1, K2 key2){
//spread more, if need be
int h1 = key1.hashCode();
int h2 = key2.hashCode();
return h1+ (h2<<4) + h2; //h1+h2*17
}
private static int kvsIndex(int baseIdx){
int idx = baseIdx;
idx+=idx<<1;//idx*3
return idx;
}
private int baseIdx(int hash){
return hash & (hashes.length-1);
}
public V get(K1 key1, K2 key2){
final int hash = hash(key1, key2);
final int[] hashes = this.hashes;
final Object[] kvs = this.kvs;
final int mask = hashes.length-1;
for(int base = baseIdx(hash);;base=(base+1)&mask){
int k = kvsIndex(base);
K1 k1 = (K1) kvs[k];
if (k1==null)
return null;//null met; no such value
Object value;
if (hashes[base]!=hash || TOMBSTONE==(value=kvs[k+2]))
continue;//next
K2 k2 = (K2) kvs[k+1];
if ( (key1==k1 || key1.equals(k1)) && (key2==k2 || key2.equals(k2)) ){
return (V) value;
}
}
}
public boolean contains(K1 key1, K2 key2){
return get(key1, key2)!=null;
}
public boolean containsValue(final V value){
final Object[] kvs = this.kvs;
if (value==null)
return false;
for(int i=0;i<kvs.length;i+=3){
Object v = kvs[2];
if (v==null || v==TOMBSTONE)
continue;
if (value==v || value.equals(v))
return true;
}
return false;
}
public V put(K1 key1, K2 key2, V value){
int hash = hash(key1, key2);
return doPut(key1, key2, value, hash);
}
public V remove(K1 key1, K2 key2){
int hash = hash(key1, key2);
return doPut(key1, key2, null, hash);
}
//note, instead of remove a TOMBSTONE is used to mark the deletion
//this may leak keys but deletion doesn't need to shift the array like in Knuth 6.4
protected V doPut(final K1 key1, final K2 key2, Object value, final int hash){
//null value -> remove
int probes = 0;
final int[] hashes = this.hashes;
final Object[] kvs = this.kvs;
final int mask = hashes.length-1;
//conservative resize: when too many probes and the count is greater than the half of the capacity
for(int base = baseIdx(hash);probes<rehashOnProbes || count<(mask>>1);base=(base+1)&mask, probes++){
final int k = kvsIndex(base);
K1 k1 = (K1) kvs[k];
K2 k2;
//find a gap, or resize
Object old = kvs[k+2];
final boolean emptySlot = k1==null || (value!=null && old==TOMBSTONE);
if (emptySlot || (
hashes[base] == hash &&
(k1==key1 || k1.equals(key1)) &&
((k2=(K2) kvs[k+1])==key2 || k2.equals(key2)))
){
if (value==null){//remove()
if (emptySlot)
return null;//not found, and no value ->nothing to do
value = TOMBSTONE;
count-=2;//offset the ++later
}
if (emptySlot){//new entry, update keys
hashes[base] = hash;
kvs[k] = key1;
kvs[k+1] = key2;
}//else -> keys and hash are equal
if (old==TOMBSTONE)
old=null;
kvs[k+2] = value;
count++;
return (V) old;
}
}
resize();
return doPut(key1, key2, value, hash);//hack w/ recursion, after the resize
}
//optimized version during resize, doesn't check equals which is the slowest part
protected void doPutForResize(K1 key1, K2 key2, V value, final int hash){
final int[] hashes = this.hashes;
final Object[] kvs = this.kvs;
final int mask = hashes.length-1;
//find the 1st gap and insert there
for(int base = baseIdx(hash);;base=(base+1)&mask){//it's ensured, no equal keys exist, so skip equals part
final int k = kvsIndex(base);
K1 k1 = (K1) kvs[k];
if (k1!=null)
continue;
hashes[base] = hash;
kvs[k] = key1;
kvs[k+1] = key2;
kvs[k+2] = value;
return;
}
}
//resizes the map by doubling the capacity,
//the method uses altervative varian of put that doesn't check equality, or probes; just inserts at a gap
protected void resize(){
final int[] hashes = this.hashes;
final Object[] kvs = this.kvs;
final int capacity = nextCapacity(hashes.length);
this.hashes = new int[capacity];
this.kvs = new Object[kvsIndex(capacity)];
for (int i=0;i<hashes.length; i++){
int k = kvsIndex(i);
K1 key1 = (K1) kvs[k];
Object value = kvs[k+2];
if (key1!=null && TOMBSTONE!=value){
K2 key2 = (K2) kvs[k+1];
doPutForResize(key1, key2, (V) value, hashes[i]);
}
}
}
public static void main(String[] args) {
DoubleKeyMap<String, String, Integer> map = new DoubleKeyMap<String, String, Integer>(4,2);
map.put("eur/usd", "usd/jpy", 1);
map.put("eur/usd", "usd/jpy", 2);
map.put("eur/jpy", "usd/jpy", 3);
System.out.println(map.get("eur/jpy", "usd/jpy"));
System.out.println(map.get("eur/usd", "usd/jpy"));
System.out.println("======");
map.remove("eur/usd", "usd/jpy");
System.out.println(map.get("eur/jpy", "usd/jpy"));
System.out.println(map.get("eur/usd", "usd/jpy"));
System.out.println("======");
testResize();
}
static void testResize(){
DoubleKeyMap<String, Integer, Integer> map = new DoubleKeyMap<String, Integer, Integer>(18, 17);
long s = 0;
String pref="xxx";
for (int i=0;i<14000;i++){
map.put(pref+i, i, i);
if ((i&1)==1)
map.remove(pref+i, i);
else
s+=i;
}
System.out.println("sum: "+s);
long sum = 0;
for (int i=0;i<14000;i++){
Integer n = map.get(pref+i, i);
if (n!=null && n!=i){
throw new AssertionError();
}
if (n!=null){
System.out.println(n);
sum+=n;
}
}
System.out.println("1st sum: "+s);
System.out.println("2nd sum: "+sum);
}
}
数百万の文字列を連結している場合、string.concatはおそらく数百万の新しい文字列オブジェクト参照を生成することに注意してください。これにより、CPU使用率が増加します。
StringBuffer ccyPair = new StringBuffer();
ccyPair.append("ccy1").append("ccy2");
文字列バッファーを使用してみてから、プロファイラーを使用してボトルネックがどこにあるかを調べましたか?.
おそらく、2つの文字列のハッシュを個別に計算し、それらを組み合わせて、おそらく整数で機能する別のハッシュ関数で問題を回避できますか?
何かのようなもの:
int h1 = ccy1.hashCode(), h2 = ccy2.hashCode(), h = h1 ^ h2;
連結のハッシュを計算するためだけに文字列を連結することは無駄に思えるので、それはかなり速くなる可能性があります。
上記は2つのハッシュをバイナリXOR(^
演算子)これはよく機能しますが、さらに調査することをお勧めします。
さて、あなたの質問は何ですか?実行することはありません。文字列を連結する必要がある場合は、実行してください。コードのプロファイルを作成しても問題ありません。これで、文字列連結演算子+がStringBuilderのappend()メソッドを自動的に使用するため、
StringBuilder ccyPair = new StringBuilder(ccy1)
ccyPair.append(ccy2);
深刻な利点はありません。
コードを最適化する唯一の真剣な方法は、おそらく連結をまったく省略するように設計を変更することです。ただし、本当に必要な場合にのみ実行してください。つまり、連結はCPU時間のかなりの部分を占めます。