我遇到了一个装箱问题,这会对我的Scala代码的性能产生负面影响。我提取了相关的代码,这仍然显示了问题,并增加了一些奇怪之处。我有一个2D双数组的如下表示,它允许我通过提供我的函数来对它执行转换:
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()中。这是与Scala2.12.4,Windows x64 jdk1.8.0_92
我的主要问题是关于装箱的问题,我在我的生产代码中也看到了这个问题:
Double装箱发生在Array.tabulate?我是否需要进行过程化(同时循环,手动Array创建)来避免这种情况?我的第二个问题是:
transform变体之后就不再进行装箱了?发布于 2018-02-07 17:33:25
为什么在我调用转换变体之后就不再进行拳击了?
我没有复制它。如果我仔细地暂停VM并检查JProfiler,它仍然会执行大量的装箱和双重分配。这正是我所期望的,我有一个解释。
查看标准库中的Function1和Function2特性,我们可以看到@专用注释:
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使您可以避免使用原语对泛型进行装箱。但这是以编译器不得不生成额外的方法和类为代价的,因此超过某个阈值,它只会产生大量可笑的代码(如果不是完全崩溃的话)。因此,如果我的数学是正确的,Function有4(T1上的规范)x6(规范R) = 24份每个专门的方法和24个额外的类,除了apply和一个通用的特性。
哦,顺便说一下,这些方法都是用$mc和JNI类型签名固定的。因此,以$mcDII结尾的方法是一个特殊的重载,它返回一个Double,并接受两个Int作为参数。这是您要传递给tabulate内部转换的函数类型,即这一部分
(x, y) => f(a(x)(y))而对f的调用应该与$mcDD一起显示(返回一个Double并接受一个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,而且在这个特性中只有一个抽象方法,所以仍然可以传递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的版本,用于您需要的维度。
https://stackoverflow.com/questions/48667703
复制相似问题