65.9K
CodeProject 正在变化。 阅读更多。
Home

g2log:一个高效的 C++11 异步日志记录器。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (43投票s)

2011年11月23日

公共领域

25分钟阅读

viewsIcon

383020

downloadIcon

1206

不要让缓慢的磁盘访问拖累你的日志记录器。通过使用 g2log 异步日志记录器,你将消除等待瓶颈,同时它拥有“传统”日志库的可靠性。

g2log/CodeProjectAsynchronousvsSynchronous.jpg

引言

g2log 旨在成为一个简单、高效且易于理解的异步日志记录器。g2log 的核心只有几个短文件,并且应该很容易修改以满足你的需求。它包含日志记录、契约式设计 CHECK 宏,以及对 SIGSEGV(非法内存访问)和 SIGFPE(浮点错误)等致命信号的捕获和日志记录。它是跨平台的,在 Linux 和 Windows 上都经过测试。

g2log 与其他日志工具的区别在于它是异步的。通过使用 Active Object 模式,g2log 在后台进行缓慢的磁盘访问。LOG 调用是异步的,因此 g2log 提高了应用程序性能。

异步 Google glog (v.0.3.1) 的比较表明,g2log 效率更高,尤其是在最坏情况下。

我将本次演示分为两部分

  1. #[第 1 部分] G2log 异步日志 解释了 g2log 的工作原理。简要介绍了用于 LOG 调用的 API 及其一些内部机制。如果你是一名好奇的开发人员,想了解它的工作原理,那么你应该阅读这一部分。代码示例保持简短,但少数几个短文件可以很容易地在 https://bitbucket.org/KjellKod/g2log/srcg2log/src 中浏览。

  2. #[第 2 部分] 性能:g2log 与 Google 的 glog 演示了一个非常高效的、伪异步但仍是同步的日志实用程序(Google 的 glog)与一个异步日志记录器(g2log)的比较。性能部分表明,异步 g2log 效率更高,尤其是在最坏情况下。任何对 g2log 性能感兴趣的人都可以单独阅读本节。

g2log 是由许多优秀的软件大师建议的代码技术构建块构建而成。我只是将这些点连接起来。可能还有其他免费的异步日志记录器,但在撰写本文时,我尚未遇到过。这就是我与大家分享这段代码的原因。这是我对社区的贡献,也是对我在个人、文章和博客中获得的所有巨大帮助的感谢。当然,考虑到这一点,将 g2log 作为公共领域奉献免费提供是理所当然的。

本文的一个版本最初发布于 www.kjellkod.cc

目录

  1. 引言
  2. [第 1 部分] g2log:异步日志记录
  3. 1 g2log 简介
  4. 2 为什么同步日志记录器传统上优于异步日志记录器
  5. 2.1 g2log 如何满足崩溃要求
  6. 3 使用 g2log
  7. 3.1 初始化
  8. 3.2 关机时刷新
  9. 3.3 日志级别
  10. 3.3.1 日志级别示例
  11. 3.4 条件日志记录
  12. 3.5 流式 API
  13. 3.6 Printf-like API
  14. 3.7 契约式设计(又名断言编程)
  15. 4 影响
  16. 5 要求
  17. 6 代码
  18. 7 为什么和如果
  19. [第 2 部分] 性能:g2log 与 Google 的 glog
  20. 8 简介:性能比较
  21. 8.1 实际的 LOG 调用
  22. 9 比较的理由
  23. 10 事实和数据
  24. 11 伪异步的简化描述
  25. 12 性能比较
  26. 12.1 平均 LOG 调用时间
  27. 12.2 拥塞时的平均 LOG 调用时间
  28. 12.3 最大 LOG 调用时间
  29. 12.4 全貌
  30. 13 后台线程:总时间 - 改进
  31. 结论
  32. 14 性能结论
  33. 15 g2log 总结与反思
  34. 参考文献
  35. 变更历史

[第 1 部分] g2Log:异步日志记录

g2log 简介

g2log 是一个异步日志工具,旨在高效且易于使用、理解和修改。创建 g2log 的原因很简单,我研究的其他日志软件在 API 或效率方面都不够好。

在 API 方面,我对我尝试的一些日志工具的调用不满意。它们过于冗长,使代码看起来杂乱无章。

在效率方面,我坚信,每当出现缓慢的文件/磁盘/网络访问时,都应该尽最大努力在后台线程中处理它。我对所有尝试过的日志软件都感到失望,因为它们是串行(同步)的,即 LOG 调用在日志调用者可以继续之前被写入文件,这显然会降低日志调用者的速度。使用同步日志记录器有充分的传统[#原因],但我相信 g2log 满足了这些原因,同时仍然是异步的。

响应迅速是我所从事软件的关键要求。由于 LOG 调用而减慢线程的速度是不够的。因此,我决定创建异步 g2log。

对于那些感兴趣的人,而非出于煽动的原因,我调查并发现不足的日志记录器是:Google 的 glogezloggerlog4Cplus

要理解 g2log 的精髓,只需阅读以下几个重点

  1. 所有缓慢的 I/O 磁盘访问都在后台线程中完成。这确保了软件的其他部分在创建日志条目后可以立即继续其工作。
  2. g2log 提供日志记录、契约式设计 [#CHECK],以及在关机时将日志刷新到文件。
  3. 它是线程安全的,因此从多个线程使用它完全没问题。
  4. 它捕获 SIGSEGV 和其他 致命信号(不是 SIGINT),并在退出前将其记录下来。
  5. 它是跨平台的。目前,已在 Windows7 (VS2010) 和 ubuntu 11.10 (gcc 4.6) 上进行测试。Linux 和 Windows 版本之间存在微小差异。在 Linux 上,捕获到的致命信号将在日志中生成堆栈转储。由于 Windows 在遍历堆栈时的复杂性,此功能在 Windows 上不可用。
  6. 自2011年初以来,g2log 已用于商业产品和业余项目。
  7. 代码以公共领域形式免费提供。这允许用户更改、使用和做任何他们想做的事情,没有任何附加条件。

就是这样,g2log 的精髓。

旁注g2 是第一个使用 g2log 的商业项目中的一个关键词。它代表第二代 (g2),因此命名很容易。g2log API 的一个灵感来源恰好名为 glog,这只是一个巧合。

为什么同步日志记录器传统上优于异步日志记录器

日志记录器的一个要求通常是在软件继续执行下一个逻辑代码指令之前将日志条目写入文件,存储在磁盘上。传统上,这意味着只能使用同步日志记录器,因为它似乎保证会直接写入文件。

在继续之前,要求生成的日志条目已写入文件,这在调试崩溃的应用程序时很常见。从现在开始,我们称之为崩溃要求

对于开发人员来说,至关重要的是要知道所有信息在采取下一步(可能是致命的)步骤之前都已记录在日志中。缺点是同步、缓慢的日志记录会影响性能。

g2log 如何满足崩溃要求

崩溃处理要求通过使用信号处理程序来解决。你可以在 https://bitbucket.org/KjellKod/g2log/src 查看,请浏览到 g2log/src/ 并查看 crashhandler.h 以及特定于操作系统的 crashhandler_win.cppcrashhandler_unix.cpp

信号处理程序将捕获常见的由操作系统或 C 库触发的致命信号,这些信号将导致应用程序终止。当捕获到致命信号时,g2log 会向后台工作者发送一条消息,告知它处理致命事件。然后,调用线程会休眠,直到后台工作者完成。同时,后台工作者正在按 FIFO 顺序处理消息。

当后台工作者收到(FIFO 队列中的)致命事件消息时,它会将其写入文件,然后继续使用原始信号终止应用程序。这样,在致命事件消息之前的所有 FIFO 队列中的日志消息都将在崩溃完成之前写入文件。

对于崩溃要求,当应用程序被致命信号终止时,g2log 仍然优于同步日志记录器。性能会很好,同时还能处理将所有写入的日志刷新到文件。

使用 g2log

g2log 使用级别特定的日志记录。这在不减慢软件日志调用部分的情况下完成。由于活跃对象的概念,g2log 实现了异步日志记录——实际的日志记录工作,包括缓慢的磁盘 I/O 访问,都在后台线程中完成。

与其他在调用线程中执行 I/O 的日志工具相比,g2log 的日志性能增益可能非常巨大。这在下面的[#性能比较]页面中有所展示,我将非常出色的 Google glog 库与 g2log 进行了比较。Google 的 glog 我称之为伪异步,因为它可以在实际上是同步日志记录器的情况下伪装异步行为。显而易见的是,g2log 的平均时间提高了 48%。在最坏的情况下,g2log 比 glog 快 10-35 倍

g2log 根据您的偏好提供流式语法和printf风格的语法。流式 API 与其他日志工具和库非常相似,因此您在使用时会感到得心应手。

初始化

使用 g2log 的典型场景如下所示。在启动时,在主函数体内,g2logWorker 使用日志前缀和日志文件路径进行初始化。一个好的规则是使用 argv[0] 作为日志文件前缀,因为那将是启动软件的名称。

#include "g2log.h"
int main(int argc, char** argv)
{
g2logWorker g2log(argv[0], "/tmp/whatever-directory-path-you-want/");
g2::initializeLogging(&g2log);
// ....

示例程序 g2log-example 将根据规则 prefix.g2log.YYYYMMDD-HHMMSS.log/tmp/ 生成一个日志文件,例如 g2log-example.g2log.20111114-092342.log

关机时刷新

在应用程序软件关闭时,g2logWorker 将超出作用域。这将触发 g2logWorker 正在使用的活跃对象的销毁。在活跃对象销毁之前,任何待定的日志写入都将刷新到日志文件。这样,就不会丢失任何日志条目。

Active::~Active() {
  Callback quit_token = std::bind(&Active::doDone, this);
  send(quit_token); // tell thread to exit, this is the last message to be processed in FIFO order
  thd_.join();      // after join is done, all messages are processed
}

日志级别

可用的日志级别包括:INFODEBUGWARNINGFATAL。这些级别在软件中是固定的,但如果需要可以轻松更改。可以从 g2log/src/g2log.h 的第一行轻松添加或删除这些级别。

通过使用 C 预处理器宏进行标记连接,级别本身用于调用相应的函数。

#define LOG(level) G2_LOG_##level.messageStream()

拼写错误或使用不存在的日志级别将导致编译器错误。

LOG(UNKNOWN_LEVEL) << "This log attempt will cause a compiler error";

The compiler error will express something like:
>> ...
>> 'G2_LOG_UNKNOWN_LEVEL' was not declared in this scope
>> ...

对于不存在或拼写错误的日志级别,连接将导致调用一个不存在的函数。这将生成编译错误。

由于安全地使用了 C 预处理器宏,API 简洁直接。

日志级别示例

FATAL 具有特殊含义。使用日志级别 FATAL 的含义与在 [#契约式设计] CHECK 失败时的评估结果相同。

#include "g2log.h"
int main(int argc, char** argv)
{
    g2logWorker g2log(argv[0], "/tmp/whatever-directory-path-you-want/");
    g2::initializeLogging(&g2log);
    
    LOG(INFO) << "Simple to use with streaming syntax, easy as ABC or " << 123;
    LOGF(WARNING, "Printf-style syntax is also %s", "available");
    
    LOGF(FATAL, "This %s is FATAL. After log flush -> Abort()", "message");
    // or using the stream API
    LOG(FATAL) << "This message is FATAL. After log flush -> Abort()";


}

条件日志记录

提供了条件日志记录。在特定条件下进行日志记录时,条件日志记录非常方便。

LOG_IF(INFO, (1 < 2)) << "If  " << 1 << "<" << 2 << " : this text will be logged"; // or 
LOGF_IF(INFO, (1<2), "if %d<%d : then this text will be logged", 1,2);

// : if 1<2 : then this text will be logged

当然,条件日志记录可以与FATAL日志级别一起使用,而不是使用[#CHECK]。如果条件评估不为 true,则忽略FATAL级别和消息。

LOG_IF(FATAL, (2>3)) << "This message is not FATAL";
LOG_IF(FATAL, (2<3)) << "This message is FATAL";

流式 API

流式 API 使用普通的 C++ std::ostringstream,便于流式传输字符串、原生类型(intfloat等)。流式 API 不存在 printf-type API 存在的格式风险

LOG(DEBUG) << "Hello I have " << 1 << " car";
LOG(INFO) << "PI is: " << std::setprecision(6) << PI;

printf-like API

在 g2log 的第一个版本中,我被说服将类似 printf 的语法添加到 g2log 中。这被实现为一个可变参数函数,并带有与 printf-like 函数相关的常见风险。至少,类似 printf 的日志记录通过 vsnprintf 受到缓冲区溢出保护。

类似 printf 的 API 对某些人仍然很有吸引力,主要是因为其美观的文本和数据分离。我希望在 Windows 支持可变参数模板时转向使用它们。

如果决定使用类似 printf 的 API,调用方式会有所不同。API 调用将更改为:LOGF、条件 LOGF_IF 和契约式设计 CHECKF

LOGF(DEBUG, "This API is popular with some %s", "programmers");
LOGF_IF(DEBUG, (1 != 2), "if true, then this %s will be logged", "message");
CHECKF(foo(), "if 'false == foo()' then the %s is broken: FATAL), "contract");

在 Linux 上可以减轻类似 printf 的 API 的风险。通过使用 -Wall 编译器标志可以生成错误语法的编译器警告。

const std::string logging = "logging"; 
LOGF(DEBUG, "Printf-type %s is the number 1 for many %s", logging.c_str());

上面的日志调用格式不正确。它有两个 %s,但只有一个字符串参数。启用 gcc 编译器和 -Wall 标志后,编译器将生成类似于 warning: format "%s" expects a matching "char*" argument [-Wformat] 的警告。

为了安全起见,我个人更喜欢在 Linux 和 Windows 上都使用流式 API。

契约式设计(又名断言编程)

通过断言进行早期错误检测是常见的编程实践。代码中的条件被检查,如果条件不满足,应用程序将被中止。这是契约式设计的重要组成部分,有时被称为断言编程

最常见的是使用各种 CHECK 宏来验证条件,如果 CHECK(condition) 失败,则退出应用程序。g2log 为流式和 printf-like API 都提供了 CHECK 功能。

CHECK(1 != 2); // true: won't be FATAL
CHECK(1 > 2) << "CHECK(false) will trigger a FATAL message, put it on log, then exit";

或者使用类似 printf 的语法

const std::string arg = "CHECKF";
CHECKF(1 > 2, "This is a test to see if %s works", arg.c_str());

影响

g2log 的流式 API 以及用于通过编译器检查日志级别创建日志 API 的宏连接可以在其他日志工具中找到。类似的日志用法可以在 Petru Marginean 在 Dr. Dobbs 的日志文章 [4] 和 [5] 以及 Google 的 glog [6] 中找到。

如果你读过我之前的博客,或者已经浏览过 g2log 的代码,那么 g2log 受到了以下影响和启发就不足为奇了

  • Petru 的文章是最初的影响。一如既往,他的文章富有启发性,让人想去探索 [4] 和 [5]。
  • Google 的 glog [6] 是一个出色的日志库,也许是世界上最好的之一。如果不是 glog 缺乏异步日志功能,那么 g2log 可能永远不会存在。
  • Herb Sutter 的有效并发博客。
  • 最后但并非最不重要的是,Anthony Williams 的 C++ Concurrency in Action 电子书 [16] 和博客 [17] a,这让我想要立即用 C++11 做点什么。

要求

g2log 的 1.0 版本随本文发布。构建它需要兼容 C++11 的编译器或 just::threadstd::thread 实现和 CMake。在 Linux 上,我使用了 gcc4.6;在 Windows 上,我使用了 Visual Studio 2010 Express 和 Visual Studio 11 (beta)。

对于 Windows 用户,从 Visual Studio 11 开始,您可以原封不动地使用 g2log,无需 just::thread,因为 Visual Studio 11 包含 C++11 std::thread。

对于 Linux 用户,目前最好的选择是使用 just::threadstd::thread 实现。gcc4.7 带有 std::thread,一位读者在成功设置 gcc4.7 并使用 g2log 后联系了我。他的初步测试表明它有效,但设置(gcc)很痛苦。

如果你没有 gcc 4.7,也不打算购买 just::thread 的 std::thread 实现,但仍然想使用这个日志工具,那么你仍然没有陷入困境。std::thread 可以轻松替换为你平台上的任何线程库(Qt、MFC、boost、pthread)。你需要解决的代码更改主要在 src/shared_queue.hsrc/active.h/.cpp 中。

g2log 的线程部分被封装在一个活跃对象中。我以前使用 QThread、pthread 等做过类似的活跃对象。如果你无法访问 std::thread,那么这些可能会有所帮助。代码可在 https://github.com/KjellKod/active-object 获取。

代码

我为 g2log 建立了一个 BitBucket 仓库。您可以在那里访问这些文件:https://bitbucket.org/KjellKod/g2log/src。您可以使用命令 hg clone https://bitbucket.org/KjellKod/g2log 进行 mercurial 下载。另一种选择是使用可能较旧的快照,该快照应随本文附带。

如果您有任何改进建议或发现需要更正的地方,请告诉我,我将尽力将其与代码的其余部分整合。

为什么和如果

  • g2log 为什么使用古老的 C 语言继承的 varargs 而不是可变参数模板.
    可变参数模板当然应该在未来使用。我只是在等待它在 Visual Studio 上得到支持。在撰写本文时,它还没有,我希望它在 Linux 和 Windows 上尽可能相似。

  • 为什么 g2log 没有使用这个或那个 boost 库?
    我希望 g2log 成为一个小型、独立的工具,除了属于 C++11 标准的库之外,不使用太多外部库。just::thread 的 std::thread 库是一个 C++11 库实现。std::thread 目前已经在 beta/实验性的 gcc 4.7 中可用。

  • 有时你写的是 Linux 和 Windows,但你的意思是 Windows 上的 Visual Studio 2010 和 Linux 上的 gcc 4.6 吗?.
    是的,当然。这样简化更容易。当然,在 Windows 上使用 gcc 也是可以的。

  • 为什么日志级别不能在运行时禁用/启用?
    根据一位读者的请求,我在 BitBucket 上创建了一个 g2log 分支来实现这一点。这可能稍后会合并到 g2log 主分支中,但您当然现在就可以使用它。您可以在以下位置找到它:https://bitbucket.org/KjellKod/g2log-dynamic-levels

  • 为什么 shared_queue 包装的是 std::queue 而不是 std::dequestd:deque 不更快吗?
    std::queue 是 FIFO 且 shared_queue 被 Active Object 以严格的 FIFO 顺序使用来看,这是有道理的。我测试了性能差异,对于我用 std::deque 运行的测试(2 个线程记录 200 万条日志),std::vector 在平均情况和最坏情况下的表现都略快。

    在涉及更高数据负载到队列的压力测试中,std::deque 可能会更快,我只是没有测试过,目前我仍然使用 std::queue。如果需要,更改很容易进行,只需修改 shared_queue.h 中的几行。


  • 如果我的软件创建的日志条目超出了 g2log 能够保存到我非常非常慢的磁盘的能力,你能给出什么保证?
    嗯。没有。怎么样?如果你的软件过度调度 LOG 调用,记录了太多的日志,以至于后台 g2log 工作者无法跟上,那么会发生以下情况:

    1 你的应用程序软件将继续运行,并相当快地将日志推送到消息队列。它仍然响应迅速。如果你改用同步日志记录器,它就不会那么响应迅速,并且在其大部分执行时间内都会停滞。

    2 共享消息队列将继续增长,消耗越来越多的内存。最终,如果日志过度调度持续进行,就会发生糟糕的事情,因为所有的内存都不够用。

    简而言之:如果日志记录器的使用完全失控,那么就会发生失控的事情。如果使用异步日志记录器,软件仍将在一段时间内保持响应。如果使用同步日志记录器,您将大部分时间处于停滞状态。在这两种情况下,您的硬盘都可能被填满。根据您的系统,磁盘空间不足的情况比内存不足更可能发生。至少在我的笔记本电脑上,在一些[#极端性能测试]期间就是这样发生的。

[第 2 部分] 性能:g2log 与 Google 的 glog

在下面的性能部分,我将 g2log 与一个[#伪异步]日志记录器:Google 的 glog 版本 0.3.1 进行了比较。如果这对您来说不太重要,我建议您只需浏览一下或直接转到[#结论]。

简介:性能比较

glog 是一个很好的比较对象,因为它平均速度非常快,甚至可能是最快的同步日志记录器,这要归功于它的伪异步行为。尽管 glog 非常高效,但 g2log 在平均情况和最坏情况下都超越了它。

本次性能比较绝不是为了诋毁 Google 的 glog。恰恰相反。我相信 Google 的 glog 是一款出色的软件,它提供的功能可能永远比 g2log 多得多。本次性能比较的目的是将 g2log 与一个非常好的同步日志记录器进行对比测试。通过这次比较,我希望性能差异以及差异的原因会变得显而易见。我也希望能够清楚地说明何时异步日志记录器真正更可取。

实际的 LOG 调用

由于 g2log 同时使用 printf 和流式 API,但它们的性能相似,因此我只展示使用与 glog 完全相同的流式 LOG 调用 g2log 的测试。

LOG(INFO) << title << " iteration #" << count << " " 
  << charptrmsg << strmsg << " and a float: " << std::setprecision(6) << pi_f;

比较的理由

在 Google glog 的论坛上,我注意到异步 glog 的请求无人回应。部分原因是我决定创建 g2log。

很久之后,我收到了一位软件工程师的联系,他注意到了我在论坛上的异步功能请求。他对 glog(引用)“糟糕的性能”也感到沮丧,自然对异步 g2log 很感兴趣。我不知道他现在是否正在使用 g2log,但感谢他表达了对性能改进的需求,我决定包含这份性能比较。

事实和数据

此处使用的所有数据均可在在线电子表格中查看。性能测试易于重现,因为它们随 g2log 代码提供。我还附上了性能测量日志的副本,以防有人需要访问。

测试在一台 Dell Latitude E6500 笔记本电脑上进行,配备 Intel Core2 Duo CPU P8600 2.40GHz x2 和固态硬盘。在另一台使用标准硬盘的系统上,同步(glog)和异步(g2log)之间的差异会更大。

伪异步的简化描述

伪异步性能的关键在于 glog 缓冲日志条目。每个缓冲的 LOG 调用都非常快,因为避免了缓慢的磁盘访问。在某些触发器下,或当缓冲区满时,会执行刷新到磁盘操作。这导致最新日志条目因许多先前条目的磁盘 I/O 访问时间而受到惩罚。

先缓冲再刷新的方法比真正的同步日志记录器具有更好的平均性能。然而,时不时地,性能会受到打击,因为所有日志条目都会一次性写入文件。

性能比较

如前所述,g2log 在后台管理磁盘访问 I/O。这意味着进行日志条目的调用将导致一个字符串异步转发给后台工作者。工作者会适时将文本保存到文件。

除了 g2log 是异步的之外,没有进行任何实际优化。这可以在下面显示,其中两个日志工具的平均时间有时差异不大。在最坏的情况下,当 I/O 等待时间达到峰值时,glog 就会受苦而 g2log 就会大放异彩。

平均 LOG 调用时间

在平均情况下,两者之间并没有太大的差异。伪异步行为使得 glog 在 1 个线程创建 100 万条日志条目的测试运行中取得了良好的结果。下面使用的数据可以在在线性能图表的“1 百万条目选项卡”中查看。

每个调用平均应用程序时间 [微秒]:1 个线程创建 100 万个日志条目
g2log glog g2log 比 glog 提升百分比
9.56 11.113 13.97%

1 thread, continously writing 1 million log entries

拥塞时的平均 LOG 调用时间

很明显,总时间(参考:在线电子表格)比 glog 花费的时间要长。总时间是从 LOG 调用开始到所有 LOG 调用都写入文件的时间。

这本应如此:g2log 不缓冲日志条目,即使一次写入多个日志条目也不会,而是后台线程一有日志条目就将其写入文件。这是 g2log 后台线程优化的一个方面。然而,它的重要性也较低——重要的是要保持应用程序时间低。减慢 LOG 调用者的将是应用程序等待 LOG 调用完成的时间。

增加争用时,Google glog 的平均时间会恶化。时不时地,线程将不得不长时间等待,因为线程安全缓冲区被清空并写入文件。

每次调用的平均应用程序时间,以微秒 [us] 为单位,4 个线程,每个线程创建 100 万个日志条目
g2log [us], glog [us], g2log 比 glog 提升百分比
6.52 12.45 47.63%

4 threads, each continously writing 1 million log entries

显然,一旦出现拥塞,g2log 在平均情况下甚至优于伪异步日志记录器。如果不在后台进行,缓慢的 I/O 肯定会惩罚软件。

最大 LOG 调用时间

由于线程切换、互斥锁开销等,g2log 肯定会有一些峰值等待时间。使用不同线程数的测试设置,可以清楚地看到这些峰值时间如何随数据负载增加。与 glog 相比,g2log 的峰值时间相形见绌。g2log 的峰值时间比 glog 的峰值时间低 10 到 35 倍 [在线电子表格选项卡:最大时间]。

最大和峰值时间(以毫秒 [ms] 为单位)如何随线程数增加。每个线程写入 100 万个日志条目。
线程 g2log [ms], glog [ms], g2log 比 glog 性能提升倍数
1 13 459 35.31
2 28 920 32.86
4 42 925 22.02
8 66 914 13.85
16 113 1065 9.42

1 to 16 threads, continously writing 1 million log entries

全貌

为了真正展示峰值时间如何影响性能,下面使用了散点图。也许这张图对于同步日志用户来说是一个很好的警醒?[在线电子表格选项卡:完整图表比较]

2 threads, scatter chart showing every LOG call (application time)

下面显示的是相同的数据,但以面积图的形式呈现。结尾的红色噪音是峰值时间。尽管它可能看起来微不足道,但它们代表了总时间消耗的很大一部分。

2 threads, scatter chart showing every LOG call (application time)

后台线程:总时间 - 改进

此处未显示但在在线电子表格中可用的,是后台日志记录器的总时间。在当前的实现中,g2log 后台线程一旦有日志条目可用,就会将其写入磁盘。这当然效率不高,因为每次新的磁盘访问操作都很慢。

如果出于某种原因,后台线程的总磁盘 I/O 时间必须减少,则可以进行优化。一个简单的优化是在后台线程侧使用 glog 的方案。通过在后台缓冲日志条目,并在触发器(时间或缓冲区满)时刷新,总磁盘 I/O 时间将显著减少。

结论

性能结论

很明显,KjellKod 的 g2log Google 的 glog 更,无论是在测量平均时间还是测量最坏情况时。这更多地与 glog 本质上是(部分)同步而 g2log 是异步有关。在平均情况下,Kjellkod 的 g2log 比 Google 的 glog 快 10-48%。在最坏情况下,g2log 比 glog 快 10-35 倍

伪异步 glog 比传统同步日志记录器效率高得多。然而,峰值时间令人担忧和沮丧。

测试是在一台配备固态硬盘的笔记本电脑上进行的。在另一台使用标准硬盘,磁盘访问速度更慢的系统上,同步(glog)和异步(g2log)之间的差异会相当大。

使用同步日志记录器的含义是,有时 I/O 等待时间会很长。使用同步日志记录器可能会长时间暂停日志调用线程。这通常是不希望的,因为响应迅速是正常的软件设计目标。

G2log 的平均时间小于 glog 的平均时间。在最坏情况下,异步 g2log 相对于同步 glog 具有巨大的优势。

g2log 总结与反思

我只能猜测 Google 的公开版本 glogezloggerlog4Cplus 为什么没有实现真正的异步日志记录。这确实是日志记录器的传统方法,但如果你仔细思考,将缓慢的磁盘访问操作放在后台进行几乎是愚蠢的。这在上面的性能比较部分中清楚地显示出来。

根据 glog 所有者 Shinichi 在 glog 论坛 google-glog/issues/detail?id=55 上的评论,Google 内部使用的 glog 确实可能是异步的

一个尚未讨论的可能原因是这些日志记录器是在 C++11 之前(std::thread 不可用,而且将它们制作成跨平台的多线程可能需要太多的精力)。另一个可能的原因是这些日志记录器的作者选择不将他们的日志记录器与第三方线程库(如 MFC、Qt 或 Boost)紧密绑定。

无论如何,C++11 已经到来。g2log 是免费的。通过 just::thread 实现或符合 C++11/std::thread 的编译器(Visual Studio 11)使用它。另外,您可以简单地将封装在活动对象内的线程部分替换为您选择的线程库。还在等什么?快去获取吧 :)

感谢您阅读我的文章。希望您可以直接使用 g2log,或者使用其中的一部分,或者只是从中获得一些灵感。

参考文献

  1. Kjellkod 博客:程序员的糖果
  2. Kjellkod 博客:契约式设计 & 为什么不也来一个快速而粗糙的日志记录器?!
  3. KjellKod.cc:C++11 方式的活跃对象
  4. Dr.Dobbs,Petru Marginean 的文章:C++ 中的日志记录
  5. Dr.Dobbs,Petru Marginean 的文章:C++ 中的日志记录:第 2 部分
  6. Google 的 glog 日志库
  7. StackOverflow 上关于流、日志的各种问答。多到无法一一列举。
  8. 维基百科:未受控制的格式字符串
  9. gcc.gnu.org 标准预定义宏
  10. gcc.gnu.org 作为字符串的函数名
  11. cppreference.com printf 格式
  12. unixwiz.net __attribute__
  13. codemaestro.com C/C++ 中的可变参数
  14. Herb Sutter 高效并发:优先使用活跃对象而非裸线程
  15. C++11 std::thread 实现来自 just::thread (by Anthony Williams)
  16. Anthony Williams 的书:C++ 并发编程实战
  17. Anthony Williams 博客:使用条件变量实现线程安全队列
  18. 维基百科,契约式设计类型:断言(计算机)
  19. Apache 列出的C++11 编译器支持
  20. StackOverflow,gcc 堆栈转储
  21. gcc.gnu.org 符号解构
  22. Suavcommunity,信号处理 Unix/Windows 移植
  23. IBM Windows/Unix 信号处理
  24. Tutorialpoints C++ 信号处理
  25. TinyMicros Linux 溢出信号处理程序示例

变更历史

  • 2011年11月22日:CodeProject 初始版本。g2log 为 v.1.0。
  • 2011年12月3日:细微更改:更正了文本和图片的格式。澄清了文本的选定部分。
  • 2011年12月7日:更新了文章概述,添加了 zip 下载,更新了格式并添加了介绍图片。
  • 2011年12月8日:更新了内容概述,少量文本更改和语法更正。
  • 2012年3月4日 - 2012年3月5日:由于 Visual Studio 11,just::thread 在 Windows 上不再是必需的。更新了源代码、BitBucket 并回滚以更正损坏的目录。
© . All rights reserved.