提问人:jlgula 提问时间:6/23/2023 更新时间:6/23/2023 访问量:49
有没有办法使用性能可与 Java 数组相媲美的 Scala 序列?
Is there a way to use Scala sequences with performance comparable to Java arrays?
问:
我有一个 Scala 应用程序,它需要在大块二进制(字节)数据中移动。遵循良好的做法,我更喜欢将数据保留为不可变序列而不是可变数组。应用程序的某些部分运行速度比预期的要慢得多,我已将问题追溯到对序列执行的操作缓慢,例如使用切片访问部分数据。
例如,请考虑以下代码,该代码通过中间缓冲区将数据复制到输出流。我有一个使用数组的版本,另一个版本使用Seq的各种实现:
package app.continuity
import java.io.{ByteArrayOutputStream, OutputStream}
import scala.collection.immutable.ArraySeq
import scala.util.Random
object SequenceTest extends App {
val data: Array[Byte] = new Random().nextBytes(1000000)
val dataAsSeq = data.toIndexedSeq
val dataAsVector = Vector(data: _*)
val dataAsArraySeq = ArraySeq(data: _*)
val output = new ByteArrayOutputStream(10000000)
val buffer = new Array[Byte](4096)
def write1(data: Array[Byte], output: OutputStream): Unit = {
var position: Int = 0
while(position < data.length) {
val writeSize = Math.min(data.length - position, buffer.length)
System.arraycopy(data, position, buffer, 0, writeSize)
output.write(buffer, 0, writeSize)
position += writeSize
}
}
def write2(data: Seq[Byte], output: OutputStream): Unit = {
var position: Int = 0
while (position < data.length) {
val writeSize = Math.min(data.length - position, buffer.length)
data.slice(position, writeSize).copyToArray(buffer, 0, writeSize)
output.write(buffer, 0, writeSize)
position += writeSize
}
}
def showTime(description: String, last: Long): Long = {
val next = System.nanoTime()
val delta = (next - last) / 1000
println(s"$description: $delta uSec")
next
}
var time = System.nanoTime()
write1(data, output)
time = showTime("Array via arraycopy", time)
write2(dataAsSeq, output)
time = showTime("Seq via toIndexSeq", time)
write2(dataAsVector, output)
time = showTime("Seq via Vector", time)
write2(dataAsArraySeq, output)
time = showTime("Seq via ArraySeq", time)
}
显然,在数组的情况下,我可以直接将整个数组写入流,但实际代码更复杂,需要中间缓冲区。
运行此代码将产生以下结果:
Array via arraycopy: 301 uSec
Seq via toIndexSeq: 2398 uSec
Seq via Vector: 2586 uSec
Seq via ArraySeq: 643 uSec
尽管ArraySeq只是包装一个数组,但访问数据所需的时间是原来的两倍。在这种情况下,我怀疑问题是使用 slice 访问部分源数据,但我没有看到像 System.arraycopy 这样的东西可以索引到序列的源数据中。我正在使用 Scala 2.13 和 Java 17。我在具有 IArray 的 Scala 3 中尝试了相同的代码,但它给出了类似的结果,因为它仍然需要切片。
有什么建议吗?
答:
0赞
Dima
6/23/2023
#1
我的结果与你的结果大不相同:
Array: 1795 uSec
Seq: 705 uSec
Iseq: 758 uSec
Aseq: 634 uSec
NOTHING: 729 uSec
(这是在我的笔记本电脑上本地运行的,scastie 速度较慢,但相对规模相同)。
正如你所看到的,不仅“arraycopy”变体是最慢的,而且是我添加的最后一个实验,什么都不做比一些实际实现慢,这表明,你正在测试的操作实际上是如此之快,与函数调用相比,执行所需的时间可以忽略不计。
我还添加了一个更惯用的 scala 实现(在下面表示为“GROUPED”),它恰好始终比您拥有的更快,尽管略有优势:
def grouped(d: Seq[Byte], out: OutputStream) = {
d.iterator.grouped(4096).map(_.toArray).map(out.write)
out.close()
}
Array: 2237 uSec
Seq: 905 uSec
Iseq: 747 uSec
Aseq: 640 uSec
GROUPED: 589 uSec
NOTHING: 582 uSec
评论
0赞
jlgula
7/13/2023
根据 @stefanobaghino 的建议,我重写了基准测试以使用 Java Microbenchmark Harness (JMH)。有一个 GitHub 项目捕获修订后的版本:github.com/jlgula/Benchmark。自述文件中显示的结果仍然显示,与从数组写入相比,从序列写入的性能仍然明显较慢。为了回答 Tim,我尝试使用从序列中滑动,但结果并不好。如果已知源是一个数组,则不需要复制,但问题是关于将序列写入 OutputStream。
评论
write2
slice
sliding(writeSize)
while