【问题标题】:Getting the best performance out of {fmt}从 {fmt} 中获得最佳性能
【发布时间】:2019-07-18 23:20:58
【问题描述】:

我需要将 FILETIME 值格式化为宽字符串缓冲区,并且配置提供了格式字符串。

我实际上在做什么:

  • Config 提供格式字符串:L"{YYYY}-{MM}-{DD} {hh}:{mm}:{ss}.{mmm}"

  • 将 FILETIME 转换为系统时间:

    SYSTEMTIME stUTC;
    FileTimeToSystemTime(&fileTime, &stUTC);
  • 格式化字符串
    fmt::format_to(std::back_inserter(buffer), strFormat,        
             fmt::arg(L"YYYY", stUTC.wYear),
             fmt::arg(L"MM", stUTC.wMonth),
             fmt::arg(L"DD", stUTC.wDay),
             fmt::arg(L"hh", stUTC.wHour),
             fmt::arg(L"mm", stUTC.wMinute),
             fmt::arg(L"ss", stUTC.wSecond),
             fmt::arg(L"mmm", stUTC.wMilliseconds));

我完全理解服务是要付出代价的 :) 但我的代码调用此语句数百万次,并且性能损失明显存在(超过 6% 的 CPU 使用率)。

欢迎“任何我可以做的改进此代码”。

我看到 {fmt} 有一个 time API support。 不幸的是,它似乎无法格式化时间/日期的毫秒部分,并且需要从FILETIMEstd::time_t 的一些转换工作...

我是否应该忘记“自定义”格式字符串并为FILETIME(或SYSTEMTIME)类型提供自定义格式器?这会显着提升性能吗?

如果您能提供任何指导,我将不胜感激。

【问题讨论】:

  • 如果我非常关心性能,我不会使用任何像这样的通用格式化工具。相反,我会考虑将自定义格式字符串处理成一个简单的状态机,其中输入由 SYSTEMTIME 结构中的偏移量驱动,以及所有 2、3 和 4 位零填充无符号值的字符串查找表。另一个针对频繁日志条目的小优化是,如果时间与之前的时间相同,则避免转换时间。
  • 如果您的日志记录例程显示在您的性能图表中,您通常会误解图表,或者做错了什么。能发一下代码吗?
  • 时间是从文件系统元数据中提取的,以百万计。因此,我无法预测它们的值(并且与当前时间无关)。这不是日志记录例程,而是将文件系统元数据转储到 csv 的工具。不过我喜欢你的状态机想法。
  • 我添加了一个答案,其中包含一些关于我将如何处理的更具体的细节。在答案中,我指出它并不是真正的状态机。好吧,我想它在技术上是,但它几乎是有史以来最简单的状态机。希望对您有所帮助。
  • 你试过使用 fmt 的 constexpr 格式字符串解析器吗? (按照文档中关于启用无效参数类型的编译时检测的说明启用它)

标签: c++ fmt


【解决方案1】:

在 cmets 中,我建议将您的自定义时间格式字符串解析为一个简单的状态机。它甚至不必是这样的状态机。它只是一系列线性指令。

目前,fmt 类需要做一些工作来解析格式类型,然后将整数转换为零填充字符串。尽管不太可能,但它有可能像我将要建议的那样进行了高度优化。

基本思想是有一个(大)查找表,当然可以在运行时生成,但为了快速说明:

const wchar_t zeroPad4[10000][5] = { L"0000", L"0001", L"0002", ..., L"9999" };

如果需要,您可以有 1 位、2 位和 3 位查找表,或者如果您只添加偏移量,则可以识别这些值都包含在 4 位查找表中。

所以要输出一个数字,您只需要知道SYSTEMTIME 中的偏移量是什么,该值是什么类型,以及要应用什么字符串偏移量(0 表示 4 位,1 表示 3 位等) .它使事情变得更简单,因为SYSTEMTIME 中的所有结构元素都是相同的类型。您应该合理地假设没有值需要范围检查,尽管如果不确定,您可以添加。

你可以这样配置:

struct Output {
    int dataOffset;  // offset into SYSTEMTIME struct
    int count;       // extra adjustment after string lookup
};

文字字符串呢?好吧,您可以复制这些内容,也可以重新调整Output 的用途,以使用负数dataOffset 表示格式字符串中的起始位置,并使用count 保存在该模式下要输出的字符数。如果您需要额外的输出模式,请使用 mode 成员扩展此结构。

Anwyay,让我们以您的字符串L"{YYYY}-{MM}-{DD} {hh}:{mm}:{ss}.{mmm}" 为例。解析后,您将得到:

Output outputs[] {
    { offsetof(SYSTEMTIME, wYear), 0 },         // "{YYYY}"
    { -6, 1 },                                  // "-"
    { offsetof(SYSTEMTIME, wMonth), 2 },        // "{MM}"
    { -11, 1 },                                 // "-"
    { offsetof(SYSTEMTIME, wDay), 2 },          // "{DD}"
    { -16, 1 },                                 // " "
    // etc...  you get the idea
    { offsetof(SYSTEMTIME, wMilliseconds), 1 }, // "{mmm}"
    { -1, 0 },                                  // terminate
};

当您有 SYSTEMTIME 作为输入、指向原始格式字符串的指针、查找表和这个基本指令数组时,您可以继续将结果输出到一个预先确定大小的缓冲区非常快。

我相信你可以想出代码来有效地执行这些指令。

这种方法的主要缺点是查找表的大小可能会导致缓存问题。但是,大多数查找将发生在前 100 个元素中。您也可以将表格压缩为普通的char 值,然后在复制时注入wchar_t 零字节。

与往常一样:实验、测量并享受乐趣!

【讨论】:

  • 我一定会看看并尝试类似的东西。我会在一些测试后报告。谢谢!
  • 祝你好运,请回来告诉我们你从中获得了哪些性能提升(如果有的话)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-05-07
  • 2022-01-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-23
相关资源
最近更新 更多