有没有办法使用性能可与 Java 数组相媲美的 Scala 序列?

Is there a way to use Scala sequences with performance comparable to Java arrays?

提问人:jlgula 提问时间:6/23/2023 更新时间:6/23/2023 访问量:49

问:

我有一个 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 中尝试了相同的代码,但它给出了类似的结果,因为它仍然需要切片。

有什么建议吗?

数组 scala 序列

评论

0赞 Tim 6/23/2023
你为什么要做一个显式的复制,而不是直接使用?使用而不是循环也可能有所帮助。write2slicesliding(writeSize)while
2赞 stefanobaghino 6/23/2023
你能重复这些测试很多次,看看会发生什么吗?我尝试运行您的代码几次,但得到了很多差异。理想情况下,对于微基准测试,您最好使用 JMH,这样您就不必重新发明(基准测试)轮子。这应该会给你一些可靠的数字来进行比较。
0赞 Luis Miguel Mejía Suárez 6/23/2023
与其去低级别,不如尝试使用 fs2AkkaStreams 之类的东西来更高级地以更有效的方式管理 I/O。

答:

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。