Nenhuma das outras respostas menciona o principal motivo da diferença de velocidade: a zipped
versão evita 10.000 alocações de tupla. Como um casal das outras respostas fazer nota, a zip
versão envolve um conjunto intermediário, enquanto que a zipped
versão não, mas alocar uma matriz para 10.000 elementos não é o que faz com que a zip
versão muito pior-É a 10.000 tuplas viveu-curtas que estão sendo colocados nessa matriz. Eles são representados por objetos na JVM, portanto, você está realizando várias alocações de objetos para coisas que imediatamente descartará.
O restante desta resposta entra em um pouco mais de detalhes sobre como você pode confirmar isso.
Melhor benchmarking
Você realmente quer usar uma estrutura como jmh para fazer qualquer tipo de benchmarking de forma responsável na JVM, e mesmo assim a parte responsável é difícil, embora a configuração do jmh em si não seja tão ruim. Se você tem algo project/plugins.sbt
assim:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
E algo build.sbt
assim (estou usando o 2.11.8, já que você mencionou que é isso que está usando):
scalaVersion := "2.11.8"
enablePlugins(JmhPlugin)
Em seguida, você pode escrever sua referência como esta:
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)
}
E execute-o com 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
O que mostra que a zipped
versão obtém uma taxa de transferência 80% mais alta, o que provavelmente é mais ou menos o mesmo que suas medidas.
Medindo alocações
Você também pode pedir ao jmh para medir alocações com -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
… Onde gc.alloc.rate.norm
é provavelmente a parte mais interessante, mostrando que ozip
versão está alocando mais de três vezes o valor zipped
.
Implementações imperativas
Se eu soubesse que esse método seria chamado em contextos extremamente sensíveis ao desempenho, provavelmente o implementaria assim:
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
}
Observe que, diferentemente da versão otimizada em uma das outras respostas, isso usa, em while
vez de um, for
pois o for
ainda será desugar nas operações de coleta do Scala. Podemos comparar essa implementação ( withWhile
), a outra resposta otimizada (mas não no local) ( withFor
) e as duas implementações originais:
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
Essa é realmente uma enorme diferença entre as versões imperativa e funcional, e todas essas assinaturas de método são exatamente idênticas e as implementações têm a mesma semântica. Não é como se as implementações imperativas estivessem usando o estado global, etc.zip
zipped
versões e sejam mais legíveis, eu pessoalmente acho que não há sentido em que as versões imperativas sejam contrárias ao "espírito de Scala" e não hesitaria em para usá-los eu mesmo.
With tabate
Atualização: eu adicionei uma tabulate
implementação ao benchmark com base em um comentário em outra resposta:
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))
}
É muito mais rápido que as zip
versões, embora ainda muito mais lento que as imperativas:
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
Isso é o que eu esperaria, já que não há nada inerentemente caro em chamar uma função e porque acessar elementos de matriz por índice é muito barato.