【问题标题】:What is the performance impact of Scala implicit type conversions?Scala 隐式类型转换对性能有何影响?
【发布时间】:2011-09-16 22:31:42
【问题描述】:

在 Scala 中,与其他可能的实现选择相比,使用隐式类型转换来增强类的功能是否会对 CPU 或内存产生重大影响?

例如,考虑一个愚蠢的字符串操作函数。此实现使用字符串连接:

object Funky {
  def main(args: Array[String]) {
    args foreach(arg => println("Funky " + arg))
  }
}

此实现通过使用隐式类型转换隐藏了成员方法后面的串联:

class FunkyString(str: String) {
  def funkify() = "Funky " + str
}

object ImplicitFunky {
  implicit def asFunkyString(str: String) = new FunkyString(str)

  def main(args: Array[String]) {
    args foreach(arg => println(arg.funkify()))
  }
}

两者都做同样的事情:

scala> Funky.main(Array("Cold Medina", "Town", "Drummer"))        
Funky Cold Medina
Funky Town
Funky Drummer

scala> ImplicitFunky.main(Array("Cold Medina", "Town", "Drummer"))
Funky Cold Medina
Funky Town
Funky Drummer

有任何性能差异吗?一些具体的考虑:

Scala 是否内联对 asFunkyString 方法的隐式调用?

Scala 是否真的为每个 arg 创建了一个新的包装 FunkyString 对象,或者它可以优化掉额外的对象分配?

假设 FunkyString 有 3 种不同的方法(funkify1、funkify2 和 funkify3),并且 foreach 的主体依次调用每个方法:

println(arg.funkify1())
println(arg.funkify2())
println(arg.funkify3())

Scala 会重复转换 3 次,还是会优化掉多余的转换,每次循环迭代只做一次?

假设我在另一个变量中显式捕获转换,如下所示:

val fs = asFunkyString(arg)
println(fs.funkify1())
println(fs.funkify2())
println(fs.funkify3())

这会改变情况吗?

实际上,隐式转换的广泛使用是潜在的性能问题,还是通常无害?

【问题讨论】:

  • ... 我在唱:“♫♩♬ 你为什么不直接测试一下?♫♩♬ 测量一下?♫♩♬ 做一百万次随机播放♫♩♬ "
  • 由于原生代码和隐式代码之间的区别是如此缓慢,我们现在可以假设 scala 不会为每个调用创建一个新的包装器对象吗?因为在其他地方,这就是我听到的,但我不确定。也能回答您的其他问题会很有趣...

标签: performance scala implicit-conversion


【解决方案1】:

JVM 可以优化掉额外的对象分配,如果它检测到这是值得的。

这很重要,因为如果您只是内联事物,您最终会得到更大的方法,这可能会导致缓存性能问题,甚至会降低 JVM 应用其他优化的机会。

【讨论】:

    【解决方案2】:

    我尝试使用出色的Scala-Benchmark-Template 设置微基准。

    编写一个仅测试隐式转换的有意义(非 JIT 优化)基准测试非常困难,因此我不得不增加一些开销。

    代码如下:

    class FunkyBench extends SimpleScalaBenchmark {
      val N = 10000
      def timeDirect( reps: Int ) = repeat(reps) {
        var strs = List[String]()
        var s = "a"
        for( i <- 0 until N ) {
          s += "a"
          strs ::= "Funky " + s 
        }
        strs
      }
      def timeImplicit( reps: Int ) = repeat(reps) {
        import Funky._
        var strs = List[String]()
        var s = "a"
        for( i <- 0 until N ) {
          s += "a"
          strs ::= s.funkify
        }
        strs
      }
    }
    

    结果如下:

    [info] benchmark  ms linear runtime
    [info]    Direct 308 =============================
    [info]  Implicit 309 ==============================
    

    我的结论:在任何不平凡的代码中,隐式转换(对象创建)的影响是不可测量的。

    编辑:我使用了 scala 2.9.0 和 java 1.6.0_24(在服务器模式下)

    【讨论】:

    • 您始终可以禁用 JIT,尽管它在基准测试中的意义值得怀疑。
    • 这就是微基准的问题,每一次试图让比较更公平的尝试都会让你与实际使用相差甚远。
    • 非常感谢您提供详细的基准测试,并感谢您一路向我介绍 Caliper 和 Scala-Benchmark-Template。我非常感激。我有一个跟进。我最初的问题也对内存利用率的任何影响感兴趣。 Caliper 基准测试能告诉我们什么吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-06-02
    • 1970-01-01
    • 2011-08-30
    • 1970-01-01
    • 2014-12-26
    • 1970-01-01
    • 2013-02-23
    相关资源
    最近更新 更多