9

通过分析我发现这里的 sprintf 需要很长时间。是否有更好的替代方案仍然可以处理 y/m/dh/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()为罪犯。

4

12 回答 12

20

如果您正在编写自己的函数来完成这项工作,那么 0 .. 61 的字符串值的查找表将避免对除年份之外的所有内容进行任何算术运算。

编辑:请注意,为了应对闰秒(并匹配strftime()),您应该能够打印 60 和 61 的秒值。

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

或者,怎么样strftime()?我不知道性能比较如何(它很可能只是调用 sprintf()),但它值得一看(它可以自己进行上述查找)。

于 2008-11-07T13:09:07.073 回答
6

您可以尝试依次填充输出中的每个字符。

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] = '-';

... ETC...

不漂亮,但你明白了。如果不出意外,它可能有助于解释为什么 sprintf 不会那么快。

OTOH,也许你可以缓存最后的结果。这样你只需要每秒生成一个。

于 2008-11-07T13:01:13.173 回答
6

Printf 需要处理很多不同的格式。您当然可以获取printf 的源代码,并以此为基础推出您自己的版本,专门处理sysTime结构。这样,您传入一个参数,它就完成了需要完成的工作,仅此而已。

于 2008-11-07T13:01:19.850 回答
3

“长”时间是什么意思 - 因为sprintf()是循环中唯一的语句并且循环的“管道”(增量,比较)可以忽略不计,所以sprintf() 必须消耗最多的时间。

还记得那个男人在第三街的一个晚上丢失了他的结婚戒指,但在第五街寻找它,因为那里的灯光更亮的老笑话吗?您已经构建了一个旨在“证明”您的假设sprintf()无效的示例。

sprintf()如果您分析除您使用的所有其他函数和算法之外还包含的“实际”代码,您的结果将更加准确。或者,尝试编写您自己的版本来解决您需要的特定零填充数字转换。

你可能会对结果感到惊讶。

于 2008-11-07T13:07:03.363 回答
3

看起来 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]);

}
于 2008-11-07T14:00:31.337 回答
2

缓存结果怎么样?这不是一种可能吗?考虑到这个特定的 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)
  {
     // 我们可以重用日期部分
     strcpy (buff, datePart);
     strcat (buff, timePart);
  }
别的 {
     // 我们还需要重新生成日期部分
     sprintf (datePart, "%4d-%02d-%02d", sysTime.wYear, sysTime.wMonth, sysTime.wDay);
     strcpy (buff, datePart);
     strcat (buff, timePart);
}

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

上面的代码有一些冗余,使代码更容易理解。您可以轻松排除。如果您知道小时和分钟的变化不会比您对例行程序的调用更快,您可以进一步加快速度。

于 2008-11-07T13:07:36.030 回答
2

我会做几件事...

  • 缓存当前时间,因此您不必每次都重新生成时间戳
  • 手动进行时间转换。printf-family 函数中最慢的部分是格式字符串解析,在每次循环执行时都将循环用于解析是很愚蠢的。
  • 尝试对所有转换使用 2 字节查找表 ( { "00", "01", "02", ..., "99" })。这是因为您想避免模数运算,而 2 字节的表意味着您一年只需要使用一个模数。
于 2008-11-07T13:48:35.450 回答
1

您可能会通过手动滚动一个在返回 buf 中布置数字的例程来提高 w perf,因为您可以避免重复解析格式字符串,并且不必处理 sprintf 处理的许多更复杂的情况。我讨厌实际上建议这样做。

我建议您尝试弄清楚您是否可以以某种方式减少生成这些字符串所需的数量,它们是否有时是可选的,它们是否可以被缓存等等。

于 2008-11-07T13:03:05.290 回答
1

我目前正在解决类似的问题。

我需要在嵌入式系统上记录带有时间戳、文件名、行号等的调试语句。我们已经有一个记录器,但是当我将旋钮转到“完整记录”时,它会占用我们所有的 proc 周期并使我们的系统处于可怕的状态,表明任何计算设备都不应该经历。

有人确实说过“如果不改变你正在测量/观察的东西,你就无法测量/观察某物”。

所以我正在改变一些事情来提高性能。目前的情况是,我比原始函数调用快 2 倍(该日志记录系统的瓶颈不在函数调用中,而是在日志阅读器中,它是一个单独的可执行文件,如果我编写自己的日志堆栈,我可以丢弃它)。

我需要提供的界面类似于 - void log(int channel, char *filename, int lineno, format, ...)。我需要附加通道名称(当前在列表中进行线性搜索!对于每个调试语句!)和时间戳,包括毫秒计数器。以下是我正在做的一些事情以使这更快 -

  • 将频道名称字符串化,这样我就可以strcpy而不是搜索列表。将宏定义LOG(channel, ...etc)log(#channel, ...etc). 如果您通过定义获得固定的10字节通道长度来固定memcpy字符串的长度,则可以使用LOG(channel, ...) log("...."#channel - sizeof("...."#channel) + *11*)
  • 每秒生成几次时间戳字符串。你可以使用 asctime 什么的。然后将固定长度的字符串 memcpy 到每个调试语句。
  • 如果您想实时生成时间戳字符串,那么带有分配的查找表(不是 memcpy!)是完美的。但这仅适用于 2 位数字,也许适用于一年。
  • 三位数(毫秒)和五位数(lineno)呢?我不喜欢 itoa,digit = ((value /= value) % 10)也不喜欢自定义 itoa(),因为 div 和 mods 很。我写了下面的函数,后来发现在 AMD 优化手册(汇编)中有类似的东西,这让我相信这些是最快的 C 实现。

    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% 的时间)

于 2009-05-02T05:40:01.393 回答
1

我测试过的两个快速格式化程序是FastFormatKarma::generateBoost Spirit的一部分)。

您可能还会发现对其进行基准测试或至少查找现有基准很有用。

例如这个(虽然它缺少 FastFormat):

C ++中的快速整数到字符串转换

于 2018-05-09T19:03:36.320 回答
0

StringStream 是我从谷歌得到的建议。

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

于 2008-11-07T12:59:23.753 回答
0

很难想象你会在格式化整数方面击败 sprintf。你确定 sprintf 是你的问题吗?

于 2008-11-07T13:05:55.487 回答