스칼라에서 지퍼보다 지퍼가 더 빠른 이유는 무엇입니까?


38

컬렉션에서 요소 별 작업을 수행하기 위해 스칼라 코드를 작성했습니다. 여기서는 동일한 작업을 수행하는 두 가지 방법을 정의했습니다. 한 방법은 사용 zip하고 다른 방법은 사용 합니다 zipped.

def ES (arr :Array[Double], arr1 :Array[Double]) :Array[Double] = arr.zip(arr1).map(x => x._1 + x._2)

def ES1(arr :Array[Double], arr1 :Array[Double]) :Array[Double] = (arr,arr1).zipped.map((x,y) => x + y)

이 두 가지 방법을 속도 측면에서 비교하기 위해 다음 코드를 작성했습니다.

def fun (arr : Array[Double] , arr1 : Array[Double] , f :(Array[Double],Array[Double]) => Array[Double] , itr : Int) ={
  val t0 = System.nanoTime()
  for (i <- 1 to itr) {
       f(arr,arr1)
       }
  val t1 = System.nanoTime()
  println("Total Time Consumed:" + ((t1 - t0).toDouble / 1000000000).toDouble + "Seconds")
}

다음 과 같이 fun메소드를 호출 하고 전달 합니다.ESES1

fun(Array.fill(10000)(math.random), Array.fill(10000)(math.random), ES , 100000)
fun(Array.fill(10000)(math.random), Array.fill(10000)(math.random), ES1, 100000)

결과는 ES1사용 하는 명명 된 zipped방법 ES이 사용하는 방법보다 빠르다는 것을 보여줍니다 zip. 이러한 관찰을 바탕으로 두 가지 질문이 있습니다.

zipped보다 빠릅 zip니까?

Scala의 컬렉션에서 요소 단위 작업을 수행하는 더 빠른 방법이 있습니까?



8
JIT는 두 번째로 재미를 불러 일으킬 때 더 적극적으로 최적화하기로 결정했기 때문에 또는 ES가 실행되는 동안 GC가 무언가를 정리하기로 결정했기 때문입니다. 또는 운영 체제가 ES 테스트를 실행하는 동안 더 나은 작업을 수행하기로 결정했기 때문입니다. 이 마이크로 벤치 마크는 결정적인 것이 아닙니다.
Andrey Tyukin

1
기계의 결과는 어떻습니까? 얼마나 빨리?
Peeyush Kushwaha

동일한 인구 규모 및 구성의 경우 Zip은 44 초
user12140540

3
결과는 의미가 없습니다. 마이크로 벤치 마크를 수행해야하는 경우 JMH를 사용하십시오 .
OrangeDog

답변:


17

두 번째 질문에 대답하려면 :

Scala의 컬렉션에서 요소 현명한 작업을 수행하는 더 빠른 방법이 있습니까?

슬픈 사실은 간결함, 생산성 향상 및 기능 언어가 반드시 성능이 뛰어나지 않은 버그에 대한 탄력성에도 불구하고 고차 함수를 사용하여 무료로 수집되지 않은 컬렉션에 대해 실행되도록 프로젝션을 정의하는 것입니다. 다른 사람들이 지적했듯이 중간 및 최종 결과에 대한 추가 스토리지 할당에도 오버 헤드가 있습니다.

보편적 인 것은 아니지만 성능이 중요한 경우, 귀하와 같은 경우에는 메모리 사용량을보다 직접적으로 제어하고 함수 호출을 제거하기 위해 Scala의 작업을 필수 항목으로 되돌릴 수 있습니다.

특정 예에서, zipped정확한 크기의 고정 가변 가변 배열을 사전 할당하고 (컬렉션 중 하나에 요소가 부족하면 zip이 중지되므로) 액세스 할 때 적절한 인덱스에 요소를 추가 하여 합계를 반드시 수행 할 수 있습니다. 서수 인덱스로 배열 요소는 매우 빠른 연산입니다).

ES3테스트 스위트에 세 번째 기능 추가 :

def ES3(arr :Array[Double], arr1 :Array[Double]) :Array[Double] = {
   val minSize = math.min(arr.length, arr1.length)
   val array = Array.ofDim[Double](minSize)
   for (i <- 0 to minSize - 1) {
     array(i) = arr(i) + arr1(i)
   }
  array
}

i7에서 다음과 같은 응답 시간이 나타납니다.

OP ES Total Time Consumed:23.3747857Seconds
OP ES1 Total Time Consumed:11.7506995Seconds
--
ES3 Total Time Consumed:1.0255231Seconds

두 배열 중 더 짧은 배열을 직접 변형시키는 것이 훨씬 더 복잡 할 것인데, 이는 배열 중 하나의 내용을 분명히 손상시키고 원래 배열이 다시 필요하지 않은 경우에만 수행됩니다.

def ES4(arr :Array[Double], arr1 :Array[Double]) :Array[Double] = {
   val minSize = math.min(arr.length, arr1.length)
   val array = if (arr.length < arr1.length) arr else arr1
   for (i <- 0 to minSize - 1) {
      array(i) = arr(i) + arr1(i)
   }
  array
}

Total Time Consumed:0.3542098Seconds

그러나 배열 요소의 직접적인 돌연변이는 스칼라의 정신에 있지 않습니다.


2
위의 코드에는 병렬화 된 것이 없습니다. 이 특정 문제는 병렬화 가능하지만 (다중 스레드가 배열의 다른 섹션에서 작동 할 수 있기 때문에) 10k 요소에 대한 간단한 작업에는 큰 의미가 없습니다. 새 스레드를 만들고 동기화하는 오버 헤드는 어떤 이점보다 클 것입니다 . 솔직히 말해서,이 수준의 성능 최적화가 필요한 경우 Rust, Go 또는 C에서 이러한 종류의 알고리즘을 작성하는 것이 좋습니다.
StuartLC

3
그것은 것보다 스칼라처럼 빠르게 사용에 Array.tabulate(minSize)(i => arr(i) + arr1(i))배열 생성하기
Sarvesh 쿠마르 싱

1
@SarveshKumarSingh 이것은 훨씬 느립니다. 거의 9 초 소요
user12140540

1
Array.tabulate보다 훨씬 빠르기해야 하나 zip또는 zipped여기 (내 벤치 마크에있다).
Travis Brown

1
@StuartLC "고차 함수가 어떻게 풀리고 인라인 된 경우에만 성능이 동일합니다." 이것은 실제로 정확하지 않습니다. 심지어 귀하의 for고차 함수 호출 ( foreach)으로 설탕을 제거했습니다 . 람다는 두 경우 모두 한 번만 인스턴스화됩니다.
트래비스 브라운

50

다른 답변은 속도 차이의 주된 이유를 언급 zipped하지 않습니다. 즉 버전이 10,000 튜플 할당을 피한다 는 것입니다 . 다른 답변의 몇으로 메모는 zip그동안 버전은 중간 배열을 포함 zipped버전은하지 않지만, 10,000 요소에 대한 배열을 할당하는 것은 무엇이 아닌 zip버전을 훨씬 더-는 10,000 단명 튜플 사용자들은 그 배열에 넣고 있습니다. 이것들은 JVM의 객체로 표시되므로 즉시 버릴 물건에 대해 많은 객체 할당을 수행합니다.

이 답변의 나머지 부분에서는이를 확인할 수있는 방법에 대해 좀 더 자세히 설명합니다.

더 나은 벤치마킹

JVM에서 책임있는 벤치마킹을 수행하기 위해 jmh 와 같은 프레임 워크를 사용하고 싶고 jmh 자체를 설정하는 것은 그리 나쁘지 않지만 책임감있는 부분은 어렵습니다. 당신이있는 경우 project/plugins.sbt이 같은를 :

addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")

그리고 build.sbt이와 같이 (나는 당신이 사용하고 있다고 언급했기 때문에 2.11.8을 사용하고 있습니다) :

scalaVersion := "2.11.8"

enablePlugins(JmhPlugin)

그런 다음 벤치 마크를 다음과 같이 작성할 수 있습니다.

package zipped_bench

import org.openjdk.jmh.annotations._

@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
class ZippedBench {
  val arr1 = Array.fill(10000)(math.random)
  val arr2 = Array.fill(10000)(math.random)

  def ES(arr: Array[Double], arr1: Array[Double]): Array[Double] =
    arr.zip(arr1).map(x => x._1 + x._2)

  def ES1(arr: Array[Double], arr1: Array[Double]): Array[Double] =
    (arr, arr1).zipped.map((x, y) => x + y)

  @Benchmark def withZip: Array[Double] = ES(arr1, arr2)
  @Benchmark def withZipped: Array[Double] = ES1(arr1, arr2)
}

그리고 그것을 실행하십시오 sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 zipped_bench.ZippedBench":

Benchmark                Mode  Cnt     Score    Error  Units
ZippedBench.withZip     thrpt   20  4902.519 ± 41.733  ops/s
ZippedBench.withZipped  thrpt   20  8736.251 ± 36.730  ops/s

이는 zipped버전이 처리량을 약 80 % 더 많이 얻었음을 보여줍니다 .

할당 측정

jmh에게 다음을 사용하여 할당을 측정하도록 요청할 수도 있습니다 -prof gc.

Benchmark                                                 Mode  Cnt        Score       Error   Units
ZippedBench.withZip                                      thrpt    5     4894.197 ±   119.519   ops/s
ZippedBench.withZip:·gc.alloc.rate                       thrpt    5     4801.158 ±   117.157  MB/sec
ZippedBench.withZip:·gc.alloc.rate.norm                  thrpt    5  1080120.009 ±     0.001    B/op
ZippedBench.withZip:·gc.churn.PS_Eden_Space              thrpt    5     4808.028 ±    87.804  MB/sec
ZippedBench.withZip:·gc.churn.PS_Eden_Space.norm         thrpt    5  1081677.156 ± 12639.416    B/op
ZippedBench.withZip:·gc.churn.PS_Survivor_Space          thrpt    5        2.129 ±     0.794  MB/sec
ZippedBench.withZip:·gc.churn.PS_Survivor_Space.norm     thrpt    5      479.009 ±   179.575    B/op
ZippedBench.withZip:·gc.count                            thrpt    5      714.000              counts
ZippedBench.withZip:·gc.time                             thrpt    5      476.000                  ms
ZippedBench.withZipped                                   thrpt    5    11248.964 ±    43.728   ops/s
ZippedBench.withZipped:·gc.alloc.rate                    thrpt    5     3270.856 ±    12.729  MB/sec
ZippedBench.withZipped:·gc.alloc.rate.norm               thrpt    5   320152.004 ±     0.001    B/op
ZippedBench.withZipped:·gc.churn.PS_Eden_Space           thrpt    5     3277.158 ±    32.327  MB/sec
ZippedBench.withZipped:·gc.churn.PS_Eden_Space.norm      thrpt    5   320769.044 ±  3216.092    B/op
ZippedBench.withZipped:·gc.churn.PS_Survivor_Space       thrpt    5        0.360 ±     0.166  MB/sec
ZippedBench.withZipped:·gc.churn.PS_Survivor_Space.norm  thrpt    5       35.245 ±    16.365    B/op
ZippedBench.withZipped:·gc.count                         thrpt    5      863.000              counts
ZippedBench.withZipped:·gc.time                          thrpt    5      447.000                  ms

gc.alloc.rate.norm어쩌면 가장 흥미로운 부분은 아마도 zip버전이 3 배 이상 할당되고 있음을 보여줍니다 zipped.

명령형 구현

이 방법이 성능에 매우 민감한 상황에서 호출 될 것임을 알고 있다면 다음과 같이 구현했을 것입니다.

  def ES3(arr: Array[Double], arr1: Array[Double]): Array[Double] = {
    val minSize = math.min(arr.length, arr1.length)
    val newArr = new Array[Double](minSize)
    var i = 0
    while (i < minSize) {
      newArr(i) = arr(i) + arr1(i)
      i += 1
    }
    newArr
  }

다른 답변 중 하나의 최적화 된 버전과 달리 이것은 Scala 수집 작업으로 탈당 할 것이므로 이후 while대신에 사용 됩니다 . 이 구현 ( ), 다른 답변의 최적화 된 (제자리에없는) 구현 ( ) 및 두 가지 원래 구현을 비교할 수 있습니다 .forforwithWhilewithFor

Benchmark                Mode  Cnt       Score      Error  Units
ZippedBench.withFor     thrpt   20  118426.044 ± 2173.310  ops/s
ZippedBench.withWhile   thrpt   20  119834.409 ±  527.589  ops/s
ZippedBench.withZip     thrpt   20    4886.624 ±   75.567  ops/s
ZippedBench.withZipped  thrpt   20    9961.668 ± 1104.937  ops/s

그것은 명령형 버전과 기능 버전 사이에 큰 차이가 있으며 이러한 모든 메소드 서명은 정확히 동일하며 구현은 동일한 의미를 갖습니다. 명령형 구현이 전역 상태 등을 사용하는 것과는 다릅니다. zipzipped버전은 더 읽기 쉽지만 개인적으로 명령형 버전이 "스칼라의 정신"에 위배되는 의미가 있다고 생각하지 않으며 주저하지 않을 것입니다 직접 사용하십시오.

표로

업데이트 : tabulate다른 답변의 의견을 기반으로 벤치 마크에 구현을 추가했습니다 .

def ES4(arr: Array[Double], arr1: Array[Double]): Array[Double] = {
  val minSize = math.min(arr.length, arr1.length)
  Array.tabulate(minSize)(i => arr(i) + arr1(i))
}

zip명령형 버전보다 훨씬 느리지 만 버전 보다 훨씬 빠릅니다 .

Benchmark                  Mode  Cnt      Score     Error  Units
ZippedBench.withTabulate  thrpt   20  32326.051 ± 535.677  ops/s
ZippedBench.withZip       thrpt   20   4902.027 ±  47.931  ops/s

이것은 함수 호출에 본질적으로 비싸지 않으며 인덱스로 배열 요소에 액세스하는 것이 매우 저렴하기 때문에 내가 기대하는 것입니다.


8

치다 lazyZip

(as lazyZip bs) map { case (a, b) => a + b }

대신에 zip

(as zip bs) map { case (a, b) => a + b }

스칼라 2.13 추가 lazyZip 찬성.zipped

함께 .zip보기에,이 대체합니다은 .zipped(현재 사용되지 않음). ( scala / collection-strawman # 223 )

zipped(따라서는 lazyZip) 속도를 초과 zip하여 설명으로 인해 마이크 알렌 , zip이어서 map인해 엄격 두 개의 별도의 변형을 초래할 것이다 반면 zipped다음 map인해 게으름 한번에 실행 한 변형을 초래할 것이다.

zipped제공 Tuple2Zipped하고 분석합니다 Tuple2Zipped.map.

class Tuple2Zipped[...](val colls: (It1, It2)) extends ... {
  private def coll1 = colls._1
  private def coll2 = colls._2

  def map[...](f: (El1, El2) => B)(...) = {
    val b = bf.newBuilder(coll1)
    ...
    val elems1 = coll1.iterator
    val elems2 = coll2.iterator

    while (elems1.hasNext && elems2.hasNext) {
      b += f(elems1.next(), elems2.next())
    }

    b.result()
  }

우리는이 명 컬렉션을 참조 coll1하고 coll2이상 반복하고 각 반복에 함수가 f전달 map길을 따라 적용

b += f(elems1.next(), elems2.next())

중개 구조를 할당하고 변환 할 필요없이


Travis의 벤치마킹 방법을 적용하면 다음 lazyZip과 같습니다 zipped.

@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
class ZippedBench {
  import scala.collection.mutable._
  val as = ArraySeq.fill(10000)(math.random)
  val bs = ArraySeq.fill(10000)(math.random)

  def lazyZip(as: ArraySeq[Double], bs: ArraySeq[Double]): ArraySeq[Double] =
    as.lazyZip(bs).map{ case (a, b) => a + b }

  def zipped(as: ArraySeq[Double], bs: ArraySeq[Double]): ArraySeq[Double] =
    (as, bs).zipped.map { case (a, b) => a + b }

  def lazyZipJavaArray(as: Array[Double], bs: Array[Double]): Array[Double] =
    as.lazyZip(bs).map{ case (a, b) => a + b }

  @Benchmark def withZipped: ArraySeq[Double] = zipped(as, bs)
  @Benchmark def withLazyZip: ArraySeq[Double] = lazyZip(as, bs)
  @Benchmark def withLazyZipJavaArray: ArraySeq[Double] = lazyZipJavaArray(as.toArray, bs.toArray)
}

준다

[info] Benchmark                          Mode  Cnt      Score      Error  Units
[info] ZippedBench.withZipped            thrpt   20  20197.344 ± 1282.414  ops/s
[info] ZippedBench.withLazyZip           thrpt   20  25468.458 ± 2720.860  ops/s
[info] ZippedBench.withLazyZipJavaArray  thrpt   20   5215.621 ±  233.270  ops/s

lazyZipzippedon 보다 약간 더 나은 것 같습니다 ArraySeq. 흥미롭게도에 사용할 때 성능이 크게 저하 lazyZip됩니다 Array.


lazyZip은 Scala 2.13.1에서 사용할 수 있습니다. 현재 Scala 2.11.8을 사용하고 있습니다.
user12140540

5

JIT 컴파일로 인해 성능 측정에 항상주의를 기울여야하지만 호출 하는 동안 zipped게으르고 원래 Arrayvaule 에서 요소를 추출 하는 동안 새 오브젝트를 작성한 다음 새 오브젝트를 호출 할 가능성이 있습니다.mapzipArraymap

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.