使用Scala中的类型类模式会对性能产生什么影响?

13

我目前在代码的性能相关部分广泛使用类型类模式。我至少发现了两个潜在的低效问题。

  1. 隐式参数会伴随着消息调用一起传递。我不知道这是否会真的发生。也许scalac可以简单地将隐式参数插入到它们被使用的位置,并从方法签名中删除它们。但在手动插入隐式参数的情况下,这可能是不可能的,因为它们可能只会在运行时解析。关于传递隐式参数,有哪些优化应用

  2. 如果类型类实例由def提供(与val相反),则需要在每次调用“类型类方法”时重新创建对象。JVM可能会通过优化对象创建来解决此问题。Scalac也可以通过重用这些对象来解决此问题。关于创建隐式参数对象,有哪些优化应用

当应用类型类模式时,当然还可能存在其他低效源。请告诉我它们是什么。

1个回答

8
如果你真的关心编写超高性能的代码(而你可能认为自己确实关心,但是关于这一点非常错误),那么类型类将会引起以下问题:
  • 许多额外虚方法调用
  • 可能会装箱原始类型(例如,如果使用Scalaz的类型类来进行单子等操作)
  • 通过def创建对象是必要的,因为函数不能被参数化
  • 创建对象以访问“pimped”方法
在运行时,JVM可能会优化掉一些错误的创建(例如仅创建MA来调用<*>),但scalac并没有做太多来帮助。可以通过编译使用类型类的代码并将-Xprint:icode作为参数来轻松查看此情况。
下面是一个例子:
import scalaz._; import Scalaz._
object TC {
  def main(args: Array[String]) {
    println((args(0).parseInt.liftFailNel |@| args(1).parseInt.liftFailNel)(_ |+| _))
  }
}

以下是代码:

final object TC extends java.lang.Object with ScalaObject {
  def main(args: Array[java.lang.String]): Unit = scala.this.Predef.println(scalaz.this.Scalaz.ValidationMA(scalaz.this.Scalaz.StringTo(args.apply(0)).parseInt().liftFailNel()).|@|(scalaz.this.Scalaz.StringTo(args.apply(1)).parseInt().liftFailNel()).apply({
  (new anonymous class TC$$anonfun$main$1(): Function2)
}, scalaz.this.Functor.ValidationFunctor(), scalaz.this.Apply.ValidationApply(scalaz.this.Semigroup.NonEmptyListSemigroup())));
def this(): object TC = {
  TC.super.this();
  ()
}
};
@SerialVersionUID(0) final <synthetic> class TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$2 extends scala.runtime.AbstractFunction0 with Serializable {
  final def apply(): Int = TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$2.this.v1$1;
  final <bridge> def apply(): java.lang.Object = scala.Int.box(TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$2.this.apply());
  <synthetic> <paramaccessor> private[this] val v1$1: Int = _;
  def this($outer: anonymous class TC$$anonfun$main$1, v1$1: Int): anonymous class TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$2 = {
    TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$2.this.v1$1 = v1$1;
    TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$2.super.this();
    ()
  }
};
@SerialVersionUID(0) final <synthetic> class TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$1 extends scala.runtime.AbstractFunction0$mcI$sp with Serializable {
  final def apply(): Int = TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$1.this.apply$mcI$sp();
  <specialized> def apply$mcI$sp(): Int = TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$1.this.v2$1;
  final <bridge> def apply(): java.lang.Object = scala.Int.box(TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$1.this.apply());
  <synthetic> <paramaccessor> private[this] val v2$1: Int = _;
  def this($outer: anonymous class TC$$anonfun$main$1, v2$1: Int): anonymous class TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$1 = {
    TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$1.this.v2$1 = v2$1;
   TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$1.super.this();
  ()
  }
};
@SerialVersionUID(0) final <synthetic> class TC$$anonfun$main$1 extends scala.runtime.AbstractFunction2$mcIII$sp with Serializable {
  final def apply(x$1: Int, x$2: Int): Int = TC$$anonfun$main$1.this.apply$mcIII$sp(x$1, x$2);
  <specialized> def apply$mcIII$sp(v1$1: Int, v2$1: Int): Int = scala.Int.unbox(scalaz.this.Scalaz.mkIdentity({
(new anonymous class TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$2(TC$$anonfun$main$1.this, v1$1): Function0)
}).|+|({
    (new anonymous class TC$$anonfun$main$1$$anonfun$apply$mcIII$sp$1(TC$$anonfun$main$1.this, v2$1): Function0)
}, scalaz.this.Semigroup.IntSemigroup()));
final <bridge> def apply(v1: java.lang.Object, v2: java.lang.Object): java.lang.Object = scala.Int.box(TC$$anonfun$main$1.this.apply(scala.Int.unbox(v1), scala.Int.unbox(v2)));
  def this(): anonymous class TC$$anonfun$main$1 = {
    TC$$anonfun$main$1.super.this();
    ()
   }
 }

你可以看到这里有大量的对象创建过程。


所以,我从你的回答中读到的一个建议可能是用自己的专门版本替换我确实在使用的scalaz.Monoid?虽然专门化似乎非常有问题...甚至Numeric也没有专门化。 - ziggystar
说实话,我会避免使用专业化。如果我处在你的位置,我会非常、非常确定我确实需要从代码中挤出每一滴性能。你如何确定自己需要这样做呢?如果事情必须接近底层,那么我会建议你回到可变集合、命令式代码和while循环。真的没有其他答案。 - oxbow_lakes
它不一定非得尽可能快。但我不想承受装箱的性能影响。我目前正在使用原始数组(而不是突变它们)。关于这个问题,可以使代码非常通用,适用于大量的问题空间(我甚至使用代数环作为抽象)。目前我不想在性能(如装箱带来的10倍打击)和抽象之间做出选择;这不是一个容易的选择,我想知道我能同时兼顾多远。 - ziggystar
作为评论:你真的认为摆脱装箱是“从代码中挤出最后一滴性能”吗?装箱是一个相当大的性能损失。 - ziggystar
老实说,对于我所处理的工作负载来说,我并没有发现拳击是一个很重要的问题。 - oxbow_lakes
5
对于这种情况的通用答案是,首先以可维护、直观、灵活的方式编写代码,然后使用分析工具("profile")查看真正的瓶颈在哪里。然后再开始优化。这里有大量的对象创建吗?是的。那会比用C语言慢吗?是的。但它比原始的C代码更易于维护、理解和灵活。 - Dan Burton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接