Pull to refresh

Comments 78

«Если нам нужны изменяемые строки, разработчики нам предлагают другие классы. Первый, который был в Java изначально — StringBuffer и более новый StringBuilder (появился начиная с версии 1.5). Как пишут в документации, StringBuilder — безопасно применять в многопоточных приложениях, но второй более эффективен.»
сломал мозг

относительно win vs linux — таблицы не соответствуют тексту
Там именно про класс String — в этой части вполне соответствует.
JRockit изначально разрабатывала Appeal Virtual Machines, которую потом купила Bea, которую купила Oracle.
для полноты картины не хватает сравнения -client/-server, разных Xmx и, возможно, экзотики типа gcj/harmony
+ разных CompileThreshold. А возможно тест стоит проводить дважды для одгого инстанса JVM, чтобы увидеть работу JIT в HotSpot.
Допустим не дважды, а проводить тест существенные промежутки времени тк оптимизация идёт не один раз, а помере накопления статистики.

Сам недавно был удивлён существенному повышению производительности на тесте более 2х часов на простейшем приложении.

Так ~первые сотни запросов шли со скоростью ~100 запросов в сек.
Через 5-10 минут это было 1к-3к запросов в сек.
Через 2-3 часа 2к-5к запросов в сек.
Через 8 часов 8к-12к запросов в сек.

Суть приложения Tomcat + jsp + класс активно работающий со строками, StringBuilder и char[]
У джавы есть внутреннее строковое хранилище, в котором хранятся все строковые константы, объявленные в коде программы, плюс туда же можно поместить любую другую строку, введённую во время работы. Видимо, через восемь часов все часто встречающиеся строки уже попали в это хранилище, и память перестала выделяться.
Как разработчик JVM, уверяю: то, о чем Вы сейчас сказали, к вопросу не относится, и вообще это работает не так. Можно сколь угодно долго работать со строками, и ни в какое «хранилище» они попадать не будут, пока не будет вызван метод intern(). А выделение памяти в JVM — очень дешевая операция, даже быстрее, чем malloc() в C++.

Увеличение производительности со временем — результат именно сбора статистики и рекомпиляции методов.
Правильно — объявленные. В предложенном же случае конструкция buff += word + " "; создаст два новых объекта, а старый объект, находящийся в buff, будет удален (за исключением первой итерации, когда в buff лежит объект "", хранящийся в хранилище строк).
Правильно — объявленные. В предложенном же случае конструкция buff += word + " "; создаст два новых объекта, а старый объект, находящийся в buff, будет удален (за исключением первой итерации, когда в buff лежит объект "", хранящийся в хранилище строк).
Можно было и с разными сборщиками мусора поиграться. Думаю именно со String была бы заметна некоторая разница.
Как пишут в документации, StringBuilder — безопасно применять в многопоточных приложениях, но второй более эффективен.

Описка, я так понимаю. StringBuffer — для многопоточности, StringBuilder — для скорости.
Кстати, конкатенация строк плюсом в цикле — дурной тон, у нас например за это п*здят ногами и лишают премии.
BTW, при конкатенации строк происходит создание объекта StringBuilder, выполнение append-ов всех строк и потом вызов .toString().
Мне кажется, это экстремизм. Если понятно, что квадратичному количеству действий браться не от куда, можно и сконкатенировать по-тупому.
Как раз есть откуда. См. Effective Java.
Все верно: str += word работает в точности как
StringBuilder tmp = new StringBuilder();
tmp.append(str);
tmp.append(word);
str = tmp.toString();
а каждый append() — это вызов System.arraycopy(), вдобавок toString() — еще один arraycopy() на всю длину буфера.
Соответственно, если строка str длиной 100000 символов, а word длиной 5 символов, то str += word должен будет выполнить копирование как минимум 200010 символов, в то время как builder.append(word) скопирует, как правило, только 5 символов. Почувствуйте разницу :) Хороший эксперимент, наглядный результат.
А разве в циклах оно не оптимизируется? Типа, создается буфер один раз, а не стотыщ?
Не. Это ж javac так компилирует, а он вообще мало что оптимизирует.
Не думаю, что для сложения ДВУХ строк создается StringBuilder.
Уверен, что используется String.concat(String), потому что он заранее создает массив из char нужного объема, копирует туда символы, а затем создает строку, не копируя более массив.

StringBuilder же (даже если компилятор сразу создаст его с буфером нужного размера, а не по умолчанию 16 чаров), в методе toString() копирует свой буфер в новую строку (до 1.5 StringBuffer — не копировал до первого изменения, но видимо посчитали, что синхронизация обходится дороже).

А для конкатенкации больше трех и более строк — согласен, испльзуется StringBuilder.
Только что проверил, для 2ух строк тоже используется StringBuilder.

Исходный код
public class Test {

 /**
  * @param args
  */
 public static void main(String[] args) {
  String s1 = "s1";
  String s2 = "s2";
  String s3 = "s3";
  
  System.out.println("-----------------");
  String res1 = s1 + s2;
  System.out.println(res1);
  
  System.out.println("-----------------");
  String res2 = s1 + s2 + s3;
  System.out.println(res2);
  
  System.out.println("-----------------");
  String res3 = s1;
  res3 += s2;
  System.out.println(res3);

  System.out.println("-----------------");
  String res4 = s1;
  res4 += s2 + s3;
  System.out.println(res4);
 }

}


* This source code was highlighted with Source Code Highlighter.


Декомпилированный код
public class Test
{

  public Test()
  {
  }

  public static void main(String args[])
  {
    String s1 = "s1";
    String s2 = "s2";
    String s3 = "s3";
    System.out.println("-----------------");
    String res1 = (new StringBuilder(String.valueOf(s1))).append(s2).toString();
    System.out.println(res1);
    System.out.println("-----------------");
    String res2 = (new StringBuilder(String.valueOf(s1))).append(s2).append(s3).toString();
    System.out.println(res2);
    System.out.println("-----------------");
    String res3 = s1;
    res3 = (new StringBuilder(String.valueOf(res3))).append(s2).toString();
    System.out.println(res3);
    System.out.println("-----------------");
    String res4 = s1;
    res4 = (new StringBuilder(String.valueOf(res4))).append(s2).append(s3).toString();
    System.out.println(res4);
  }
}


* This source code was highlighted with Source Code Highlighter.
Спасибо, буду знать.
Хотя мне не понятно, почему сделано так.
UFO just landed and posted this here
Нельзя говорить о производительности языка. Можно говорить о производительности реализации языка. То есть нужно делать внятный акцент на то, какую именно реализацию (имплементацию) Java вы тестировали.

P.S. я не Java-программист. Если Java == J2EE и это больше чем язык, то это всё же не меняет сути сказанного выше.
уточню: содержание статьи ОК, комментарий относился лишь к названию.
Это относится к реализации строк в Java. J2EE это дополнительный набор технологий, но это та же виртуальная машина с той же реализацией строк.
и ещё: в HotSpot вроде важно сколько раз вызывалась функция. Если мало, то эта функция не проходит JIT-компиляцию. А значит в тесте нужно сначала сделать «разогрев».

далее, «в отклонения скрывается дьявол» (с) — то есть, тест для каждой пары (виртуальная машина * тестируемый класс) надо бы запустить несколько раз, скажем 20, чтобы убедиться что отклонение от указанной вами в таблице величины замера не умаляет ваших утверждений. Общепринято указывать отклонения в результатах.

Ах, да, спасибо за статью надо коллеге дать глянуть, он в Java в отличие от меня разбирается :)
Да ладно вам скромничать, всем бы так в Java не разбираться :)
Возможно, StringBuilder и StringBuffer используют модифицированные алгоритмы соединения большого числа строк, например, как здесь: habrahabr.ru/blogs/algorithm/99373/
можно еще ускорить, задав начальную длину StringBuffer buff = StringBuffer(100000);

а вообще, основные потери производительности идут в момент копирования. И так как String увеличивает свою длину каждый раз на столько насколько это необходимо, то StringBuffer это делает только при недостатке место (сразу на 2*старый размер +2).

вот здесь поподробней описаны все механизмы — www.precisejava.com/javaperf/j2se/StringAndStringBuffer.htm
Да, хорошая статья, я читал подобную. Просто хотелось показать, что даже с дефолтным конструктором StringBuilder/StringBuffer работают значительно быстрее. Это как с BufferedInputStream/BufferedReader, есть конструктор с помощью которого можно задать размер этого самого буфера. В ряде случаев мы можем подобрать буфер так, чтоб и памяти много не израсходовать и чтение ускорить. Например, когда мы точно знаем размер читаемых объектов (и все объекты в файле одинаковой длины) и создаем буфер, кратный этому размеру.
Просто хотелось показать, что даже с дефолтным конструктором StringBuilder/StringBuffer работают значительно быстрее.
Имхо, это очевидно должно быть даже для джуниоров ) Ну, или хотя бы для тех, кто хотя бы образно представляет, как работаю строки в Java и что из себя представляет конкатенация строк. Выше показали, что работает через StringBuilder. Кстати, именно поэтому всё описанное относится только к конкатенации в цикле и совсем не имеет отношения к линейной конкатенации, типа String s = " bla " + bla() + " bla " + ..., на которой никакого смысла делать через StringBuilder.append нету, ибо оно так и делается, только выглядит короче и писать удобнее.
А что можно сказать в плане производительности о методе String.concat()? Он тоже реализован с использованием StringBuffer?
Нет. Создается новый массив символов достаточной длины и в него перегоняются символы из обеих строк. Потом из массива создается новый объект. Вот как он реализован:
    public String concat(String str) {
	int otherLen = str.length();
	if (otherLen == 0) {
	    return this;
	}
	char buf[] = new char[count + otherLen];
	getChars(0, count, buf, 0);
	str.getChars(0, otherLen, buf, count);
	return new String(0, count + otherLen, buf);
    }
Я сейчас прогнал тест, указанный в данном посте, с использованием String.concat(). Результаты на моем файле (12 тыс. слов) такие:
String += String: 30 сек.
StringBuffer.append(): 25 мсек.
String.concat(): 25 мсек.
Вот, а если посмотреть StringBuffer.append:
public AbstractStringBuilder append(String str) {
	if (str == null) str = "null";
        int len = str.length();
	if (len == 0) return this;
	int newCount = count + len;
	if (newCount > value.length)
	    expandCapacity(newCount);
	str.getChars(0, len, value, count);
	count = newCount;
	return this;
}

void expandCapacity(int minimumCapacity) {
	int newCapacity = (value.length + 1) * 2;
        if (newCapacity < 0) {
            newCapacity = Integer.MAX_VALUE;
        } else if (minimumCapacity > newCapacity) {
	    newCapacity = minimumCapacity;
	}
        value = Arrays.copyOf(value, newCapacity);
}

Т.е. практически тоже самое, если размера буфера недостаточно, но если достаточно, то добавляемая строка просто дописывается в конец буфера и выигрыш должен быть. Вы же вот так делаете?
buff = buff.concat(word).concat(" ");
Да, именно так и делаю
А где параметры запуска jvm? Почему jdk не последний? Почему бы не использовать G1 сборщик мусора?

p.s. честно говоря, не очень понял, что вы хотели показать этим тестом. :(
То, что я хотел показать, я написал во второй строчке поста. А использование последней версии jdk и сборщика мусора не приведет к ускорению конкатенации строк даже в 10 раз, не говоря уже о 1000.
Разница между 16 и 19 ms это не 10 и не 1000, так что могли бы и получить одинаковые скорости для этих типов.
А вдруг приведут? проверьте. Вы же проверяете вещи, которые и так всем известны из учебников для джуниоров.
Ну посмотрите исходники
public final class StringBuilder  extends AbstractStringBuilder
...
    public StringBuilder append(String str) {
	super.append(str);
        return this;
    }

и
public final class StringBuffer  extends AbstractStringBuilder
...
    public synchronized StringBuffer append(String str) {
	super.append(str);
        return this;
    }


Какие опции нужно задать JVM чтобы StringBuffer работал быстрее? Я хотел показать соотношение в скорости работы между String, StringBuffer и StringBuilder. Я не собирался сравнивать JVM между собой, потому, что такое сравнение должно быть комплексным, а не только по скорости работы со строками.
Впечатлила разница между HotSpot и JRockit на линуксе. (StringBuffer/Builder)
Вторая — в 2 раза быстрее.
Неужели ТАК заоптимайзили…
Ну так получается Оракл не зря хвастается тем, что у него самая быстрая Java-машина, ну точнее — хороший JIT-компилятор :) и GC с прогнозируемым поведением. Но каждой JVM свое место. Например, пользователю не нравится ждать при старте программы в три-четыре раза дольше, пока JIT JRockit-а произведет компиляцию. Я как то сравнивал, время запуска Netbeans-а под HotSpot и JRockit, под первой ~15s, под второй больше 40. Говорят, математика быстро работает в JRockit, сам не проверял…
Как то тоже показывал коллегам как быстр JRockit, а потом сделали java -server и оказалось, что обычная java работает точно так же быстро.
Или наоборот -client, это было полгода назад, забыл уже =( Но результаты стали практически идентичными.
Вы забыли написать о работе со строками как массивами char-ов. Это дало бы наибольшую производительность.
глупость. Тут самая дорогая операция — аллокация нового буфера и дублирование строки.

Вызов методов оптимизирует jit, тем более что все три класса — final
А вы попробуйте код напишите и измерте его время работы. В java далеко не все inline-ится.
А когда вы сами напишете расширяемый массив, там что, совсем не будет методов?
Реализация расширяемого массива напрямую вместо использования коллекции в критическом месте, довольно частая и эффективная оптимизация.
Это, конечно, всё замечательно, но только здесь вы предлагаете изобрести свои велосипед при налии уже двух, которые, по сути, и представляют из себя тот же расширяемый массив.
Ну а что делать, раз JVM не делает таких оптимизаций. Не инлайнится этот масив туда, и все. Поэтому, и приходится переписывать код ручками.
Вот вам пример кода:
public class Sandbox {
  static interface Array {
    void append(Object o);
  }

  static class ArrayListArray implements Array {
    List<Object> contents = new ArrayList<Object>();

    @Override
    public void append(Object o) {
      contents.add(o);
    }
  }

  static class SimpleArray implements Array {
    int size = 0;
    Object[] contents = new Object[16];

    @Override
    public void append(Object o) {
      if (contents.length - 1 == size) {
        Object[] newContents = new Object[contents.length * 2];
        System.arraycopy(contents, 0, newContents, 0, contents.length);
        contents = newContents;
      }

      contents[size++] = o;
    }
  }


  private static void measure(Runnable r) {
    long start = System.currentTimeMillis();
    r.run();
    System.out.println("current = " + (System.currentTimeMillis() - start));

  }

  private static void measureArray(final Array a) {
    measure(new Runnable() {
      @Override
      public void run() {
        for (int i = 0; i < 1000000; i++) {
          a.append("aaaa" + i);
        }
      }
    });
  }

  public static void main(String[] args) {    
    measureArray(new ArrayListArray());
    measureArray(new SimpleArray());
  }
}


На моей машине результаты такие: 836 мс, и 510 мс, реализация ручками дает выигрыш почти в два раза.
А при чем тут коллекция? Речь о том, что внутри буфера и билдера сидит всё тот же массив.
Кстати говоря, у меня(w7, quad) разница в вашем тесте не такая впечатляющая: 575/471.

А теперь поменяйте местами строчки
    measureArray(new SimpleArray());
    measureArray(new ArrayListArray());
и померите заново. Ну как? :-)

Забавно, что результат на самом деле зависит не от реализации, а от порядка строк.
Тем не менее, это вполне ожидаемо, если понимать, как работает HotSpot.
Вы только что написали бенчмарку, про которую в свое время Dr. Cliff Click рассказывал в своей презентации «How NOT to write a Microbenchmark».
Спасибо. Интересная статья. В теории я давно знал, что StringBuffer быстрее, но вот только сейчас я узнал насколько он быстрее :)
>Не совсем понятно, почему такая большая разница в работе с объектами класса String под Linux и WinXP.

Почти наверняка изза другого размера кучи по умолчанию. В тестах стоило бы задавать его явно и привести здесь в пример. Чем меньше объем кучи – тем чаще будет сборка мусора в варианте со String.
Ещё, наверное, может быть разница из-за copy-on-write?
хмм, при чем copy-on-write к объектам String которые неизменимы?
Хотя сейчас уточнил, к джаве линуксовый copy-on-write не имеет отношения, только к порождению процессов. К тому же не в ту сторону прочитал цифры линукс vs виндоус.
UFO just landed and posted this here
Ещё небольшая «фича» класса String:

String s = «fjkdsjfjkd… 10 кб текста… ksjdkfjdskjf»;
String s1 = s.substring(1,2);

И объект s1 занимает столько же памяти как и объект s. Понятно, что это сделано для скорости, но всё же =)
Нет. Результат s.substring(1, 2) использует тот же char[] что и в исходной строке, т.е. дополнительной памяти вообще не потребляет (ну кроме самой String, как враппера над char[]).
Чорт! Хотел написать совсем же другое :)

String s = «fjkdsjfjkd… 10 кб текста… ksjdkfjdskjf»;
s = s.substring(1,2);

Вот, получается, что s остаётся здоровенным, хотя использует совсем маленький кусочек.
Да, но у программиста остается возможность избавиться от ненужного хлама самому, написав
s = new String(s.substring(1, 2));
Совершенно верно, но до этого ещё нужно догадаться :) (Как самое простое — залезть в исходники String)
UFO just landed and posted this here
А! Как-то будучи еще недоджуниором проводили похожие тесты.
У нас при числе итераций >= 8 строки начинали сливать строкостроителям. С тех пор таким правилом и руководствуюсь (чего не скажешь по нашим товарищам по команде из Индии, ага. Жалко, что ну никак не настучать им линейкой по пальцам :( )
если вы через плюс соединяете кучу строк, то компилятор породит один стрингбилдер и будет через него сшивать все
String st = «Маша»;
st += «Саша»;

Создаст новый объект содержащий строку «МашаСаша» а исходные объекты будут уничтожены сборщиком мусора.


Данное высказывание неверно. Новый объект будет создан, но старые при этом не будут удалены, т.к. они находятся в хранилище строк.
Если операций конкатенации над одним и тем же строковым объектом производится много, это приводит к интенсивному процессу порождения новых объектов и добавляет работы сборщику мусора.


Опять же, неверное. Если применить конкатенацию к одному и тому же объекту, то будут созданы n объектов, при этом старый объект никуда не денется.

Если же имеется в виду конструкция вида:
for (String t : strList) {
    s += t;
}

, то конкатенация здесь будет применяться к разным объектам, которые будут храниться в по одной и той же ссылке s.
Если бы вы запускали тесты на HotSpot JVM для java 6 update 23 или выше, то результаты StringBuilder и StringBuffer скорее всего дали бы идентичные результаты, так как начиная с этой версии по умолчанию включен EscapeAnalysis, который увидел бы что StringBuffer исползуется только одним потоком и не синхронизировал бы код вообще, тем самым превратив его по сути в StringBuilder
Only those users with full accounts are able to leave comments. Log in, please.