【问题标题】:is it possible to disable javac's inlining of static final variables?是否可以禁用 javac 的静态最终变量内联?
【发布时间】:2010-08-19 16:53:09
【问题描述】:

Java 静态编译器 (javac) 内联了一些静态最终变量,并将这些值直接带入常量池。考虑以下示例。 A类定义了一些常量(public static final variables):

public class A {
    public static final int INT_VALUE = 1000;
    public static final String STRING_VALUE = "foo";
}

B 类使用这些常量:

public class B {
    public static void main(String[] args) {
        int i = A.INT_VALUE;
        System.out.println(i);
        String s = A.STRING_VALUE;
        System.out.println(s);
    }
}

当你编译 B 类时,javac 从 A 类中获取这些常量的值并将这些值内联到 B.class 中。结果,在编译时必须对类 A 的依赖项 B 从字节码中删除。这是一种相当奇特的行为,因为您在编译时输入了这些常量的值。你会认为这是 JIT 编译器在运行时可以做的最简单的事情之一。

是否有任何方法或任何隐藏的编译器选项可以让您禁用 javac 的这种内联行为?作为背景,我们正在研究出于依赖性目的进行字节码分析,这是字节码分析无法检测编译时依赖性的少数情况之一。谢谢!

编辑:这是一个令人烦恼的问题,因为通常我们不会控制所有源(例如定义常量的第三方库)。我们有兴趣从使用常量的角度检测这些依赖关系。由于引用已从使用常量的代码中删除,因此没有简单的方法来检测它们,除非进行源代码分析。

【问题讨论】:

  • 好问题!!!有时这种常量内联会导致奇怪的问题......但我还没有看到一个适当的方法来限制它。

标签: java dependencies bytecode javac


【解决方案1】:

Java Puzzlers (Joshua Bloch) 的第 93 项说,您可以通过防止最终值被视为常量来解决此问题。例如:

public class A {
  public static final int INT_VALUE = Integer.valueOf(1000).intValue();
  public static final String STRING_VALUE = "foo".toString();
}

当然,如果您无权访问定义常量的代码,这一切都无关紧要。

【讨论】:

  • @DJClayworth:很好。没想到 :) 对于大多数类型,可能存在可行的方法(例如String.valueOf()
  • 我只记得System.out的赋值是这样的:public final static PrintStream out = nullPrintStream();(实际流是后面本地代码设置的)。如果currentTimeMillis()>0nullPrintStream() 返回 null。直到现在我才知道他们为什么这样做(而不仅仅是...out = null;)。当然,如果我费心阅读nullInputStream() 的文档,那会很清楚......
  • 无法在编译时禁用此优化是javac 削弱创建适当调试器能力的众多方式之一。
  • @MarkPeters 这是深奥的优化之一,我喜欢 Java 核心开发人员。由于 PrintStreamnull 引用类型的对象都不能是编译时常量,因此这种内联只能引用 JIT,但 JIT 并不关心赋值之前值的来源。请注意,从 Java 7 开始,代码确实看起来像 public final static PrintStream out = null;.
  • 关于可能真正是编译时常量的字段,就像在这个答案中一样,使用避免不必要的方法调用的public static final int INT_VALUE; public static final String STRING_VALUE; static { INT_VALUE = 1000; STRING_VALUE = "foo"; } 会更有效,更不用说@987654334 的装箱开销@.
【解决方案2】:

我不相信。最简单的解决方法是将这些公开为属性而不是字段:

public class A {
    private static final int INT_VALUE = 1000;
    private static final String STRING_VALUE = "foo";

    public static int getIntValue() {
        return INT_VALUE;
    }
    public static String getStringValue() {
        return STRING_VALUE;
    }
}

不要忘记,在某些情况下,内联对于值的使用是必不可少的——例如,如果您要在 switch 块中使用 INT_VALUE 作为 case,则 具有被指定为一个常数值。

【讨论】:

  • 乔恩,你对为什么 JLS 建议访问者有任何见解,但不建议私下使用 final 修饰符(见我的回答)?
  • @Mark - 如果私有成员是 final (结果可以在编译时确定),是否不可能内联访问器函数,本质上是重新创建 @987654324 @ 设想? (我的推测性推理)
  • @Tim:我的直觉是函数只能在运行时内联。否则,您创建的任何存根方法 (return null;) 都需要您在实现该方法后重新编译所有使用它的类。
  • @Mark - 啊,当然。那里有点精神失常,对此感到抱歉。 :)
  • @Mark - 一个原因是没有代码设置(私有)字段使得final 模型冗余......从纯粹的语言意义上。 (风格考虑另有说明,但 JLS 并不想成为一本风格手册。)
【解决方案3】:

要停止内联,您需要使值成为非编译时常量(JLS 术语)。您可以在不使用函数的情况下执行此操作,并通过在初始化表达式中使用 null 创建最少的字节码。

public static final int INT_VALUE = null!=null?0: 1000;

虽然它的代码生成非常直接,javac 应该将其优化为推送一个立即整数,然后存储到静态初始化器中的静态字段。

【讨论】:

  • 1000+0 怎么样。但这会在案例陈述中起作用吗?
  • @Tuntable 1000+0 是一个编译时常量。它与1000 没有什么不同,所以它可以在case 语句中工作,但它也可以内联。答案中的magicnull 不是编译时常量,因此也没有使用它的表达式。
  • 如果你想确保编译器只是生成一个赋值,你可以简单地这样写,public static final int INT_VALUE; static { INT_VALUE = 1000; }
  • @Holger 虽然这是生成完全相同的字节码的更清晰的方法,但我认为还有更多的缺点。 null!=null?0: 清楚地表明了做某事的意图,因此不太可能被清洁工拔掉。虽然这个成语并不明显,但应该很快就能知道结果是什么,即使不是 compile-time constant 语义*。重新格式化不会完全破坏它。您不必匹配标识符(它们匹配吗?不是吗?这是一个奇怪的拼写吗?)。
  • 我没有坚持把它放在一行;这就是 cmets 的格式。因此,如果格式化程序将其分成多行,我可以接受。一个清洁工,足够聪明,可以理解null!=null 是有意义的,在理解编译时常量和初始化器中分配的变量之间的区别应该没有问题。由于未立即分配的static final 变量必须在静态初始化程序中分配并且必须只分配一次,因此编译器将对其进行检查。这是初始化器不适合一个表达式的标准习惯用法。
【解决方案4】:

JLS 13.4.9 处理这个问题。他们的建议是,如果值可能以任何方式发生变化,则基本上避免使用编译时常量。

(需要内联的一个原因 常量是switch语句 在每种情况下都需要常量,并且没有 两个这样的常数值可能是 相同的。编译器检查 在开关中重复常量值 编译时的声明;班上 文件格式不做符号 案例值的链接。)

避免问题的最佳方法 “不定常数”在 广泛分布的代码是声明 作为编译时常量仅值 这真的不太可能 改变。除了真实 数学常数,我们推荐 该源代码非常节省使用 声明的类变量 静态和最终。如果只读 最终的性质是必需的,一个更好的 选择是声明一个私有静态 变量和合适的访问器 方法来获取它的值。因此我们 推荐:

private static int N;
public static int getN() { return N; }

而不是:

public static final int N = ...;

没有问题:

public static int N = ...;

如果 N 不需要是只读的。

【讨论】:

    【解决方案5】:

    我认为这是一个严重错误。 Java 不是 C/C++。有一个原则(或没有)“一次编译,到处运行”。

    在这种情况下,当 A 类更改时。任何引用 A.CONST_VALUE 的类都必须重新编译,它们几乎不知道 A 类是否被更改。

    【讨论】:

    • 这不是错误,而是一项功能。它确保如果您在另一个模块中的对象中有一个可能甚至没有加载的常量,因此当您请求 BANANA 时,它不会同时获取 Monkey 类和丛林包。 Java提供反射和序列化(以及其他)以避免缓存,但没有显式强制内联的机制,所以我很高兴它实际上内联它(如果你删除带有常量的实用程序类,使用这些常量的类仍然可以工作)。其他解决方案提出了仅通过使用非编译时间常量来实现它的方法。
    • @Tuntable 看起来很简单,当您只考虑读取 static 字段而不是使用值时。但是想象一下,当它应该支持编译后的更改时,应该如何编译另一个类的常量上的 switch 语句。同样,该语言在代码可访问性方面也很严格。允许在编译后更改常量会破坏语言安全性。并且强制所有对常量的使用都使用常量而不是读取动态值是一致的。
    • @Tuntable javac 产生字节码。该字节码必须包含转换表的编译形式,无论它看起来如何。现在告诉编译后的表单应该是什么样子,如果在编译后允许更改键值。您说的是“哈希表”,但哈希码也取决于实际值,因此不允许更改键值。不要试图用你的“可选参数”来转移话题。 C++ 或 C# 是否允许 switch 语句的键值在编译后更改?我不这么认为。
    • @Tuntable switch 语句中的字符串仍然需要 constant 标签。这就是 switch 的工作原理。如果你想要“一个用表达式初始化的数组”或者一些可以处理改变键的散列结构,那么,只需实现它。 “有很多方法” 但是不要责怪 Java 没有做与写入源代码完全不同的事情。
    • @Tuntable 好吧,我应该说“这就是 Java 的 开关的工作原理”。它以这种方式工作,并且需要不断的标签。是的,其他语言支持其他结构。但这从来都不是话题。主题是 Java 中的常量字段。方式,switch 在 Java 中工作,方式,注释在 Java 中工作,方式,代码可达性检查在 Java 中工作,强制不变,resp。复制常量值,即使只是隐式的。更改这些并不像简单的字段读取那么简单。这才是重点。强制代码流检查比switch 语句更有趣。
    【解决方案6】:

    像这样重写A类:

    public class A {
        public static final int INT_VALUE;
        public static final String STRING_VALUE;
    
        static {
            INT_VALUE = 1000;
            STRING_VALUE = "foo";
        }
    }
    

    【讨论】:

      【解决方案7】:

      jmake 是一个开源项目,声称可以完成跟踪 Java 文件之间的依赖关系和增量编译所需的最小文件集的全部工作。它声称可以正确处理对静态最终常量的更改,尽管有时需要重新编译整个项目。它甚至以比类文件更精细的粒度来处理更改;如果(例如)方法 C.m() 的签名发生变化,那么它只会重新编译实际依赖于 m() 的类,而不是所有使用 C 的类。

      免责声明:我没有使用 jmake 的经验。

      【讨论】:

      • 死链接,似乎是一个死项目。
      【解决方案8】:

      我最近遇到了一个类似的issue,正如上面所说,这种内联可以使用非编译时表达式来解决,比如:

      public final class A {
      
          public static final int INT_VALUE = constOf(1000);
          public static final String STRING_VALUE = constOf("foo");
      
      }
      

      constOf 方法族仅仅是:

      // @formatter:off
      public static boolean constOf(final boolean value) { return value; }
      public static byte constOf(final byte value) { return value; }
      public static short constOf(final short value) { return value; }
      public static int constOf(final int value) { return value; }
      public static long constOf(final long value) { return value; }
      public static float constOf(final float value) { return value; }
      public static double constOf(final double value) { return value; }
      public static char constOf(final char value) { return value; }
      public static <T> T constOf(final T value) { return value; }
      // @formatter:on
      

      这比 Integer.valueOf(1000).intValue()null!=null?0: 1000 等其他建议短一些

      【讨论】:

        【解决方案9】:

        我觉得 java 非常依赖动态编译,它不像 C++ 那样做任何花哨的编译逻辑。

        您可以尝试使用 JIT 编译器的一些选项来进行运行时优化,这些选项可能有一些选项来禁用/启用它。

        在默认 javac 中,您可能无法获得该选项。你必须使用 1. 某种类型的依赖图,如扩展或实现 2. 使用基于方法的链接。

        -s

        【讨论】:

        • 我不确定你的意思...这严格来说是 javac(静态编译器)行为,它似乎是由语言规范决定的。
        猜你喜欢
        • 2018-08-23
        • 1970-01-01
        • 1970-01-01
        • 2022-01-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多