Конкатенация строк в Java — быстрее?

15 января 2010 г.

За последнее время появилось несколько постов связанных с измерениями скорости работы со строками в Java. Хотелось поделиться полученным несколько месяцев назад методом «ускорения» приложения, а именно, ускорением конкатенации строк. Это обычно одна из наиболее используемых строковых операций в общего рода приложениях и ее улучшение часто повышает производительность в целом.

В статье описывается несколько методов ускорения работы конкатенации и в конце будут выбраны лучшие по времени работы исходники.

Как можно соединить одну или несколько строк?

Способ #1: плюс

String result = a + b;

Самый очевидный для многих начинающих программистов, особенно перешедших в Java из других ЯП, где строки тоже можно «плюсовать».
На самом деле за этой конструкцией скрывается достаточно емкая операция. Тут создается новый StringBuilder и в него добавляются все полученные на вход строки. И не забываем про .toString(), на него тоже идет время.
Кстати, эта скрытая возможность может смущать начинающих кодеров. Компилируем код javac версии 1.5+ и запускаем на JVM без StringBuilder-а, например, BlackBerry, J2ME или других урезанных системах и получаем ошибку ClassNotFoundException, хотя явно нигде этот класс не используется.

Способ #2: String.concat(String)

String result = a.concat(b);

Использование «родных» методов String-а даст ощутимый прирост в производительности — они писались с расчетом на скорость и часто дают хорошие результаты. Впрочем, почему тогда не заменять одинарный “+” на .concat хотя бы на этапе компиляции?

Способ #3: StringBuffer

StringBuffer sb = new StringBuffer();
sb.append(a);
sb.append(b);
String result = sb.toString;

Хороший способ для конкатенации многих строк, допустим, в цикле. Впрочем, эту конструкцию можно еще оптимизировать:

String result = new StringBuffer(a).append(b).toString();

и за счет того, что в конструктор первая строка уже будет передаваться, этот способ будет работать немного быстрее.

Способ #4: StringBuilder

Использование аналогично предыдущему пункту, вместо StringBuffer — StringBuilder. Будет работать быстрее из-за не-синхронизированных методов последнего. Правда, проверка на java6 показала, что время почти не отличается, хотя на 5-й — почти на треть. Оптимизировали значит.

Способ #5: Re-using variables

Часто в советах оптимизации проскакивает — не плодите сущностей (с), то-есть используйте уже созданные экземпляры классов. Можем применить это для нашей конкатенации:

private / static private StringBuilder staticSb = new StringBuilder();
...
staticSb.delete(0, staticSb.length());
String result = staticSb.append(a).append(b).toString;

Такой метод очищения буфера, кстати, быстрее чем, например, StringBuilder.setLength(0);

Результаты

Тестирование проводилось на jre1.6.0_21 не так быстро как хотелось бы. Так же соединялись 3 и больше подстрок. Лучшие результаты выделены цветом.
Цифры в наносекундах за одно исполнение, исполнений каждого блока было по 100000. Выполнялось 5 измерений, лучшее и худшее отбрасывалось.

Аргументы виртуальной машины: -Xmx1024M -Xms1024M для уменьшения влияния GarbageCollector-а на время.

2345
a+b+c...1109153417492751
a.concat(b)…39576812951885
StringBuffer1263164325042740
StringBuilder1042148223712397
Pre-allocated StringBuilder552617785967

Исходный код программы-мерялки:

package com.stringtest;

public class StringTimeTest {

@SuppressWarnings("unused")
public static void main(String[] args) {
String a = "first looooooooooooong striiiiiiiiiing test";
String b = "second loooooooooooong striiiiiiiiiing test123";
String c = "third looooooooooooong striiiiiiiiiing test123456";
String d = "fourth looooooooooooong striiiiiiiiiing test12345678";
String e = "fifth looooooooooooong striiiiiiiiiing test12345678901";
N=100000;
t = System.nanoTime();
System.out.println(" ===== 2 concatenation ==== ");
for (int i=0;i<N;i++) {
String res = a + b;
}
t();
for (int i=0;i<N;i++) {
String res = a.concat(b);
}
t();
for (int i=0;i<N;i++) {
StringBuffer sb = new StringBuffer();
String res = sb.append(a).append(b).toString();
}
t();
for (int i=0;i<N;i++) {
StringBuilder sb = new StringBuilder();
String res = sb.append(a).append(b).toString();
}
t();
for (int i=0;i<N;i++) {
StringBuilder sb = new StringBuilder(200);
String res = sb.append(a).append(b).toString();
}
t();
StringBuilder sbGlob = new StringBuilder();
for (int i=0;i<N;i++) {
sbGlob.delete(0, sbGlob.length());
String res = sbGlob.append(a).append(b).toString();
}
t();
// 3
System.out.println(" ===== 3 concatenation ==== ");
for (int i=0;i<N;i++) {
String res = a + b + c;
}
t();
for (int i=0;i<N;i++) {
String res = a.concat(b).concat(c);
}
t();
for (int i=0;i<N;i++) {
StringBuffer sb = new StringBuffer();
String res = sb.append(a).append(b).append(c).toString();
}
t();
for (int i=0;i<N;i++) {
StringBuilder sb = new StringBuilder();
String res = sb.append(a).append(b).append(c).toString();
}
t();
for (int i=0;i<N;i++) {
StringBuilder sb = new StringBuilder(200);
String res = sb.append(a).append(b).append(c).toString();
}
t();
//StringBuilder sbGlob = new StringBuilder();
for (int i=0;i<N;i++) {
sbGlob.delete(0, sbGlob.length());
String res = sbGlob.append(a).append(b).append(c).toString();
}
t();

// 4
System.out.println(" ===== 4 concatenation ==== ");
for (int i=0;i<N;i++) {
String res = a + b + c + d;
}
t();
for (int i=0;i<N;i++) {
String res = a.concat(b).concat(c).concat(d);
}
t();
for (int i=0;i<N;i++) {
StringBuffer sb = new StringBuffer();
String res = sb.append(a).append(b).append(c).append(d).toString();
}
t();
for (int i=0;i<N;i++) {
StringBuilder sb = new StringBuilder();
String res = sb.append(a).append(b).append(c).append(d).toString();
}
t();
for (int i=0;i<N;i++) {
StringBuilder sb = new StringBuilder(200);
String res = sb.append(a).append(b).append(c).append(d).toString();
}
t();
//StringBuilder sbGlob = new StringBuilder();
for (int i=0;i<N;i++) {
sbGlob.delete(0, sbGlob.length());
String res = sbGlob.append(a).append(b).append(c).append(d).toString();
}
t();
// 5
System.out.println(" ===== 5 concatenation ==== ");
for (int i=0;i<N;i++) {
String res = a + b + c + d + e;
}
t();
for (int i=0;i<N;i++) {
String res = a.concat(b).concat(c).concat(d).concat(e);
}
t();
for (int i=0;i<N;i++) {
StringBuffer sb = new StringBuffer();
String res = sb.append(a).append(b).append(c).append(d).append(e).toString();
}
t();
for (int i=0;i<N;i++) {
StringBuilder sb = new StringBuilder();
String res = sb.append(a).append(b).append(c).append(d).append(e).toString();
}
t();
for (int i=0;i<N;i++) {
StringBuilder sb = new StringBuilder(200);
String res = sb.append(a).append(b).append(c).append(d).append(e).toString();
}
t();
//StringBuilder sbGlob = new StringBuilder();
for (int i=0;i<N;i++) {
sbGlob.delete(0, sbGlob.length());
String res = sbGlob.append(a).append(b).append(c).append(d).append(e).toString();
}
t();

}
static long t;
static long N;
static void t() {
System.out.println((System.nanoTime()-t)/1.0/N);
t = System.nanoTime();
}
}

Выводы

  • Выбор метода зависит от количества строк
  • Для соединения двух строк лучше использовать родной String.concat()
  • Для часто вызываемого метода, в котором будут соединяться больше двух строк, лучше использовать последний метод с очищением буфера и повторного его использования. Для thread-safety можно делать это поле ThreadLocal, например. Или вообще не-статичным членом класса.
  • В ходе экспериментов так же выяснилось, что одиночные a+b(+c) лучше заменять на a.concat, при большем количестве — StringBuilder.

Впрочем, не забываем, что Premature optimization is the root of all evil

Теги:
рубрика Java