컬렉션에서 요소 별 작업을 수행하기 위해 스칼라 코드를 작성했습니다. 여기서는 동일한 작업을 수행하는 두 가지 방법을 정의했습니다. 한 방법은 사용 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
메소드를 호출 하고 전달 합니다.ES
ES1
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의 컬렉션에서 요소 단위 작업을 수행하는 더 빠른 방법이 있습니까?
답변
두 번째 질문에 대답하려면 :
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
그러나 배열 요소의 직접적인 돌연변이는 스칼라의 정신에 있지 않습니다.
답변
다른 답변은 속도 차이의 주된 이유를 언급 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
대신에 사용 됩니다 . 이 구현 ( ), 다른 답변의 최적화 된 (제자리에없는) 구현 ( ) 및 두 가지 원래 구현을 비교할 수 있습니다 .for
for
withWhile
withFor
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
그것은 명령형 버전과 기능 버전 사이에 큰 차이가 있으며 이러한 모든 메소드 서명은 정확히 동일하며 구현은 동일한 의미를 갖습니다. 명령형 구현이 전역 상태 등을 사용하는 것과는 다릅니다. zip
및 zipped
버전은 더 읽기 쉽지만 개인적으로 명령형 버전이 “스칼라의 정신”에 위배되는 의미가 있다고 생각하지 않으며 주저하지 않을 것입니다 직접 사용하십시오.
표로
업데이트 : 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
이것은 함수 호출에 본질적으로 비싸지 않으며 인덱스로 배열 요소에 액세스하는 것이 매우 저렴하기 때문에 내가 기대하는 것입니다.
답변
치다 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
lazyZip
zipped
on 보다 약간 더 나은 것 같습니다 ArraySeq
. 흥미롭게도에 사용할 때 성능이 크게 저하 lazyZip
됩니다 Array
.
답변
JIT 컴파일로 인해 성능 측정에 항상주의를 기울여야하지만 호출 하는 동안 zipped
게으르고 원래 Array
vaule 에서 요소를 추출 하는 동안 새 오브젝트를 작성한 다음 새 오브젝트를 호출 할 가능성이 있습니다.map
zip
Array
map