【问题标题】:Does Java casting introduce overhead? Why?Java 强制转换会引入开销吗?为什么?
【发布时间】:2010-01-31 07:10:42
【问题描述】:

当我们将一种类型的对象转换为另一种类型时是否有任何开销?还是编译器只是解决所有问题而在运行时没有成本?

这是一般情况,还是有不同的情况?

例如,假设我们有一个 Object[] 数组,其中每个元素可能有不同的类型。但是我们总是可以肯定地知道,比如说,元素 0 是一个 Double,元素 1 是一个字符串。 (我知道这是一个错误的设计,但我们假设我必须这样做。)

Java 的类型信息在运行时是否仍然保留?或者编译后一切都忘记了,如果我们做 (Double)elements[0],我们只会跟随指针并将这 8 个字节解释为双精度,不管那是什么?

我非常不清楚 Java 中的类型是如何完成的。如果您对书籍或文章有任何建议,那么也谢谢您。

【问题讨论】:

标签: java performance casting static-typing


【解决方案1】:

有两种类型的投射:

隐式转换,当您从一个类型转换为更广泛的类型时,这是自动完成的,没有开销:

String s = "Cast";
Object o = s; // implicit casting

显式 转换,当您从较宽的类型转换为较窄的类型时。对于这种情况,您必须像这样显式使用强制转换:

Object o = someObject;
String s = (String) o; // explicit casting

在第二种情况下,运行时存在开销,因为必须检查这两种类型,并且如果强制转换不可行,JVM 必须抛出 ClassCastException。

取自JavaWorld: The cost of casting

Casting 用于在 types -- 在引用类型之间 特别是对于铸件的类型 我们感兴趣的操作 这里。

Upcast 操作(也称为 扩大Java中的转换 语言规范)转换 对祖先的子类引用 类参考。这个铸件 操作通常是自动的,因为 它总是安全的,可以 由编译器直接实现。

Downcast 操作(也称为 缩小 Java 中的转换范围 语言规范)转换 祖先类对子类的引用 参考。本次铸造作业 产生执行开销,因为 Java 要求检查演员表 运行时以确保它是有效的。 如果引用的对象不是 任一目标类型的实例 该类型的强制转换或子类, 不允许尝试的演员表 并且必须抛出一个 java.lang.ClassCastException。

【讨论】:

  • 那篇 JavaWorld 文章已有 10 多年的历史了,所以我会接受它关于性能的任何陈述。
  • 如果我不将转换的对象分配给引用并且只调用它的方法,情况是否相同?喜欢((String)o).someMethodOfCastedClass()
  • 现在这篇文章已经快 20 年了。答案也有很多年了。这个问题需要一个现代的答案。
  • 原始类型怎么样?我的意思是,例如 - 从 int 转换为 short 会导致类似的开销吗?
【解决方案2】:

对于Java的合理实现:

每个对象都有一个标头,其中包含一个指向运行时类型的指针(例如DoubleString,但它永远不可能是CharSequenceAbstractList)。假设运行时编译器(在 Sun 的情况下通常是 HotSpot)无法静态确定类型,则需要由生成的机器代码执行一些检查。

首先需要读取指向运行时类型的指针。无论如何,这对于在类似情况下调用虚方法是必要的。

对于转换为类类型,在点击java.lang.Object 之前确切知道有多少超类,因此可以从类型指针(实际上是 HotSpot 中的前八个)的恒定偏移量处读取该类型。同样,这类似于为虚拟方法读取方法指针。

然后读取的值只需要与预期的静态类型转换进行比较。根据指令集架构,另一条指令将需要在不正确的分支上进行分支(或故障)。像 32 位 ARM 这样的 ISA 有条件指令,也许可以让悲伤的路径通过快乐的路径。

由于接口的多重继承,接口更加困难。通常,接口的最后两个强制转换缓存在运行时类型中。在早期(十多年前),界面有点慢,但这已不再重要。

希望您能看到这类事情在很大程度上与性能无关。你的源代码更重要。就性能而言,您的方案中最大的打击可能是由于到处追逐对象指针而导致的缓存未命中(类型信息当然很常见)。

【讨论】:

  • 很有趣——这是否意味着对于非接口类,如果我写 Superclass sc = (Superclass)subclass; (jit ie:加载时间)编译器将“静态”在它们的“类”标头中的每个超类和子类中放入对象的偏移量,然后通过简单的添加+比较能够解决问题? - 又好又快 :) 对于接口,我认为不会比小哈希表或 btree 差?
  • @peterk 对于类之间的转换,对象地址和“vtbl”(方法指针表,加上类层次结构表,接口缓存等)都不会改变。所以 [class] 强制类型转换检查类型,如果它适合,则不需要发生任何其他事情。
【解决方案3】:

例如,假设我们有一个 Object[] 数组,其中每个元素可能有不同的类型。但是我们总是可以肯定地知道,比如说,元素 0 是一个 Double,元素 1 是一个字符串。 (我知道这是一个错误的设计,但我们假设我必须这样做。)

编译器不会记录数组中各个元素的类型。它只是检查每个元素表达式的类型是否可分配给数组元素类型。

Java 的类型信息在运行时是否仍然保留?或者编译后一切都忘记了,如果我们做 (Double)elements[0],我们只会跟随指针并将这 8 个字节解释为双精度,不管那是什么?

在运行时会保留一些信息,但不会保留各个元素的静态类型。您可以通过查看类文件格式来判断这一点。

理论上,JIT 编译器可以使用“转义分析”来消除某些赋值中不必要的类型检查。但是,按照您建议的程度执行此操作将超出实际优化的范围。分析单个元素类型的回报太小了。

此外,人们无论如何都不应该编写这样的应用程序代码。

【讨论】:

  • 原语呢? (float) Math.toDegrees(theta)这里也会有很大的开销吗?
  • 一些原始类型转换存在开销。是否重要取决于上下文。
【解决方案4】:

在运行时执行强制转换的字节码指令称为checkcast。你可以使用javap反汇编Java代码,看看生成了什么指令。

对于数组,Java 在运行时保存类型信息。大多数情况下,编译器会为您捕获类型错误,但在某些情况下,您会在尝试将对象存储在数组中时遇到ArrayStoreException,但类型不匹配(编译器不匹配)抓住它)。 Java language spec 给出了以下示例:

class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
    public static void main(String[] args) {
        ColoredPoint[] cpa = new ColoredPoint[10];
        Point[] pa = cpa;
        System.out.println(pa[1] == null);
        try {
            pa[0] = new Point();
        } catch (ArrayStoreException e) {
            System.out.println(e);
        }
    }
}

Point[] pa = cpa 有效,因为 ColoredPoint 是 Point 的子类,但 pa[0] = new Point() 无效。

这与泛型类型相反,泛型类型在运行时没有类型信息。编译器在必要时插入checkcast 指令。

泛型类型和数组类型的这种差异使得它通常不适合混合使用数组和泛型类型。

【讨论】:

    【解决方案5】:

    理论上,会引入开销。 但是,现代 JVM 很聪明。 每个实现都是不同的,但是假设可能存在一个实现,当它可以保证永远不会发生冲突时,JIT 优化了强制转换检查并不是不合理的。 至于哪些特定的 JVM 提供此功能,我无法告诉您。我必须承认我自己很想知道 JIT 优化的细节,但这些都是 JVM 工程师担心的。

    故事的寓意是首先编写可理解的代码。如果您遇到减速,请分析并确定您的问题。 很有可能不是因为演员阵容。 永远不要为了优化它而牺牲干净、安全的代码,直到你知道你需要这样做。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-12-28
      • 1970-01-01
      • 2019-03-12
      • 2021-05-28
      • 2012-02-10
      • 1970-01-01
      • 2012-09-04
      • 1970-01-01
      相关资源
      最近更新 更多