Перейти к содержимому

Почему не рекомендуются множественные конкатенации string

  • автор:

Конкатенация строк и производительность

Советы от 5 февраля 2002 «Запись методов toString » (источник и перевод на JavaGu.ru) включали следующее предложение:

Обратите внимание, что использование «+» в toString для построения возвращаемого значения не всегда является самым эффективным подходом. Возможно, вы захотите использовать вместо этого StringBuffer .

Читатель технических советов отметил, что в документации по Java говорится о том, что для фактической реализации оператора + применяется StringBuffer . Поэтому возникает вопрос, какой выигрыш в производительности, если он существует, вы получите при явном использовании StringBuffer в ваших программах? В этой статье делается попытка ответить на этот вопрос.

Для начала рассмотрим пример, в котором строка формируется путем повторения одного и того же символа:

class MyTimer <
  private final long start;

  public long getElapsed () <
    return System.currentTimeMillis () — start;
  >
>

public class AppDemo1 <
  static final int N = 47500 ;

  public static void main ( String args []) <

    // создать строку при помощи оператора +

    // создать строку при помощи StringBuffer

После выполнения этой программы вы должны получить примерно следующий результат:

Подход №2 явно использует StringBuffer , тогда как подход №1 использует его неявно, как часть реализации оператора + . Вы можете исследовать байт-коды, использующиеся для реализации первого подхода при помощи команды:

Разница в «+» и StringBuffer

Откуда такая огромная разница между этими двумя подходами? Во втором подходе символы добавляются в StringBuffer , что довольно эффективно. А в первом подходе не используется этот метод? На самом деле нет. Выражение:

не добавляет символы к строке str1 . Это происходит из-за того, что Java-строки постоянны, они не изменяются после создания. Вот что происходит в действительности:

str1 копируется в него

«*» добавляется в буфер

Результат преобразуется в строку

Ссылка str1 меняется для указания на эту строку

Старая строка, на которую ранее ссылалась переменная str1 , делается доступной для сборщика мусора.

Цикл проходит через N итераций, и на каждой итерации содержимое str1 (содержащей N-1 символов) должно быть скопировано в буфер. Такое поведение подразумевает, что первый подход имеет квадратичную или худшую производительность. «Квадратичная» означает, что время выполнения пропорционально квадрату N. Есть вероятность эффективно заморозить приложение при применении такого типа цикла.

В примере AppDemo1 демонстрируется ситуация, когда периодически присоединяется одна строка к другой, так что обе строки должны быть скопированы во временную область ( StringBuffer ), создана новая строка и, затем, ссылка на оригинальную строку заменяется ссылкой на новую строку.

Но что если вы не выполняете этот тип операции, а вместо этого, просто имеете некоторый код, похожий на следующий:

Здесь нет цикла или повторений и нет строки, которая становится все длиннее и длиннее. Есть какой-либо вред от применения + вместо StringBuffer в этом примере?

Поясняющий пример

В примере AppDemo1 демонстрируется ситуация, когда периодически присоединяется одна строка к другой, так что обе строки должны быть скопированы во временную область ( StringBuffer ), создана новая строка и, затем, ссылка на оригинальную строку заменяется ссылкой на новую строку.

Но что если вы не выполняете этот тип операции, а вместо этого, просто имеете некоторый код, похожий на следующий:

Здесь нет цикла или повторений и нет строки, которая становится все длиннее и длиннее. Есть какой-либо вред от применения + вместо StringBuffer в этом примере?

Для ответа на этот вопрос рассмотрим дополнительный код:

class MyPoint <
  private final int x, y;
  private final String cache;

class MyTimer <
  private final long start;

  public long getElapsed () <
    return System.currentTimeMillis () — start;
  >
>

public class AppDemo2 <
  static final int N = 1000000 ;

  public static void main ( String args []) <
    MyPoint mp = new MyPoint ( 37 , 47 ) ;
    String s1 = null ;
    String s2 = null ;
    String s3 = null ;
    String s4 = null ;

    // проверка исправности для того, чтобы убедиться,
    // что результаты, возвращенные из каждого метода toString идентичны

В этой программе создается класс MyPoint , который используется для представления точек X,Y . В ней реализуются различные методы toString для класса. Результат выполнения программы может выглядеть примерно так:

Расшифровка результатов

Первые два способа, использующие + и StringBuffer , имеют примерно одинаковую производительность. Поэтому вы можете сделать вывод, что эти два способа фактически идентичны. Генерируемый для toString1 байт-код указывает, что создается StringBuffer , а затем различные строки просто добавляются к нему. Полученный код очень похож на toString2 .

Но не все так просто. Первая проблема в том, что вы не всегда сможете сформулировать возвращаемое из toString значение в виде отдельного выражения. toString3 и toString1 показывают идентичные результаты. Но время работы toString3 в два раза больше по причине, описанной в примере AppDemo1 . Пример AppDemo2 демонстрирует ситуацию, когда надо создать возвращаемое значение за один раз. В этом случае toString2 , использующий явно StringBuffer , является более хорошим выбором.

Другая проблема касается высказывания, найденного в «Спецификации по языку программирования Java» в разделе 15.18.1.2, в котором говорится:

Реализация может выполнить преобразование и конкатенацию за один шаг, чтобы избежать создания и удаления промежуточного объекта String . Для увеличения производительности повторных конкатенаций строки компилятор Java может использовать класс StringBuffer или аналогичную технику для уменьшения количества промежуточных объектов String , создающихся при вычислении выражения.

Это утверждение говорит о том, что компилятор Java не обязательно оптимизирует такое выражение как:

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

Поэтому будьте осторожны при использовании оператора + , особенно для длинных строк или в циклах.

Отметим, что существует даже более быстрый способ реализации toString для этого примера. MyPoint является постоянным классом. Это означает, что его экземпляры не могут быть модифицированы после создания. Учитывая это, возвращаемое из toString значение всегда будет одним и тем же. Поскольку значения одинаковы, оно может быть вычислено один раз в конструкторе MyPoint и затем просто возвращено из toString4 .

Такой вид кэширования часто очень полезен, но есть и отрицательные стороны. Если класс является изменяемым, то кэширование может не иметь смысла. Тоже самое можно сказать и для ситуаций, когда вычисление значения кэша трудоемко, когда кэш занимает много памяти, или когда метод toString вызывается нечасто.

String concatenation in a for loop. Java 9

Please correct me if i’m wrong. In Java 8, for performance reasons, when concatenating several strings by the «+» operator StringBuffer was invoked. And the problem of creating a bunch of intermediate string objects and polluting the string pool was «resolved».

What about Java 9? There’a new feature added as Invokedynamic. And a new class that resolves the problem even better, StringConcatFactory.

My question are: How many objects are created in this loop? Are there any intermedier objects? And how can i verify that?

Mad Physicist's user avatar

D2k's user avatar

3 Answers 3

For the record, here is a JMH test.

Produces result (only for 100000 shown here) that I did not really expect:

Eugene's user avatar

My question is: How many objects are created in this loop? Are there any intermediate objects? How can I verify that?

Spoiler:

JVM doesn’t try to omit intermediate objects in the loop — so they will be created when using plain concatenation.

Let’s take a look at the bytecode first. I used performance tests kindly provided by @Eugene, compiled them for java8 and then for java9. Here are those 2 methods we gonna compare:

My java versions are the following:

The JMH version is 1.20

Here is the output I get from javap -c LoopTest.class :

Method concatBuilder() that utilises StringBuilder explicitly looks exactly the same for java8 and java9:

Note that the invocation of StringBuilder.append happens inside the loop, while StringBuilder.toString is called outside of it. This is important — it means that there will be no intermediate objects created. In java8 bytecode it’s a bit different:

Method concatPlain() in Java8:

You can see that in java8 both StringBuilder.append and StringBuilder.toString are called inside the loop statement which means that it doesn’t even try to omit creation of intermediate objects! It can be described in the code below:

This explains performance difference between concatPlain() and concatBuilder() (which is few thousand times(!)). The same issue happening with java9 — it doesn’t try to avoid intermediate objects inside a loop, but it does a slightly better job inside a loop than java8 does (performance results are added):

Method concatPlain() Java9:

Here are performance results:

For java 9 there are different strategies defined with -Djava.lang.invoke.stringConcat . I tried all of them:

Default (MH_INLINE_SIZED_EXACT):

-Djava.lang.invoke.stringConcat=BC_SB

-Djava.lang.invoke.stringConcat=BC_SB_SIZED

-Djava.lang.invoke.stringConcat=BC_SB_SIZED_EXACT

-Djava.lang.invoke.stringConcat=BC_SB_SIZED_EXACT

-Djava.lang.invoke.stringConcat=MH_SB_SIZED_EXACT

-Djava.lang.invoke.stringConcat=MH_INLINE_SIZED_EXACT (yes, it’s the default one but I decided to set it explicitly for clarity of experiment)

I decided to investigate memory usage but didn’t find anything interesting except that java9 consumes more memory. Attached screenshots in case anybody would be interested. Of course, they were made after the actual performance measurements, but not during them.

Java8 concatBuilder(): Java8 concatBuilder() Java8 concatPlain(): enter image description here Java9 concatBuilder(): enter image description here Java9 concatPlain(): enter image description here

So yeah, answering your question I can say that neither java8 nor java9 can avoid creating intermediate objects inside a loop.

UPDATE:

As pointed out by @Eugene naked bytecode migt be meaningless since JIT does a lot of optimizations in runtime which looks logical to me, so I decided to add the output of optimized by JIT code (captured by -XX:CompileCommand=print,*LoopTest.concatPlain ).

As you can see StringBuilder::toString is invoked before the goto which means that everything is happening inside the loop. Similar situation with java9 — StringConcatHelper::newString is invoked before the goto command.

Конкатенация строк в Java — когда использовать +, StringBuilder и concat [duplicate]

Когда мы должны использовать + для конкатенации строк, когда предпочтительнее StringBuilder и когда подходит для использования concat.

Я слышал, что StringBuilder предпочтительнее для конкатенации внутри циклов. Почему это так?

9 ответов

Я обычно использую StringBuilder в кодах, где производительность является проблемой. Повторная конкатенация строк внутри цикла часто является хорошим кандидатом.

Причиной предпочтения StringBuilder является то, что оба + и concat создают новый объект каждый раз, когда вы их вызываете (если аргумент правой стороны не пуст). Это может быстро добавить к большому количеству объектов, почти все из которых совершенно не нужны.

Как указывали другие, когда вы используете + несколько раз в рамках одного и того же оператора, компилятор может часто оптимизировать это для вас. Однако, по моему опыту, этот аргумент не применяется, когда конкатенации происходят в отдельных утверждениях. Это, конечно, не помогает с циклами.

Сказав все это, я считаю, что главный приоритет должен заключаться в написании четкого кода. Для Java доступны некоторые отличные инструменты для профилирования (я использую YourKit), что позволяет легко выявлять узкие места производительности и оптимизировать только те биты, в которых это важно.

P.S. Мне никогда не приходилось использовать concat .

Современный компилятор Java преобразует ваши + операции в приложение StringBuilder. Я хочу сказать, если вы выполните str = str1 + str2 + str3 , тогда компилятор сгенерирует следующий код:

Вы можете декомпилировать код с помощью DJ или Cavaj, чтобы подтвердить это:) Итак, теперь это более важный выбор, чем преимущество в производительности для использования + или StringBuilder:)

Однако, учитывая ситуацию, когда компилятор не делает этого для вашего (если вы используете какой-либо частный Java SDK для этого, это может произойти), то, безусловно, StringBuilder — это путь, по которому вы избегаете большого количества ненужных String объектов.

Почему не рекомендуются множественные конкатенации string

Неявное приведение int в String с использованием конкатенации. Конкатенация это плохой способ?

Это хорошие способы. Почему?введите сюда описание изображения

user avatar

Нет, конкатенация — это не плохо. Она выглядит более наглядно и в большинстве случаев, javac умеет ее оптимизировать с использованием java.lang.StringBuilder . Так же, срабатывает интернирование, что позволяет не создавать множество объектов. Например,

А методы Integer.toString и String.valueOf идентичны, т.к. последний делегирует вызов второму. Но у них есть особенность — при каждом вызове создается новая строка.

user avatar

Ну, может быть, если там не просто число, а выражение, не сразу очевидно, что получится в результате. Например такой код:

В первой строке выведет 10, а во второй — 46.

Это плохо потому, что Java — это строго типизированный язык, и неявное приведение типов является нарушением парадигмы языка.

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

Два варианта кода:

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

user avatar

При использовании конкатенации выполняется несколько ненужных действий:

неявно вызывается Integer.toString(a) ;

выделяется StringBuffer для конкатенации, куда копируется пустая строка «» и переведённое в строку число

преобразовывается StringBuffer в строку

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

user avatar

Напишем тестовый класс с двумя способами приведения числа к строки:

Байткод для них будет следующий:

Глядя на байткод можно сделать вывод что в случае конкатенации числа со строкой создается объект StringBuilder , выполняется два раза метод append() и затем toString() .

Методы StringBuilder.append(int value) и Integer.toString(int values) в конечном счете вызывают метод Integer.getChars() . Однако для Android метод Integer.toString(int values) оптимизирован, путем кэширования значений в диапазоне -100..100. Не знаю правда c какой версии эта оптимизация добавилась, но тем не менее она есть.

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

Улучшение производительности конкатенации строк в Java

Как повысить производительность этого фрагмента кода:

ОТВЕТЫ

Ответ 1

Вы можете использовать StringBuilder, вместо того, чтобы делать + = с отдельными строками. Строки неизменны в Java, а это означает, что после создания объекта String вы не можете его изменить. Использование + = для строк в цикле приведет к созданию множества отдельных экземпляров String, что может вызвать проблемы с производительностью. StringBuilder может конкатенировать строки без необходимости создавать новые экземпляры, что может сэкономить некоторое время, в зависимости от точного сценария.

Ответ 2
  • Используйте StringBuilder , когда вам нужно построить строку в цикле
    • + отлично подходит для простой конкатенации, но ужасно для инкрементной сборки
    Не использовать необработанные типы

    Использование типов raw допускается только как уступка совместимости устаревшего кода. Использование необработанных типов в коде, написанном после введения родословности в язык программирования Java, настоятельно не рекомендуется. Возможно, что будущие версии языка программирования Java будут запрещать использование необработанных типов.

    Эффективное Java 2nd Edition: Пункт 23: Не используйте необработанные типы в новом коде

    Если вы используете необработанные типы, вы теряете все преимущества безопасности и выразительности дженериков.

    См. также
    • Конкатенация строк Java
    • Учебники по Java — общие сведения
    Ответ 3

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

    Код, заданный в вопросе, будет фактически скомпилирован (с Sun javac ) к чему-то по следующей строке:

    Компилятор изменит конкатенацию += на ту, которая использует StringBuilder . Однако компилятор, вероятно, перепишет код внутри цикла, поэтому на каждой итерации будет создан новый экземпляр StringBuilder , который не очень удобен для пользователя.

    Следовательно, в этом случае, вероятно, было бы лучшей идеей создать StringBuilder вне цикла самостоятельно и выполнить ручную конкатенацию строк:

    Ответ 4

    Возможно, немного за бортом, здесь каждая оптимизация, о которой я мог подумать для concatStrings() — продемонстрировал выше — некоторые из них могут быть неприменимы к вашей среде:

    • Используйте StringBuilder — это намного эффективнее для этих последовательных конкатенаций
    • Используйте StringBuilder(int capacity) , чтобы указать вероятную необходимую емкость, если есть способ ее предвидеть (используется средний размер выше, но другие методы может быть более удобным)
    • Используйте параметр параметра Collection , чтобы обеспечить более эффективную структуру данных, чем Vector , который синхронизирован — плюс вызывающий имеет гораздо большую гибкость (например, нет необходимости копировать Set<String> в Vector<String> только для вызова этого метода).
    • Простые случаи жесткого кода, если они вероятны (например, null , размер 0 и размер 1 выше).
    • Используйте final , чтобы облегчить встраивание и оптимизацию JIT
    • Загрузите размер strings , если он используется несколько раз. (например, используется 3 раза в приведенном выше коде.)

    Наконец, если эта операция выполняется очень часто над большим количеством строк, загляните в Веревки для Java.

    • Веревки для статьи Java — http://www.ibm.com/developerworks/java/library/j-ropes
    • Веревки реализации Java — http://ahmadsoft.org/ropes/
    • Веревки (Википедия) — http://en.wikipedia.org/wiki/Rope_(computer_science)
    Ответ 5

    Также, если вы хотите сделать это быстрее, вы можете реорганизовать код для использования ArrayList вместо Vector. ArrayList не является потокобезопасным, поэтому он немного быстрее, чем Vector (зависит от ситуации, может быть 0% разница, может быть разница в 5%).

    Ответ 6

    Вы создаете строку каждый раз, когда вы вызываете + =. Например

    Использование построителя строк позволяет избежать этой проблемы.

    Обратите внимание, что в первом примере мы сделали все эти промежуточные строки, где во втором примере мы создали только StringBuilder и финальную строку (в обоих примерах мы создали «1» «2» и «3», когда мы их использовали как аргументы). Вы можете видеть, что в первом примере создано меньше объектов, и если вы много добавляете к String, вы можете себе представить, как это складывается!

    Ответ 7

    В дополнение к использованию StringBuilder вы можете заранее пройти список строк и рассчитать точный размер, необходимый для StringBuilder. Затем передайте это значение в конструктор StringBuilder. Обратите внимание, что это относится к категории преждевременной оптимизации, но вы просили производительность. (Вы должны посмотреть на код для выращивания буферов StringBuilder/StringBuffer, его образовательных)

    Конкатенация строк и производительность

    Советы от 5 февраля 2002 «Запись методов toString » (источник и перевод на JavaGu.ru) включали следующее предложение:

    Обратите внимание, что использование «+» в toString для построения возвращаемого значения не всегда является самым эффективным подходом. Возможно, вы захотите использовать вместо этого StringBuffer .

    Читатель технических советов отметил, что в документации по Java говорится о том, что для фактической реализации оператора + применяется StringBuffer . Поэтому возникает вопрос, какой выигрыш в производительности, если он существует, вы получите при явном использовании StringBuffer в ваших программах? В этой статье делается попытка ответить на этот вопрос.

    Для начала рассмотрим пример, в котором строка формируется путем повторения одного и того же символа:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *