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

StackOverflow https://stackoverflow.com/questions/271971

Вопрос

С помощью профилирования я обнаружил, что sprintf здесь занимает много времени.Есть ли более производительная альтернатива, которая по-прежнему обрабатывает начальные нули в полях 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);

}

Примечание:OP объясняет в комментариях, что это урезанный пример."Реальный" цикл содержит дополнительный код, который использует различные значения времени из базы данных.Профилирование точно определило sprintf() как преступник.

Это было полезно?

Решение

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

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

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

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

Другие советы

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

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, может быть, вы могли бы кэшировать последний результат. Таким образом, вам нужно будет генерировать только одну каждую секунду.

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

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

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

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

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

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

В дополнение к уже предложенному методу таблицы поиска (массив 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]);

}

Как насчет кэширования результатов? Разве это не возможно? Учитывая, что этот конкретный вызов 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));

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

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

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

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

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

В данный момент я работаю над аналогичной проблемой.

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

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

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

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

  • Укажите название канала, чтобы я мог strcpy вместо того, чтобы искать по списку.определить макрос LOG(channel, ...etc) как log(#channel, ...etc).Вы можете использовать memcpy если вы исправите длину строки, определив LOG(channel, ...) log("...."#channel - sizeof("...."#channel) + *11*) чтобы починиться 10 длины каналов в байтах
  • Генерируйте строку временной метки пару раз в секунду.Вы можете использовать asctime или что-то в этом роде.Затем memcpy добавляет строку фиксированной длины к каждому оператору отладки.
  • Если вы хотите сгенерировать строку временной метки в режиме реального времени, то поисковая таблица с присваиванием (не memcpy!) идеально подойдет.Но это работает только для двухзначных чисел и, возможно, для года.
  • Как насчет трехзначных (миллисекунды) и пятизначных (линейно) значений?Мне не нравится итоа, и мне не нравится обычай итоа (digit = ((value /= value) % 10)) либо потому, что divs и моды медленно.Я написал приведенные ниже функции и позже обнаружил, что нечто подобное есть в руководстве по оптимизации 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%)

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

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

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

 Быстрое преобразование целого числа в строку в C ++

StringStream - это предложение, которое я получил от Google.

http://bytes.com/forum/thread132583.html

Трудно представить, что вы собираетесь выиграть у sprintf при форматировании целых чисел. Вы уверены, что sprintf это ваша проблема?

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top