我遇到了一个拳击问题,这会影响我的Scala代码的负面效果。我已经提取了相关的代码,这仍然显示了问题,并增加了一些奇怪的内容。我有一个2D Double数组的以下表示,它允许我通过提供我的函数对其执行转换:
case class Container(
a: Array[Array[Double]] = Array.tabulate[Double](10000, 10000)((x,y) => x.toDouble * y)
) {
def transformXY(f: (Double, Double, Double) => Double): Container = {
Container(Array.tabulate[Double](a.length, a.length) { (x, y) =>
f(x, y, a(x)(y))
})
}
def transform(f: Double => Double): Container = {
Container(Array.tabulate[Double](a.length, a.length) { (x, y) =>
f(a(x)(y))
})
}
}
以下代码为我重现了这个问题:
object Main extends App {
def now = System.currentTimeMillis()
val iters = 3
def doTransformsXY() = {
var t = Container()
for (i <- 0 until iters) {
val start = now
t = t.transformXY { (x, y, h) =>
h + math.sqrt(x * x + y * y)
}
println(s"transformXY: Duration ${now - start}")
}
}
def doTransforms() = {
var t = Container()
for (i <- 0 until iters) {
val start = now
t = t.transform { h =>
h + math.sqrt(h * h * h)
}
println(s"transform: Duration ${now - start}")
}
}
if (true) { // Shows a lot of boxing if enabled
doTransformsXY()
}
if (true) { // Shows a lot of boxing again - if enabled
doTransformsXY()
}
if (true) { // Shows java8.JFunction...apply()
doTransforms()
}
if (true) { // Shows java8.JFunction...apply() if doTransforms() is enabled
doTransformsXY()
}
}
当我运行此代码并使用Java VisualVM对其进行采样时,我会遇到以下情况:
doTransformsXY
正在运行时,我发现scala.runtime.BoxesRunTime.boxToDouble()
doTransforms
后,没有更多时间用于装箱,示例显示scala.runtime.java8.JFunction2$mcDII$sp.apply()
doTransformsXY
,仍然没有明显的拳击,scala.runtime.java8.JFunction2$mcDII$sp.apply()
这是使用Scala 2.12.4,Windows x64 jdk1.8.0_92
我的主要问题是拳击,我在生产代码中也看到了:
Double
为什么发生Array.tabulate
拳击?我是否需要进行程序化(循环,手动Array
创建)以避免它?我的第二个问题是:
transform
变种后不再进行拳击?答案 0 :(得分:1)
为什么一旦我调用变换变体就不再进行拳击了?
我没有重现那个。如果我小心地暂停VM并检查JProfiler,它仍然会进行大量的拳击和双打分配。这是我的预期,我有一个解释。
查看标准库中的Function1
和Function2
特征,我们可以看到@specialized注释:
trait Function1[@specialized(Int, Long, Float, Double) -T1, @specialized(Unit, Boolean, Int, Float, Long, Double) +R]
trait Function2[@specialized(Int, Long, Double) -T1, @specialized(Int, Long, Double) -T2, @specialized(Unit, Boolean, Int, Float, Long, Double) +R]
但Function3
只是
trait Function3[-T1, -T2, -T3, +R]
@specialized
是Scala如何让你避免使用基元对泛型进行装箱。但是这需要编译器必须生成其他方法和类的代价,因此超出某个阈值只会产生大量的代码(如果不是直接崩溃)。所以Function
,如果我的数学是正确的,4(T1上的规格)x 6(R上的规格)=每个专业方法的24个副本和24个额外的类除了apply
和一个通用性状。
哦,顺便说一句,这些方法后缀为$mc
和JNI type signatures。因此,以$mcDII
结尾的方法是一个返回Double的专用重载,并接受两个Ints作为参数。这是您在变换中传入tabulate
的函数类型,即此部分
(x, y) => f(a(x)(y))
虽然对f
的调用应显示$mcDD
后缀(返回Double并接受双倍)。
但是,请致电
f(x, y, a(x)(y))
会变成类似
的东西unbox(f(box(x), box(y), box(a(x)(y))))
所以我对解释充满了困扰。现在是解决问题的时候了。要将两种方法的装箱带到等效的形状,请创建一个专门的界面:
trait DoubleFunction3 {
def apply(a: Double, b: Double, c: Double): Double
}
并在transformXY
def transformXY(f: DoubleFunction3): Container = //... same code
由于它是Scala 2.12并且你在trait中只有一个抽象方法,你仍然可以传递lambdas,所以这段代码:
t = t.transformXY { (x, y, h) =>
h + math.sqrt(x * x + y * y)
}
不需要更改。
现在您可能会注意到这并不能完全消除拳击,因为tabulate
也会导致拳击。这是一维tabulate
:
def tabulate[T: ClassTag](n: Int)(f: Int => T): Array[T] = {
val b = newBuilder[T]
b.sizeHint(n)
var i = 0
while (i < n) {
b += f(i)
i += 1
}
b.result()
}
请注意,它适用于通用Builder[T]
,调用方法+=(elem: T)
。 Builder
本身并不专业,因此在创建阵列时会造成浪费的装箱/拆箱。您对此的解决方法是编写一个直接使用Double
而不是T
的版本,以获取所需的维度。