我想在基数3中编写数字,表示为固定长度Array[Byte]
。
以下是一些尝试:
val byteBoard = Array.fill(9)(1.toByte)
val cache: Seq[(Int, Int)] = (0 to 8).map(i => (i, math.pow(3d, i.toDouble).toInt))
@Benchmark
def composePow(): Unit = {
val _ = (0 to 8).foldLeft(0) { case (acc, i) => acc + math.pow(3d, i.toDouble).toInt * byteBoard(i) }
}
@Benchmark
def composeCachedPowWithFold(): Unit = {
val _ = cache.foldLeft(0) { case (acc, (i, k)) => acc + k * byteBoard(i).toInt }
}
@Benchmark
def composeCachedPowWithForeach(): Unit = {
var acc = 0
cache.foreach { case (i, k) => acc = acc + k * byteBoard(i)}
}
@Benchmark
def composeUnrolled(): Unit = {
val _ = byteBoard(0) +
3 * byteBoard(1) +
3 * 3 * byteBoard(2) +
3 * 3 * 3 * byteBoard(3) +
3 * 3 * 3 * 3 * byteBoard(4) +
3 * 3 * 3 * 3 * 3 * byteBoard(5) +
3 * 3 * 3 * 3 * 3 * 3 * byteBoard(6) +
3 * 3 * 3 * 3 * 3 * 3 * 3 * byteBoard(7) +
3 * 3 * 3 * 3 * 3 * 3 * 3 * 3 * byteBoard(8)
}
您能否确认以下结论:
composePow
:装箱+类型转换以使用math.pow
composeCachedPowWithFold
:拳击因为fold是参数化方法composeCachedPowWithForeach
:没有拳击,较少惯用的Scala(局部突变)composeUnrolled
:没有拳击并解释为什么4.比3.更快?
PS:以下是JMH基准测试的结果
[info] IndexBenchmark.composeCachedPowWithFold thrpt 10 7180844,823 ± 1015310,847 ops/s
[info] IndexBenchmark.composeCachedPowWithForeach thrpt 10 14234192,613 ± 1449571,042 ops/s
[info] IndexBenchmark.composePow thrpt 10 1515312,179 ± 34892,700 ops/s
[info] IndexBenchmark.composeUnrolled thrpt 10 152297653,110 ± 2237446,053 ops/s
答案 0 :(得分:1)
我大多同意你对案例1,2,4的分析,但第三种变体真的很有趣!
我同意你的前两个版本:foldLeft
不是@specialized
,所以,是的,有一些装箱拆箱。但是math.pow
对整数算术来说是邪恶的,所有这些转换只会产生额外的开销。
现在让我们仔细看看第三个变种。它是如此缓慢,因为你正在构建一个可变状态的闭包。查看scala -print
的输出。这是您的方法被重写为:
private def composeCachedPowWithForeach(): Unit = {
var acc: runtime.IntRef = scala.runtime.IntRef.create(0);
anon$1.this.cache().foreach({
((x0$3: Tuple2) =>
anon$1.this.
$anonfun$composeCachedPowWithForeach$1(acc, x0$3))
})
};
以下是foreach
中使用的函数:
final <artifact> private[this] def
$anonfun$composeCachedPowWithForeach$1(
acc$1: runtime.IntRef, x0$3: Tuple2
): Unit = {
case <synthetic> val x1: Tuple2 = x0$3;
case4(){
if (x1.ne(null))
{
val i: Int = x1._1$mcI$sp();
val k: Int = x1._2$mcI$sp();
matchEnd3({
acc$1.elem = acc$1.elem.+(k.*(anon$1.this.byteBoard().apply(i)));
scala.runtime.BoxedUnit.UNIT
})
}
else
case5()
};
case5(){
matchEnd3(throw new MatchError(x1))
};
matchEnd3(x: scala.runtime.BoxedUnit){
()
}
};
您看到模式匹配显然产生了大量代码。我不确定它是否会对开销产生很大影响。我个人觉得更有趣的是runtime.IntRef
部分。这是一个对象,它在代码中保留与var acc
对应的可变变量。即使它在代码中看起来像一个简单的局部变量,它也必须以某种方式从闭包中引用,因此被包装到一个对象中,然后被驱逐到堆中。我假设在堆上访问这个可变变量会导致大部分开销。
与此相反,如果byteBoard
作为参数传递,那么第四个变体中的任何内容都不会留下函数的堆栈帧:
private def composeUnrolled(): Unit = {
val _: Int =
anon$1.this.byteBoard().apply(0).+
(3.*(anon$1.this.byteBoard().apply(1))).+
(9.*(anon$1.this.byteBoard().apply(2))).+
(27.*(anon$1.this.byteBoard().apply(3))).+
(81.*(anon$1.this.byteBoard().apply(4))).+
(243.*(anon$1.this.byteBoard().apply(5))).+
(729.*(anon$1.this.byteBoard().apply(6))).+
(2187.*(anon$1.this.byteBoard().apply(7))).+
(6561.*(anon$1.this.byteBoard().apply(8)));
()
};
基本上没有控制流可言,实际上没有任何方法调用(apply
用于访问数组元素,并不算数),而且整体而言它只是一个简单的算术运算,甚至可以适合处理器的寄存器。这就是为什么它如此之快。
当你在这里时,你可能想要对这两种方法进行基准测试:
def ternaryToInt5(bytes: Array[Byte]): Int = {
var acc = 0
val n = bytes.size
var i = n - 1
while (i >= 0) {
acc *= 3
acc += bytes(i)
i -= 1
}
acc
}
def ternaryToInt6(bytes: Array[Byte]): Int = {
bytes(0) +
3 * (bytes(1) +
3 * (bytes(2) +
3 * (bytes(3) +
3 * (bytes(4) +
3 * (bytes(5) +
3 * (bytes(6) +
3 * (bytes(7) +
3 * (bytes(8)))))))))
}
此外,如果您经常使用字节数组,您可能会发现this syntactic sugar有用。