从 {fmt} 中获得最佳性能
Getting the best performance out of {fmt}
我需要将 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));
我完全理解服务是有代价的 :) 但我的代码调用此语句数百万次并且性能损失明显存在(超过 CPU 使用率的 6%)。
"Anything"欢迎我改进这段代码。
我看到 {fmt} 有一个 time API support。
不幸的是,它似乎无法格式化 time/date 的毫秒部分,并且需要一些从 FILETIME
到 std::time_t
...
的转换工作
我是否应该忘记 "custom" 格式字符串并为 FILETIME
(或 SYSTEMTIME
)类型提供自定义格式化程序?这会导致显着的性能提升吗?
如果您能提供任何指导,我将不胜感激。
在评论中我建议将您的自定义时间格式字符串解析为一个简单的状态机。它甚至不必是这样的状态机。它只是一系列线性指令。
目前,fmt
class 需要做一些工作来解析格式类型,然后将整数转换为 zero-padded 字符串。尽管不太可能,但它有可能像我将要建议的那样进行了大量优化。
基本思想是进行(大)查找 table,当然可以在运行时生成,但为了快速说明:
const wchar_t zeroPad4[10000][5] = { L"0000", L"0001", L"0002", ..., L"9999" };
如果需要,您可以进行 1 位、2 位和 3 位查找 table,或者识别这些值都包含在 4 位查找 table 中,如果您只需添加一个偏移量。
所以要输出一个数字,你只需要知道 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
作为输入、指向原始格式字符串的指针、查找 table 和这个基本指令数组时可以继续并很快将结果输出到 pre-sized 缓冲区。
我相信您可以想出有效执行这些指令的代码。
这种方法的主要缺点是查找的大小 table 可能会导致缓存问题。但是,大多数查找将发生在前 100 个元素中。您还可以将 table 压缩为普通的 char
值,然后在复制时插入 wchar_t
零字节。
一如既往:实验、测量、玩得开心!
我需要将 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));
我完全理解服务是有代价的 :) 但我的代码调用此语句数百万次并且性能损失明显存在(超过 CPU 使用率的 6%)。
"Anything"欢迎我改进这段代码。
我看到 {fmt} 有一个 time API support。
不幸的是,它似乎无法格式化 time/date 的毫秒部分,并且需要一些从 FILETIME
到 std::time_t
...
我是否应该忘记 "custom" 格式字符串并为 FILETIME
(或 SYSTEMTIME
)类型提供自定义格式化程序?这会导致显着的性能提升吗?
如果您能提供任何指导,我将不胜感激。
在评论中我建议将您的自定义时间格式字符串解析为一个简单的状态机。它甚至不必是这样的状态机。它只是一系列线性指令。
目前,fmt
class 需要做一些工作来解析格式类型,然后将整数转换为 zero-padded 字符串。尽管不太可能,但它有可能像我将要建议的那样进行了大量优化。
基本思想是进行(大)查找 table,当然可以在运行时生成,但为了快速说明:
const wchar_t zeroPad4[10000][5] = { L"0000", L"0001", L"0002", ..., L"9999" };
如果需要,您可以进行 1 位、2 位和 3 位查找 table,或者识别这些值都包含在 4 位查找 table 中,如果您只需添加一个偏移量。
所以要输出一个数字,你只需要知道 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
作为输入、指向原始格式字符串的指针、查找 table 和这个基本指令数组时可以继续并很快将结果输出到 pre-sized 缓冲区。
我相信您可以想出有效执行这些指令的代码。
这种方法的主要缺点是查找的大小 table 可能会导致缓存问题。但是,大多数查找将发生在前 100 个元素中。您还可以将 table 压缩为普通的 char
值,然后在复制时插入 wchar_t
零字节。
一如既往:实验、测量、玩得开心!