Array.tabulate期间的拳击双打
Boxing Double during Array.tabulate
我遇到了一个装箱问题,它对我的 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
是 运行ning,但我看到 scala.runtime.BoxesRunTime.boxToDouble()
花费了很多时间
- 一旦
doTransforms
是 运行ning,就没有更多的时间花在拳击上了,样本显示 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
我的主要问题是关于装箱,我在生产代码中也看到了装箱:
- 为什么
Array.tabulate
会出现 Double
拳击?我是否需要通过程序(while 循环,手动 Array
创建)来避免它?
我的第二个问题是:
- 为什么我调用
transform
变体后不再进行装箱?
why is no more boxing done once I call the transform variant?
我没有重现。如果我小心地暂停 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
结尾的方法是 returns 一个 Double 的特殊重载,并接受两个 Int 作为参数。这是您在转换中传递给 tabulate
的函数类型,即这部分
(x, y) => f(a(x)(y))
虽然调用 f
应该显示 $mcDD
后缀(returns 一个 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,而且你在 trait 中只有一个抽象方法,你仍然可以传递 lambda,所以这段代码:
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
本身不是专门的,因此在创建数组时会造成浪费 boxing/unboxing。您对此的解决方法是编写一个直接使用 Double
而不是 T
的版本来满足您需要的维度。
我遇到了一个装箱问题,它对我的 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
是 运行ning,但我看到scala.runtime.BoxesRunTime.boxToDouble()
花费了很多时间
- 一旦
doTransforms
是 运行ning,就没有更多的时间花在拳击上了,样本显示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
我的主要问题是关于装箱,我在生产代码中也看到了装箱:
- 为什么
Array.tabulate
会出现Double
拳击?我是否需要通过程序(while 循环,手动Array
创建)来避免它?
我的第二个问题是:
- 为什么我调用
transform
变体后不再进行装箱?
why is no more boxing done once I call the transform variant?
我没有重现。如果我小心地暂停 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
结尾的方法是 returns 一个 Double 的特殊重载,并接受两个 Int 作为参数。这是您在转换中传递给 tabulate
的函数类型,即这部分
(x, y) => f(a(x)(y))
虽然调用 f
应该显示 $mcDD
后缀(returns 一个 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,而且你在 trait 中只有一个抽象方法,你仍然可以传递 lambda,所以这段代码:
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
本身不是专门的,因此在创建数组时会造成浪费 boxing/unboxing。您对此的解决方法是编写一个直接使用 Double
而不是 T
的版本来满足您需要的维度。