繁体   English   中英

减少条件跟踪/记录调用的开销

[英]Overhead reduction of conditional trace/logging calls

为了跟踪和调试Java代码,我使用了一个简单的Util类,而不是一个成熟的日志框架:

public class Util {
    public static int debugLevel;

    public static void info(String msg) {
      //  code for logfile output
      //  handling of unprintable characters
      //  etc. omitted 
      System.out.println(msg);
    }

    public static void info4(String msg) {
        if (debugLevel >= 4) {
           info(msg);
        }
    }
}

这允许紧凑的单行语句,如下所示:

info4(String.format("%16s: ", host) + Util.toHex(sidNetto));

使用debugLevel变量,我可以控制程序的详细程度。 通常,调试级别是在执行开始时全局设置的。 但是它也可以在常规级别上进行本地调整。

基本上,我将重复的if (debugLevel >= DEBUG_ALL) {...}括号保存在跟踪调用周围。 但是,无论调试级别如何,都必须在运行时准备并传递调用的参数。

我的问题:

我怎样才能轻推编译时优化器或JVM来删除多余的跟踪调用? 我在考虑C/C++函数内联。

这里讨论了有关C#一个相关问题。 但是我不确定如何将建议的答案移植到Java 2010年的另一篇相关文章讨论了类似的方法。 我想知道是否真的需要像ProGuard这样的第三方工具来解决这样一个常见的任务。

大多数日志记录框架就是这样做的。 对于轻量级参数(最好是包括内置格式化程序),请不要检查级别,否则请在序列化复杂的字符串参数之前检查级别。

您可以使用Java 8 java.util.functions.Supplier<String>进行花边评估,但是我认为在显式级别的测试用例上可能无法获得任何性能。

记录器如下所示:

void debug(String ptrn, Supplier<String> args...)

您可以像这样使用它:

debug("Hello {0}", this::getName());

我知道的大多数日志记录API建议在实际调用log方法之前先检查是否启用了日志级别,以防必须先准备消息,例如:

if (logger.isTraceEnabled()) {
    String msg = String.format("Name changed from %s to %s", oldName, newName);
    logger.trace(msg);
}

一些日志记录API(例如SLF4J)还提供了更复杂的日志记录方法,这些方法接受格式字符串和多个参数,因此仅在启用日志级别的情况下才生成日志消息:

logger.trace("Name changed from {} to {}", oldName, newName);

在大多数情况下,这已经足够了,但是有时您的消息构建起来会更复杂,或者必须先将参数转换为字符串。 在这种情况下,检查日志级别仍然是一种好方法。

从Java 8开始,您还可以利用Lambda表达式解决此问题。 您的日志方法可以这样实现:

public void log(Supplier<String> messageSupplier) {
    if (isLogEnabled()) {
        String msg = messageSupplier.get();
        // TODO: log msg
    }
}

如您所见,仅在启用日志记录的情况下,才从messageSupplier检索消息。 多亏了lambda表达式,实现Supplier<String>非常容易:

logger.log(() -> String.format("Name changed from %s to %s", oldName, newName));

更新(感谢Joshua Taylor)

从Java 8开始,java.util.logging API已经支持消息提供者,例如,请参阅Logger#info ,因此您可以通过JRE的“内置”解决方案轻松地交换日志记录实现。

由于它们的复杂性,似乎不使用已建立的日志记录框架很奇怪,但是担心诸如方法内联之类的较小优化,却忽略了格式化日志字符串的更大问题,而与日志级别无关。 但是,如果您坚持重新发明轮子:

JVM(至少是Oracle热点JVM)自动内联短方法,并对无法访问的分支执行无效代码消除。 要检测为无法到达,消息的日志级别和级别阈值必须是恒定的(编译时常数或静态最终值)。 否则,JVM仍将比较每次调用的日志记录级别,尽管它仍可能执行推测性内联(内联通常采用的分支,由条件分支指令保护),以确保仅在异常情况下执行分支指令。

但是,更令人担忧的是构建日志消息的成本,只有在必须实际记录该消息的情况下才产生该消息。 在准备消息之前,要求调用代码检查是否已启用日志记录的旧log4j方法相当冗长,很容易被遗忘。 相反,SLF4J通过使log方法采用格式字符串和可变数量的要插入占位符的对象,将字符串连接推迟到日志记录系统。 SLF4J常见问题解答写道

以下两行将产生完全相同的输出。 但是,在禁用日志记录语句的情况下,第二种形式的性能将比第一种形式高至少30倍。

 logger.debug("The new entry is "+entry+"."); logger.debug("The new entry is {}.", entry); 

值得注意的是,参数(此处为entry )的类型为Object ,因此仅当实际上必须记录消息时才将其转换为String

需要明确的是,没有可靠的方法可以通过重新定义方法来跳过对方法参数的求值,因为只有在即时编译器可以证明该求值没有副作用的情况下,这种消除才可能发生,热点jvm仅在其发现时才进行检测已内联整个评估,仅用于非常简单的评估。 因此,将格式转移到日志记录系统中的API解决方案可能是您所希望的最好的解决方案。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM