Java日志API的开销问题

3

我已经了解了一些使用Java记录调试消息的方法,由于我的背景是C语言,我的关注点如下:

这些库声称在禁用日志记录的情况下(例如生产环境)具有最小的开销,但由于它们的log()函数的参数仍然会被评估,因此我的担忧是,在实际情况下,开销实际上并不可忽略。

例如,log(myobject.toString(), "info message")仍然具有评估myobject.toString()的开销,这可能非常大,即使日志函数本身什么也不做。

有人有解决这个问题的办法吗?

PS:对于那些想知道我为什么提到C语言背景的人:C语言允许您使用预处理器宏和编译时指令,在编译时完全删除与调试相关的所有代码,包括宏参数(根本不会出现)。

编辑: 阅读了第一批答案后,似乎Java显然没有任何可以解决这个问题的东西(考虑在每个CPU位都很重要的移动环境中在一个大循环中记录一个数字的余弦值)。所以我会选择基于IDE的解决方案。我最后的选择是构建类似于“查找所有/替换”宏的东西。 我最初认为,也许从一个面向方面的框架中获取的一些东西会有所帮助... 有人吗?

5个回答

9
我认为log4j FAQ做了很好的解答:

For some logger l, writing,

l.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));

incurs the cost of constructing the message parameter, that is converting both integer i and entry[i] to a String, and concatenating intermediate strings. This, regardless of whether the message will be logged or not.

If you are worried about speed, then write

 if(l.isDebugEnabled()) {
     l.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
 }

This way you will not incur the cost of parameter construction if debugging is disabled for logger l. On the other hand, if the logger is debug enabled, you will incur the cost of evaluating whether the logger is enabled or not, twice: once in debugEnabled and once in debug. This is an insignificant overhead since evaluating a logger takes less than 1% of the time it takes to actually log a statement.

使用守卫条款是避免在此处进行字符串构建的一般方法。

其他流行的框架,例如slf4j,采用使用格式化字符串/参数化消息的方法,以便只有在需要时才评估消息。


最后一行加1。参数化参数始终是一个选项。 - Tim Bender
@Tim Bender,假如你没有任何基本类型。 - Peter Lawrey
谢谢你的回答。参数化消息对我来说不太适用,因为有时需要构建复杂的表达式(如计算),我真的不想在生产环境中进行这些操作。我会等一段时间看看是否有人提出了神奇的解决方案,因为我仍然觉得 Java 没有一种完全没有开销的方法来实现它是不可思议的... - Ben G
在任何编程环境中,如何在没有任何开销的情况下完成?为了传递到函数中,需要解析表达式/变量。您可以传递函数指针,但这似乎有些繁琐。在Java中,您可以通过传递一个匿名函数来实现,该函数扩展Object并返回toString()所需的表达式(有条件地)进行日志记录,但这会使您的代码变得非常丑陋。 - matt b
看看 C 预处理器和宏,你会惊讶的。另外,看看我的“EDIT”评论。 - Ben G

4
答案很简单:不要在日志调用本身中调用昂贵的方法。如果无法避免,请在日志调用周围使用守卫。
 if(logger.isDebugEnabled()) {
    logger.debug("this is an "+expensive()+" log call.");
 }

正如其他人指出的那样,如果您的日志框架支持格式化(即,如果您使用的是足够现代的框架,应该是 所有 框架,但并非如此),您应该依赖它来帮助减轻记录点的开销。如果您选择的框架 不支持 格式化,那么要么切换框架,要么编写自己的包装器。


那就不用它呗?我的意思是,嘿,你可以选择使用什么日志框架以及如何使用;我自己不使用守卫语句,因为如果底层结构不支持格式化语句的延迟评估,我会使用一个包装器来记录日志。 - Joseph Ottinger
抱歉如果你觉得那是一次攻击,我并不是有意要这样做。我只是想添加一条评论,以便大家知道这不是一个被普遍喜爱的方法;-) - Joachim Sauer
当然,没问题。我并不觉得被攻击了;我只是在重申Ceki Gulcu关于守卫条件的党派立场。我并不太尊重大多数日志框架,因为它们实际上相当不灵活,而且很有趣的是,Ceki - 顺便说一句,他是个好人 - 现在正在进行第四次尝试编写一个全面的日志框架。 - Joseph Ottinger
@matt:是的,我知道,但我们公司的编码标准要求在 if/while/... 上加块(我也同意这一点!)。仅为日志语句更改标准会阻碍对这些标准的工具支持。总的来说,我认为格式化解决方案更加优雅(但从投票结果来看,我在少数派)。 - Joachim Sauer
@Joachim:至少不是我这样做。如果我使用日志框架(通常情况下我不会),我会使用具有格式化功能的框架(希望与printf的语法匹配)或编写自己的包装器。但再说一遍:日志记录,呃。就像缓存一样:一旦你开始使用它们,你真的在弥补一个不应该存在的缺陷。 - Joseph Ottinger
显示剩余3条评论

4
现代日志框架具有变量替换功能。因此,您的日志记录类似于以下内容:
log.debug("The value of my first object is %s and of my second object is %s", firstObject, secondObject).

给定对象的 toString() 方法只会在日志记录设置为 debug 时执行。否则,它将忽略参数并返回空值。

2

你说得对,评估log()调用的参数可能会增加不必要的开销,可能会很昂贵。

这就是为什么大多数合理的日志框架也提供了一些字符串格式化函数,这样你就可以编写像这样的代码:

log.debug("Frobnicating {0}", objectWithExpensiveToString);

这样,您唯一需要考虑的是调用debug()。如果该级别未激活,则不会执行任何操作;如果已激活,则会解释格式字符串,调用objectWithExpensiveToString()上的toString(),并将结果插入到格式字符串中,然后记录日志。
有些日志语句使用MessageFormat风格的占位符({0}),其他一些使用format()风格的占位符(%s),还有一些可能采用第三种方法。

-1

你可以使用一种有趣的方式 - 尽管有点冗长 - 来进行断言。打开断言时,会有输出和开销,关闭断言时则没有输出和绝对没有开销。

public static void main(String[] args) {
    assert returnsTrue(new Runnable() {
        @Override
        public void run() {
            // your logging code
        }
    });
}

public static boolean returnsTrue(Runnable r) {
    r.run();
    return true;
}

需要returnsTrue()函数,因为我不知道更好的方法使表达式返回true,而assert需要一个布尔值。


assert 不需要括号,我认为没有括号更清晰。因为这样看起来像一个方法调用,但实际上它不是。 - Joachim Sauer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接