Как я могу улучшить / заменить sprintf, который, по моим оценкам, является горячей точкой производительности? - PullRequest
8 голосов
/ 07 ноября 2008

Благодаря профилированию я обнаружил, что спринт здесь занимает много времени. Есть ли более эффективная альтернатива, которая по-прежнему обрабатывает ведущие нули в полях y / m / d h / m / s?

SYSTEMTIME sysTime;
GetLocalTime( &sysTime );
char buf[80];
for (int i = 0; i < 100000; i++)
{

    sprintf(buf, "%4d-%02d-%02d %02d:%02d:%02d",
        sysTime.wYear, sysTime.wMonth, sysTime.wDay, 
        sysTime.wHour, sysTime.wMinute, sysTime.wSecond);

}

Примечание. ФП поясняет в комментариях, что это упрощенный пример. «Реальный» цикл содержит дополнительный код, который использует различные значения времени из базы данных. Профилирование определило sprintf() как нарушителя.

Ответы [ 12 ]

19 голосов
/ 07 ноября 2008

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

edit: Обратите внимание, что для того, чтобы справиться с високосными секундами (и соответствовать strftime()), вы должны иметь возможность печатать значения секунд 60 и 61.

char LeadingZeroIntegerValues[62][] = { "00", "01", "02", ... "59", "60", "61" };

В качестве альтернативы, как насчет strftime()? Я понятия не имею, как сравнивается производительность (это может быть просто вызов sprintf ()), но на это стоит обратить внимание (и он мог бы выполнить сам поиск выше).

6 голосов
/ 07 ноября 2008

Printf должен работать с множеством различных форматов. Вы, конечно, можете получить исходный текст для printf и использовать его в качестве основы для создания своей собственной версии, в которой конкретно рассматривается структура sysTime . Таким образом, вы передаете один аргумент, и он выполняет именно ту работу, которая должна быть выполнена, и ничего более.

6 голосов
/ 07 ноября 2008

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

buf[0] = (sysTime.wYear / 1000) % 10 + '0' ;
buf[1] = (sysTime.wYear / 100) % 10 + '0';
buf[2] = (sysTime.wYear / 10) % 10 + '0';
buf[3] = sysTime.wYear % 10 + '0';
buf[4] = '-';

... и т.д ...

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

OTOH, может быть, вы могли бы кэшировать последний результат. Таким образом, вам нужно будет генерировать только одну каждую секунду.

3 голосов
/ 07 ноября 2008

Похоже, Джейуокер предлагает очень похожий метод (побейте меня менее чем за час).

В дополнение к уже предложенному методу таблицы поиска (массив n2s [] ниже), как насчет генерации буфера форматирования, чтобы обычный sprintf был менее интенсивным? Приведенный ниже код должен будет заполнять только минуты и секунды каждый раз в цикле, если только год / месяц / день / час не изменились. Очевидно, что если что-то из этого изменилось, вы получаете еще одно попадание в sprintf, но в целом оно может быть не больше того, что вы наблюдаете в настоящее время (в сочетании с поиском в массиве).


static char fbuf[80];
static SYSTEMTIME lastSysTime = {0, ..., 0};  // initialize to all zeros.

for (int i = 0; i < 100000; i++)
{
    if ((lastSysTime.wHour != sysTime.wHour)
    ||  (lastSysTime.wDay != sysTime.wDay)
    ||  (lastSysTime.wMonth != sysTime.wMonth)
    ||  (lastSysTime.wYear != sysTime.wYear))
    {
        sprintf(fbuf, "%4d-%02s-%02s %02s:%%02s:%%02s",
                sysTime.wYear, n2s[sysTime.wMonth],
                n2s[sysTime.wDay], n2s[sysTime.wHour]);

        lastSysTime.wHour = sysTime.wHour;
        lastSysTime.wDay = sysTime.wDay;
        lastSysTime.wMonth = sysTime.wMonth;
        lastSysTime.wYear = sysTime.wYear;
    }

    sprintf(buf, fbuf, n2s[sysTime.wMinute], n2s[sysTime.wSecond]);

}
3 голосов
/ 07 ноября 2008

Что вы подразумеваете под «длинным» временем - поскольку sprintf() является единственным оператором в вашем цикле, а «вертикаль» цикла (приращение, сравнение) незначительна, sprintf() потребляет больше всего времени.

Помните старую шутку о человеке, который однажды ночью потерял свое обручальное кольцо на 3-й улице, но искал его 5-го, потому что там свет был ярче? Вы создали пример, который призван «доказать» ваше предположение о том, что sprintf() неэффективно.

Ваши результаты будут более точными, если вы профилируете «фактический» код, который содержит sprintf() в дополнение ко всем другим функциям и алгоритмам, которые вы используете. В качестве альтернативы попробуйте написать свою собственную версию, которая обращается к конкретному преобразованию чисел с добавлением нуля, которое вам требуется.

Вы можете быть удивлены результатами.

2 голосов
/ 07 ноября 2008

Я бы сделал несколько вещей ...

  • кэширует текущее время, чтобы вам не приходилось каждый раз перегенерировать отметку времени
  • сделать преобразование времени вручную. Самая медленная часть функций printf -семейства - это синтаксический анализ строки формата, и глупо выделять циклы этому анализу при каждом выполнении цикла.
  • попробуйте использовать 2-байтовые таблицы для всех преобразований ({ "00", "01", "02", ..., "99" }). Это потому, что вы хотите избежать модульной арифметики, а 2-байтовая таблица означает, что вы должны использовать только один модуль по году.
2 голосов
/ 07 ноября 2008

Как насчет кеширования результатов? Разве это не возможно? Учитывая, что этот конкретный вызов sprintf () выполняется слишком часто в вашем коде, я предполагаю, что между большинством этих последовательных вызовов год, месяц и день не меняются.

Таким образом, мы можем реализовать что-то вроде следующего. Объявите старую и текущую структуру SYSTEMTIME:

SYSTEMTIME sysTime, oldSysTime;

Кроме того, объявите отдельные части для хранения даты и времени:

char datePart[80];
char timePart[80];

В первый раз вам нужно будет заполнить как sysTime, oldSysTime, так и datePart и timePart. Но последующие sprintf () могут быть сделаны довольно быстро, как указано ниже:

sprintf (timePart, "%02d:%02d:%02d", sysTime.wHour, sysTime.wMinute, sysTime.wSecond);
if (oldSysTime.wYear == sysTime.wYear && 
  oldSysTime.wMonth == sysTime.wMonth &&
  oldSysTime.wDay == sysTime.wDay) 
  {
     // we can reuse the date part
     strcpy (buff, datePart);
     strcat (buff, timePart);
  }
else {
     // we need to regenerate the date part as well
     sprintf (datePart, "%4d-%02d-%02d", sysTime.wYear, sysTime.wMonth, sysTime.wDay);
     strcpy (buff, datePart);
     strcat (buff, timePart);
}

memcpy (&oldSysTime, &sysTime, sizeof (SYSTEMTIME));

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

1 голос
/ 09 мая 2018

Я протестировал два быстрых форматера: FastFormat и Karma :: generate (часть Boost Spirit ).

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

Например, , этот (хотя в нем отсутствует FastFormat):

Fast integer to string conversion in C++

1 голос
/ 02 мая 2009

Я сейчас работаю над аналогичной проблемой.

Мне нужно регистрировать операторы отладки с отметкой времени, именем файла, номером строки и т. Д. Во встроенной системе. У нас уже есть регистратор, но когда я поворачиваю ручку на «полное ведение журнала», он съедает все наши циклы обработки и переводит нашу систему в ужасное состояние, заявляя, что никакое вычислительное устройство никогда не должно испытывать.

Кто-то сказал: «Вы не можете измерить / наблюдать что-то, не изменив то, что вы измеряете / наблюдаете».

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

Интерфейс, который мне нужно предоставить, похож на void log(int channel, char *filename, int lineno, format, ...). Мне нужно добавить имя канала (который в настоящее время выполняет линейный поиск в списке! Для каждого отдельного оператора отладки!) И метку времени, включая счетчик миллисекунд. Вот некоторые вещи, которые я делаю, чтобы сделать это быстрее -

  • Stringify название канала, чтобы я мог strcpy вместо поиска в списке. определить макрос LOG(channel, ...etc) как log(#channel, ...etc). Вы можете использовать memcpy, если вы фиксируете длину строки, определяя LOG(channel, ...) log("...."#channel - sizeof("...."#channel) + *11*), чтобы получить фиксированную 10 длину канала в байтах
  • Генерация строки метки времени пару раз в секунду. Вы можете использовать asctime или что-то. Затем memcpy строка фиксированной длины для каждого оператора отладки.
  • Если вы хотите сгенерировать строку метки времени в реальном времени, тогда справочная таблица с присваиванием (не memcpy!) Идеальна. Но это работает только для двузначных чисел и, возможно, для года.
  • А как насчет трех цифр (миллисекунд) и пяти цифр (белья)? Мне не нравится itoa, и мне не нравятся пользовательские itoa (digit = ((value /= value) % 10)), потому что div и моды медленные . Я написал нижеприведенные функции, а потом обнаружил, что нечто подобное есть в руководстве по оптимизации AMD (в сборке), которое дает мне уверенность в том, что речь идет о самых быстрых реализациях языка Си.

    void itoa03(char *string, unsigned int value)
    {
       *string++ = '0' + ((value = value * 2684355) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = ' ';/* null terminate here if thats what you need */
    }
    

    Аналогично, для номеров строк,

    void itoa05(char *string, unsigned int value)
    {
       *string++ = ' ';
       *string++ = '0' + ((value = value * 26844 + 12) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = '0' + ((value = ((value & 0x0FFFFFFF)) * 10) >> 28);
       *string++ = ' ';/* null terminate here if thats what you need */
    }
    

В целом мой код теперь довольно быстрый. vsnprintf(), который мне нужно использовать, занимает около 91% времени, а остальная часть моего кода занимает всего 9% (тогда как остальная часть кода, т. Е. За исключением vsprintf() раньше использовалась на 54% раньше)

1 голос
/ 07 ноября 2008

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

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

...