Ошибка производительности: по сравнению с String.Format
Некоторое время назад пост Джона Скита привил в мою голову идею создания класса CompiledFormatter
для использования в цикле вместо String.Format()
.
Идея состоит в том, что часть вызова String.Format()
потраченная на разбор строки формата, является дополнительной; мы должны иметь возможность повысить производительность, переместив этот код за пределы цикла. Хитрость, конечно, в том, что новый код должен точно соответствовать поведению String.Format()
.
На этой неделе я наконец сделал это. Я использовал исходный код .Net, предоставленный Microsoft, чтобы напрямую адаптировать их синтаксический анализатор (оказывается, String.Format()
самом деле обрабатывает эту работу как StringBuilder.AppendFormat()
). Код, который я придумал, работает, так как мои результаты точны в пределах моих (по общему признанию ограниченных) тестовых данных.
К сожалению, у меня все еще есть одна проблема: производительность. В моих начальных тестах производительность моего кода близко совпадает с обычной String.Format()
. Там нет никакого улучшения вообще; это даже последовательно на несколько миллисекунд медленнее. По крайней мере, он все еще в том же порядке (то есть: количество, которое медленнее, не увеличивается; оно остается в течение нескольких миллисекунд даже при росте набора тестов), но я надеялся на что-то лучшее.
Вполне возможно, что внутренние вызовы StringBuilder.Append()
- это то, что на самом деле влияет на производительность, но я хотел бы посмотреть, могут ли умные люди помочь улучшить ситуацию.
Вот соответствующая часть:
private class FormatItem
{
public int index; //index of item in the argument list. -1 means it a literal from the original format string
public char[] value; //literal data from original format string
public string format; //simple format to use with supplied argument (ie: {0:X} for Hex
// for fixed-width format (examples below)
public int width; // {0,7} means it should be at least 7 characters
public bool justify; // {0,-7} would use opposite alignment
}
//this data is all populated by the constructor
private List<FormatItem> parts = new List<FormatItem>();
private int baseSize = 0;
private string format;
private IFormatProvider formatProvider = null;
private ICustomFormatter customFormatter = null;
// the code in here very closely matches the code in the String.Format/StringBuilder.AppendFormat methods.
// Could it be faster?
public String Format(params Object[] args)
{
if (format == null || args == null)
throw new ArgumentNullException((format == null) ? "format" : "args");
var sb = new StringBuilder(baseSize);
foreach (FormatItem fi in parts)
{
if (fi.index < 0)
sb.Append(fi.value);
else
{
//if (fi.index >= args.Length) throw new FormatException(Environment.GetResourceString("Format_IndexOutOfRange"));
if (fi.index >= args.Length) throw new FormatException("Format_IndexOutOfRange");
object arg = args[fi.index];
string s = null;
if (customFormatter != null)
{
s = customFormatter.Format(fi.format, arg, formatProvider);
}
if (s == null)
{
if (arg is IFormattable)
{
s = ((IFormattable)arg).ToString(fi.format, formatProvider);
}
else if (arg != null)
{
s = arg.ToString();
}
}
if (s == null) s = String.Empty;
int pad = fi.width - s.Length;
if (!fi.justify && pad > 0) sb.Append(' ', pad);
sb.Append(s);
if (fi.justify && pad > 0) sb.Append(' ', pad);
}
}
return sb.ToString();
}
//alternate implementation (for comparative testing)
// my own test call String.Format() separately: I don't use this. But it useful to see
// how my format method fits.
public string OriginalFormat(params Object[] args)
{
return String.Format(formatProvider, format, args);
}
Дополнительные примечания:
Я опасаюсь предоставить исходный код для моего конструктора, потому что я не уверен в последствиях лицензирования из-за моей зависимости от оригинальной реализации .Net. Однако любой, кто хочет проверить это, может просто сделать соответствующие частные данные общедоступными и назначить значения, которые имитируют определенную строку формата.
Кроме того, я очень открыт для изменения класса FormatInfo
и даже списка parts
если у кого-то есть предложение, которое может улучшить время сборки. Поскольку моя главная задача - это последовательное время итерации от начала до конца, может быть, LinkedList
будет лучше?
[Обновить]:
Хм... еще кое-что, что я могу попробовать, это настроить свои тесты. Мои тесты были довольно просты: составление имен в формате "{lastname}, {firstname}"
и составление отформатированных телефонных номеров из кода города, префикса, номера и добавочных компонентов. Ни один из них не имеет большого количества буквальных сегментов в строке. Размышляя о том, как работал оригинальный синтаксический анализатор конечных автоматов, я думаю, что эти литеральные сегменты именно там, где мой код имеет наилучшие шансы на успех, потому что мне больше не нужно проверять каждый символ в строке.
Еще одна мысль:
Этот класс все еще полезен, даже если я не могу заставить его идти быстрее. Пока производительность не хуже базовой String.Format(), я по-прежнему создавал строго типизированный интерфейс, который позволяет программе собирать свою собственную "строку формата" во время выполнения. Все, что мне нужно сделать, это предоставить открытый доступ к списку деталей.
Ответы
Ответ 1
Здесь конечный результат:
Я изменил строку формата в тестовом тестировании на то, что должно помочь моему коду немного больше:
Быстро коричневый {0} перепрыгнул через ленивый {1}.
Как я и ожидал, этот тариф намного лучше по сравнению с оригиналом; 2 миллиона итераций за 5,3 секунды для этого кода против 6,1 секунды для String.Format
. Это неоспоримое улучшение. Возможно, у вас может возникнуть соблазн начать использовать это как беззаботную замену для многих ситуаций String.Format
. В конце концов, вы сделаете не хуже, и вы даже можете добиться небольшого повышения производительности: всего 14%, и что ничего не чихать.
Кроме того, что это так. Имейте в виду, что мы по-прежнему говорим о менее чем половине второго разницы за 2 миллиона попыток в ситуации, специально разработанной для поддержки этого кода. Даже не занятые страницы ASP.Net, вероятно, создадут такую большую нагрузку, если вам не повезет работать на веб-сайте.
В первую очередь это исключает одну важную альтернативу: вы можете просто создавать новый StringBuilder
каждый раз и вручную обрабатывать свое собственное форматирование с помощью raw Append()
вызовов. С помощью этой методики мой результат закончился всего лишь 3,9 секунды. Это намного больше.
Итак, в конце концов, если вы находитесь в ситуации, когда важна производительность, есть лучшая альтернатива. И если это не имеет значения, вы, вероятно, захотите придерживаться ясности использования простого встроенного метода.
Ответ 2
Не останавливайся сейчас!
Пользовательский форматтер может быть немного более эффективным, чем встроенный API, но вы можете добавить больше возможностей для своей собственной реализации, что сделало бы его более полезным.
Я сделал аналогичную вещь в Java, и вот некоторые из функций, которые я добавил (помимо только предварительно скомпилированных строк формата):
1) Метод format() принимает либо массив varargs, либо Map (в .NET, это будет словарь). Поэтому мои строки формата могут выглядеть так:
StringFormatter f = StringFormatter.parse(
"the quick brown {animal} jumped over the {attitude} dog"
);
Затем, если у меня уже есть мои объекты на карте (что довольно распространено), я могу вызвать метод формата следующим образом:
String s = f.format(myMap);
2) У меня есть специальный синтаксис для выполнения замещений регулярных выражений в строках во время процесса форматирования:
// After calling obj.toString(), all space characters in the formatted
// object string are converted to underscores.
StringFormatter f = StringFormatter.parse(
"blah blah blah {0:/\\s+/_/} blah blah blah"
);
3) У меня есть специальный синтаксис, который позволяет отформатировать проверку аргумента для null-ness, применяя другой форматтер в зависимости от того, является ли объект нулевым или ненулевым.
StringFormatter f = StringFormatter.parse(
"blah blah blah {0:?'NULL'|'NOT NULL'} blah blah blah"
);
Есть еще миллионы других вещей, которые вы можете сделать. Одна из задач в моем списке задач - это добавить новый синтаксис, в котором вы можете автоматически форматировать списки, наборы и другие коллекции, указав форматтера для применения к каждому элементу, а также строку для вставки между всеми элементами. Что-то вроде этого...
// Wraps each elements in single-quote charts, separating
// adjacent elements with a comma.
StringFormatter f = StringFormatter.parse(
"blah blah blah {0:@['$'][,]} blah blah blah"
);
Но синтаксис немного неудобен, и я еще не люблю его.
Во всяком случае, дело в том, что существующий класс может быть не намного эффективнее, чем API фреймворка, но если вы его расширите, чтобы удовлетворить все ваши личные потребности в форматировании строк, вы можете получить очень удобную библиотеку в конец. Лично я использую свою собственную версию этой библиотеки для динамического построения всех строк SQL, сообщений об ошибках и строк локализации. Это чрезвычайно полезно.
Ответ 3
Мне кажется, что для того, чтобы добиться фактического повышения производительности, вам нужно будет отформатировать любой формат анализа, сделанный вашими спецификаторами и форматируемыми аргументами, в функцию, которая возвращает некоторую структуру данных, которая сообщает о более позднем форматировании, что делать, Затем вы извлекаете эти структуры данных в свой конструктор и сохраняете их для последующего использования. Предположительно это предполагает расширение ICustomFormatter и IFormattable. Кажется, маловероятно.
Ответ 4
У вас есть время для компиляции JIT? В конце концов, структура будет ngen'd, которая могла бы учитывать различия?
Ответ 5
Структура предоставляет явные переопределения методам формата, которые используют списки параметров фиксированного размера вместо подхода params object [], чтобы удалить накладные расходы для выделения и сбора всех временных массивов объектов. Вы можете также подумать об этом для своего кода. Кроме того, предоставление сильно типизированных перегрузок для общих типов значений уменьшит накладные расходы бокса.
Ответ 6
Я должен полагать, что тратить столько времени на оптимизацию данных IO будет получать экспоненциально большие прибыли!
Это, наверняка, кузен для YAGNI для этого. Избегайте преждевременной оптимизации. APO.