如果编译器可能内联日志记录调用,为什么还要在记录API时使用lambda表达式

许多日志框架(例如,log4j)允许您将lambda表达式而不是String s传递给日志记录API。 参数是如果字符串对构造特别有表现力,那么字符串结构可以通过lambda表达式延迟执行。 这样,只有在系统的日志级别与调用的日志级别匹配时才构造字符串。

但是,鉴于现代编译器会自动执行大量内联方法,是否真的有必要以这种方式使用lambda表达式? 我将在下面提供一个简化示例来certificate这一问题。

假设我们的传统日志记录方法如下所示:

 void log(int level, String message) { if (level >= System.logLevel) System.out.println(message); } // .... System.logLevel = Level.CRITICAL; log(Level.FINE, "Very expensive string to construct ..." + etc); 

让我们假设FINE小于CRITICAL ,因此,尽管构造了一个昂贵的字符串,但由于没有输出消息,所以不是。

Lambda日志记录API可以帮助解决这种情况,以便在必要时仅对字符串进行评估(构造):

 void log(int level, Supplier message) { if (level >= System.logLevel) System.out.println(message.apply()); } // .... System.logLevel = Level.CRITICAL; log(Level.FINE, () -> "Very expensive string to construct ..." + etc); 

但是,编译器可以内联日志记录方法是可行的,因此净效果如下:

 System.logLevel = Level.CRITICAL; if (Level.FINE >= System.logLevel) System.out.println("Very expensive string to construct..." + etc); 

在这种情况下,我们不必在记录API调用之前评估字符串(因为没有),并且可能,我们将从内联中获得性能。

总之,我的问题是,鉴于编译器可能内联记录API调用,lambda表达式如何在这种情况下帮助我们? 我唯一能想到的是,不知何故,在lambda情况下,如果日志记录级别不匹配,则不评估字符串。

您的优化不仅仅引入了内联 – 它改变了排序。 这通常不是有效的。

特别是,除非JIT能够certificate这些方法没有其他效果,否则更改方法是否被调用是无效的。 如果JIT编译器内联并重新排序到那个程度,我会非常惊讶 – 检查构造方法参数所涉及的所有操作没有副作用的成本在大多数情况下可能不值得。 (JIT编译器无法以不同于其他方法的方式处理日志记录方法。)

因此,尽管真正,非常聪明的JIT编译器可以做到这一点,但我会惊讶地发现任何实际上都是这样做的。 如果你发现自己正在使用一个,并编写测试来certificate这种方法并不比使用lambda表达式更昂贵,并且继续certificate随着时间的推移,这很好 – 但听起来你更愿意假设情况如此,我肯定不会。

Raffi让我们看看一个例子,说明你正在讨论的内联编译器将如何改变程序逻辑,编译器需要非常聪明才能解决这个问题:

  public String process(){ //do some important bussiness logic return "Done processing"; } 

1)无需内联,无论日志级别如何,都将调用process()

 log( Level.FINE, "Very expensive string to construct ..." + process() ); 

2)内联process()将仅在特定日志级别下调用,并且我们的重要业务逻辑将无法运行:

 if (Level.FINE >= System.logLevel) System.out.println("Very expensive string to construct..." + process() ); 

在这种情况下,编译器必须弄清楚如何创建消息字符串,如果在创建过程中调用任何其他方法,则不要内联该方法。

这种优化内联仅适用于您提供的非常简单的示例(当它只是String连接时)。

实际上,这个API可以以更复杂的方式使用:

  public void log(Level level, Supplier msgSupplier) 

假设我有一个专门的供应商,它执行一个非常昂贵的日志消息产生:

  Supplier supplier = () -> { // really complex stuff }; 

然后我在几个地方使用它:

 LOGGER.log(Level.SEVERE, supplier); ... LOGGER.log(Level.SEVERE, supplier); 

然后,你会内联什么? 将其打包成内嵌

 System.logLevel = Level.CRITICAL; if (Level.FINE >= System.logLevel) System.out.println(supplier.get()); 

没有任何意义。

正如它在java.util.logging.Logger类JavaDoc中所说:

记录消息,只有在日志记录级别实际记录消息时才构建消息。

所以这是一个目的:如果你可以避免构造,那么你不需要执行这些计算并将结果作为参数传递。