Ошибка производительности: по сравнению с 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.