Проблема производительности: сравнение с String.Format - PullRequest
15 голосов
/ 17 апреля 2009

Некоторое время назад пост Джона Скита привил мне в голову идею создания класса 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's 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's useful to see
// how my format method fits.
public string OriginalFormat(params Object[] args)
{
    return String.Format(formatProvider, format, args);
}
Дополнительные примечания:

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

Кроме того, я очень открыт для изменения класса FormatInfo и даже списка parts, если у кого-то есть предложение, которое может улучшить время сборки. Поскольку моя главная задача - это последовательное итерационное время от начала до конца, может быть, LinkedList будет лучше?

[Update]:

Хм ... еще кое-что, что я могу попробовать, это настроить свои тесты Мои тесты были довольно просты: составление имен в формате "{lastname}, {firstname}" и составление отформатированных телефонных номеров из кода города, префикса, номера и добавочных компонентов. Ни один из них не имеет большого количества буквальных сегментов в строке. Размышляя о том, как работал оригинальный синтаксический анализатор конечных автоматов, я думаю, что именно эти литеральные сегменты именно там, где мой код имеет наилучшие шансы на успех, потому что мне больше не нужно проверять каждый символ в строке. Еще одна мысль:

Этот класс все еще полезен, даже если я не могу заставить его идти быстрее. Пока производительность не хуже , чем базовая String.Format (), я все же создал строго типизированный интерфейс, который позволяет программе собирать свою собственную "строку формата" во время выполнения. Все, что мне нужно сделать, это предоставить открытый доступ к списку деталей.

Ответы [ 6 ]

8 голосов
/ 18 апреля 2009

Вот окончательный результат:

Я изменил форматную строку в пробном тесте на что-то, что должно быть более благоприятным для моего кода:

Быстрый коричневый {0} перепрыгнул через ленивого {1}.

Как я и ожидал, этот тариф намного лучше по сравнению с оригиналом; 2 миллиона итераций за 5,3 секунды для этого кода против 6,1 секунды для String.Format. Это неоспоримое улучшение. Вы можете даже испытать желание начать использовать это как простую замену для многих String.Format ситуаций. В конце концов, у вас все будет не хуже, и вы даже можете получить небольшое повышение производительности: целых 14%, и это не к чему чихать.

За исключением того, что это так. Имейте в виду, что мы все еще говорим о разнице менее чем в полсекунды для 2 миллионов попыток в ситуации, специально разработанной для поддержки этого кода. Даже загруженные страницы ASP.Net могут создать такую ​​большую нагрузку, если только вам не повезло работать на топ-100 веб-сайта.

Прежде всего, это исключает одну важную альтернативу: вы можете каждый раз создавать новый StringBuilder и вручную обрабатывать свое собственное форматирование с использованием необработанных вызовов Append(). С этой техникой мой тест закончился за всего за 3,9 секунды. Это намного большее улучшение.


Таким образом, если производительность не так важна, вам следует придерживаться ясности и простоты встроенной опции. Но когда в ситуации, когда профилирование показывает, что это действительно влияет на вашу производительность, есть лучшая альтернатива, доступная через StringBuilder.Append().

3 голосов
/ 18 апреля 2009

Не останавливайся сейчас!

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

Я сделал то же самое в Java, и вот некоторые из функций, которые я добавил (кроме просто скомпилированных строк формата):

1) Метод format () принимает либо массив varargs, либо карту (в .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) У меня есть специальный синтаксис, который позволяет форматированным проверять аргумент на нулевое значение, применяя другой форматер в зависимости от того, является объект нулевым или ненулевым.

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, сообщений об ошибках и строк локализации. Это чрезвычайно полезно.

1 голос
/ 17 апреля 2009

Мне кажется, что для того, чтобы добиться реального улучшения производительности, вам нужно вычленить любой анализ формата, выполненный вашим customFormatter, и аргументы форматирования в функцию, которая возвращает некоторую структуру данных, которая сообщает последующему вызову форматирования, что делать , Затем вы извлекаете эти структуры данных в свой конструктор и сохраняете их для дальнейшего использования. Предположительно, это будет связано с расширением ICustomFormatter и IFormattable. Кажется маловероятным.

0 голосов
/ 18 апреля 2009

Я должен верить, что, потратив столько же времени на оптимизацию ввода / вывода данных, вы получите экспоненциально большую отдачу!

Это, безусловно, двоюродный брат Ягни за это. Избегайте преждевременной оптимизации. APO.

0 голосов
/ 17 апреля 2009

Каркас предоставляет явные переопределения для методов форматирования, которые принимают списки параметров фиксированного размера вместо подхода params object [], чтобы удалить издержки на выделение и сбор всех временных массивов объектов. Вы можете рассмотреть это и для своего кода. Кроме того, предоставление строго типизированных перегрузок для общих типов значений уменьшит накладные расходы на упаковку.

0 голосов
/ 17 апреля 2009

У вас также есть время для компиляции JIT? В конце концов, структура будет ngen'd, который может объяснить различия?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...