Почему String.Concat не оптимизирован для StringBuilder.Append? - программирование

Почему String.Concat не оптимизирован для StringBuilder.Append?

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

Теперь, когда строка конкатенации строк известна только во время выполнения, почему компилятор не оптимизирует конкатенацию строк в циклах и конкатенациях, например, более 10 строк вместо StringBuilder.Append? Я имею в виду, это возможно, не так ли? Создайте экземпляр StringBuilder и возьмите каждую конкатенацию и превратите ее в вызов Append().

Есть ли какая-то причина, по которой это должно было бы или не могло бы не быть оптимизировано? Что мне не хватает?

4b9b3361

Ответ 1

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

Если ваш вопрос заключается в том, почему компилятор не делает этого:

string s = "";
for( int i = 0; i < 100; i ++ )
    s = string.Concat( s, i.ToString() );

в это:

StringBuilder sb = new StringBuilder();
for( int i = 0; i < 100; i++ )
    sb.Append( i.ToString() );
string s = sb.ToString();

Самый вероятный ответ: это не оптимизация. Это переписывание кода, который вводит новые конструкции, основанные на знаниях и намерениях разработчика, а не на компиляторе.

Этот тип изменения потребует от компилятора большего знания BCL, чем это подходит. Что, если завтра появится более оптимальная служба сборки строк? Должен ли компилятор использовать это?

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

Наконец, я не уверен, что во всех случаях это приведет к ускорению выполнения кода. Существует стоимость создания экземпляра StringBuilder и изменения размера его внутреннего буфера при добавлении текста. Фактически, затраты на добавление сильно привязаны к размеру конкатенированной строки, сколько есть, какое давление памяти выглядит. Это те вещи, которые компилятор не может предсказать заранее.

Это ваша работа в качестве разработчика для написания хорошо исполняемого кода. Компилятор может только помочь, создав определенные безопасные, сохраняющие инварианты оптимизации. Не переписывайте свой код для вас.

Ответ 2

Ответ Л.Бускина превосходный; У меня есть только несколько вещей, которые нужно добавить.

Во-первых, JScript.NET делает эту оптимизацию. JScript часто используется менее опытными программистами для задач, связанных с построением больших строк в циклах, таких как создание объектов JSON, данных HTML и т.д.

Поскольку эти программисты могут не знать о n-квадратичной стоимости наивного распределения строк, могут не знать о существовании строковых сборщиков и часто писать код с использованием этого шаблона, мы чувствовали, что было бы разумно поставить эту оптимизацию в JScript.NET.

Программисты С#, как правило, более осведомлены о базовых затратах кода, который они пишут, и больше знают о существовании готовых частей, таких как StringBuilder, поэтому им нужна эта оптимизация меньше. И, что более важно, философия дизайна С# заключается в том, что это "делать то, что я сказал", язык с минимальной "магией"; JScript - это "делать то, что я имею в виду", который делает все возможное, чтобы понять, как наилучшим образом служить вам, даже если это означает, что иногда ошибается. Обе философии действительны и полезны.

Иногда он "идет другим путем". Сравните этот выбор с выбором, который мы делаем для переключателей на строках. Переключение на строки фактически скомпилировано как создание словаря, содержащего строки, а не как ряд сравнений строк. Эта оптимизация может быть плохим; возможно, быстрее просто выполнить сравнение строк. Но здесь мы предполагаем, что вы "означали", что переключатель будет выглядеть в виде таблицы, а не в серии "if", - если бы вы имели в виду серию утверждений if, вы могли бы легко написать это сами.

Ответ 3

Для одной конкатенации нескольких строк (например, a + b + c + d + e + f + g + h + я + j) вы действительно хотите использовать String.Concat IMO. У него есть накладные расходы на создание массива для каждого вызова, но он имеет то преимущество, что метод может выработать точную длину результирующей строки до того, как потребуется выделить любую память. StringBuilder.Append(a).Append(b)... дает только одно значение за раз, поэтому строитель не знает, сколько памяти выделяется.

Как для этого в циклах - в этот момент вы добавили новую локальную переменную, и вам нужно добавить код для записи обратно в строковую переменную точно в нужное время (вызов StringBuilder.ToString()). Что происходит, когда вы работаете в отладчике? Разве это не было бы довольно запутанным, чтобы не видеть, как нарастает значение, только становясь видимым в конце цикла? О, и, конечно же, вы должны выполнить соответствующую проверку того, что значение не используется в какой-либо момент до конца цикла...

Ответ 4

Две причины:

  • Вы не можете программно идентифицировать места, где он будет строго выше.
  • "Оптимизация" замедлит работу, если будет выполнена некорректно.

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

Изменить: Что касается отсечки, у нас есть еще две проблемы:

  • Единственный способ точно знать, что обрезание достигнуто, - это сложный анализ потока. Число мест, где это могло бы найти разделы, которые могли быть преобразованы, крайне невелико.
  • Анализ потока дорог. Если вы это сделаете во время выполнения, вся программа будет работать медленнее за редкий шанс, что один фрагмент плохо написанного кода будет быстрее. Если вы это делаете во время компиляции, это не ошибка в соответствии с синтаксисом языка, но вы можете выпустить предупреждение - и это именно то, что делает FXCop (медленный, но доступный инструмент анализа потока). Подумайте, всегда ли FXCop должен был работать с компилятором; так много часов люди будут просто ждать, чтобы запустить код. И если это было во время выполнения, хорошо приветствуем время запуска JVM...

Ответ 5

Я считаю, что это было бы слишком сложно для компиляторов. И когда вы ссылаетесь на промежуточные строки внутри циклов помимо конкатенации (например, передавая их каким-то другим методам или таким образом), эта оптимизация будет невозможна.

Ответ 6

Потому что это задание компилятора для генерации семантически-правильного кода. Изменение invocations String.Concat на вызовы StringBuilder.Append приведет к изменению семантики кода.

Ответ 7

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

Для конкатенации известного набора строк StringBuilder не быстрее, чем String.Concat.

Ответ 8

Строка - неизменяемый тип, поэтому использование конкатенации строки происходит медленнее, чем при использовании StringBuilder.Append.

Изменить: Чтобы пояснить мою точку зрения немного, когда вы говорите о том, почему String.Concat не оптимизирован для StringBuilder.Append, класс StringBuilder имеет совершенно другую семантику неизменяемого типа String. Почему вы должны ожидать, что компилятор будет оптимизировать это, поскольку они явно представляют собой две разные вещи? Кроме того, a StringBuilder является изменяемым типом, который может динамически изменять свою длину, почему компилятор должен оптимизировать неизменяемый тип к изменяемому типу? Это дизайн и семантика, укоренившиеся в спецификации ECMA для .NET Framework, независимо от языка.

Это немного похоже на запрос компилятора (и, возможно, слишком многого), чтобы скомпилировать char и оптимизировать его в int, потому что int работает на 32 бита вместо 8 бит и будет считаться быстрее!