Skip to content

Latest commit

 

History

History
121 lines (87 loc) · 7.04 KB

specialized_types.adoc

File metadata and controls

121 lines (87 loc) · 7.04 KB

Specialized Types

@specialized

比起"类型系统",Type specialization 实际上更像是性能技巧, 但是如果你想要写出性能良好的集合那么它是非常重要的,值得用心牢记。 在我们的例子中,我们将实现一个非常有用的集合,叫做 Parcel[A],能够保存一个指定类型的值 - 这是多么有用!

case class Parcel[A](value: A)

这就是我们的基本实现。那么缺点在哪里呢?因为 A 可以是任何类型,即使我们只放入一个 Int ,它也是被表示为 Java object。所以上面的类只能处理 objects,对于原始类型需要装箱和拆箱。

val i: Int = Int.unbox(Parcel.apply(Int.box(1)))

正如我们所知道 - 不必要的装箱并不是一个好主意,因为它在运行时需要更多的工作量,来回转换 intobject Int。 有什么可以解决这个问题呢?在这里能应用的一个技巧是为所有的原始类型 "specialize 我们的 Parcel 类(比如现在只需要 Long 和 Int),正如这样:

Note
如果你已经读过 Value Class 一节,你可能会注意到 Parcel 可以轻松使用它们来实现。 这的确是事实。不过,Scala 从 2.8.1 就引入了 specialized,而在 2.10.x 才引入 Value Classes。 而且,你可以专门化一个以上的值(尽管这会*指数级地*增加产生的字节码),而使用 Value Classes 你就被限制只能使用单个值。
case class Parcel[A](value: A) {
  def something: A = ???
}

// "手动" specialzation
case class IntParcel(intValue: Int) {
  override def something: Int = /* 基于底层 Int,没有包装! */ ???
}

case class LongParcel(intValue: Long) {
  override def something: Long = /* 基于底层 Long,没有包装! */ ???
}

IntParcelLongParcel 的实现能有效的避免装箱,因为他们直接使用原始类型,而没有触及到对象帝国。 现在取决于我们的使用,我们必须手动选择使用哪个 Parcel

这样的实现很不错但是…​如果有 N 个实现,包含每个我们想支持的原始类型(可以是 intlongbytecharshortfloatdoublebooleanvoid 加上 Object 的任意类型),那么我们需要维护很多样板代码。

既然我们熟悉了 specialization 的概念,我们不需要手动实现,来看看 Scala 是如何通过引入 @specialized 注解帮助我们的:

case class Parcel[@specialized A](value: A)

因为我们给类型参数 A 加上了 @specialized 注解, 这告诉编译器要为这个类生成所有 specialized 变体 - 即 ByteParcelIntParcelLongParcelFloatParcelDoubleParcelBooleanParcelCharParcel,甚至 VoidParcel(这些并不是实际实现的名字,但是你应该明白背后的想法)。 编译器还负责选择"正确"的类型,尽可能使用 specialized 版本(如果有的话),所以我们写代码时可以不关心这个类是否是 specialized:

val pi = Parcel(1)     // 会使用 `int` specialized 方法
val pl = Parcel(1L)    // 会使用 `long` specialized 方法
val pb = Parcel(false) // 会使用 `boolean` specialized 方法
val po = Parcel("pi")  // 会使用 `Object` 方法

"太棒了,我们可以在任意的地方使用!" — 这是人们发现 specialiation 的常见反应,因为它可以数倍加快低级别操作速度,同时降低内存使用率。 不幸的是,我们需要很高的代价:如果对多个参数使用该注解,那么生成的代码就会变得非常庞大:

class Thing[A, B](@specialized a: A, @specialized b: B)

在上面的例子中,我们使用了 specialization 的第二种应用方式 - 将注解加在参数前 - 效果等价于我们直接 specialize AB。 请注意到上面的代码会产生 8 * 8 = 64 种实现,这是因为编译器要处理 "A 是一个 int,B 是一个 int`" 以及 "A 是一个 `boolean,但是 B 是一个 long`" 这些情况 - 你应该明白编译器做了什么。 事实上,最终生成的类的数量大约在 `2 * 10^(nr_of_type_specializations),对于三个类型参数,很容易就产生了上千个类。

当然有办法限制这种指数增长,比如限制需要 specialization 的目标类型。 比如说我们的 Parcel 多数情况都用于整数类型,从不用于浮点数 - 我们可以告诉编译器只为我们生成 Long 和 Int 类型的:

case class Parcel[@specialized(Int, Long) A](value: A)

让我们使用 :javap Parcel 看看生成的字节码:

// Parcel, 为 Int 和 Long 专门化
public class Parcel extends java.lang.Object implements scala.Product,scala.Serializable{
    public java.lang.Object value(); // 泛型版本, "处理其他所有情况"
    public int value$mcI$sp();       // int specialized 版本
    public long value$mcJ$sp();}     // long specialized 版本

    public boolean specInstance$();  // 检查我们是否在使用 specialized 类的实现
}

正如你所看到的,编译器为我们准备了额外的 specialized 方法, 比如 value$mcI$sp() 返回一个 intvalue$mcJ$sp() 返回一个 long。 另一个值得提起的方法是 specInstance$,如果使用的是 specialized 类,那么该方法返回 true

如果你好奇的话,当前这些类在 Scala 中会被 specialized(这个列表可能不完整):Function0,Function1,Function2,Tuple1,Tuple2,Product1,Product2,AbstractFunction0,AbstractFunction1,AbstractFunction2。 尽管可以 specialized 两个以上的参数,但是由于开销太大,人们通常不这么做。

Warning
我们要避免装箱的一个主要原因还包含内存效率。想象一个 boolean, 如果能在内存中使用一个 bit 存储它该多好呀。 悲伤的是这并不是事实(我所知道的任何 JVM),比如说在 HotSpot 中,一个 boolean 是用 int 表示的,那么它需要占据 4 个字节的空间。 另一个方面,它的表兄弟 java.lang.Boolean 需要 8 个字节存储对象头,正如每个 Java 对象都需要,在内部存储 boolean(需要额外 4 字节), 然后由于 Java 对象布局对齐规则(Object Layout Alignment Rules),这个对象所占据的空间会被对齐到 16 个字节(8 个字节存储对象头,4 个字节存储值,4 个字节对齐)。 这就是为什么我们非常想避免装箱的另一个原因。

Miniboxing

Warning
Miniboxing 不是 Scala 的一个特征,但是可以作为*编译器插件*与 scalac 一起使用。

我们在前一节解释到 specialization 是非常强大的,但同时也是"编译器炸弹",存在指数增长的可能性。 现在已经有一个解决办法了,那就是 Mibiboxing。Miniboxing 是一个编译器插件,能够实现与 @specialized 相同的功能,但不会生成上千个类。

Warning
TODO EPFL 有一个项目可以使得 specialiation 更高效 Scala Miniboxing