Eu escrevi algum código Scala para executar uma operação elemento a elemento em uma coleção. Aqui eu defini dois métodos que executam a mesma tarefa. Um método usa zip
e o outro usa 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)
Para comparar esses dois métodos em termos de velocidade, escrevi o seguinte código:
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")
}
Eu chamo o fun
método e passo ES
e ES1
como abaixo:
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)
Os resultados mostram que o método nomeado ES1
que usa zipped
é mais rápido que o método ES
que usa zip
. Com base nessas observações, tenho duas perguntas.
Por que é zipped
mais rápido que zip
?
Existe uma maneira ainda mais rápida de executar operações por elementos em uma coleção no Scala?
scala
performance
scala-collections
jmh
elementwise-operations
user12140540
fonte
fonte
Respostas:
Para responder sua segunda pergunta:
A triste verdade é que, apesar da concisão, da produtividade aprimorada e da resiliência aos bugs, as linguagens funcionais não são necessariamente as de maior desempenho - usando funções de ordem superior para definir uma projeção a ser executada em coleções não gratuitas, e seu loop restrito destaca isso. Como outros salientaram, a alocação de armazenamento adicional para resultados intermediários e finais também terá custos indiretos.
Se o desempenho for crítico, embora de modo algum universal, em casos como o seu, você pode recuperar as operações do Scala de volta a equivalentes imperativos, a fim de recuperar um controle mais direto sobre o uso da memória e eliminar as chamadas de função.
No seu exemplo específico, as
zipped
somas podem ser executadas imperativamente pré-alocando uma matriz fixa e mutável do tamanho correto (já que o zip pára quando uma das coleções fica sem elementos) e, em seguida, adiciona elementos no índice apropriado (desde o acesso elementos da matriz pelo índice ordinal é uma operação muito rápida).Adicionando uma terceira função
ES3
ao seu conjunto de testes:No meu i7, recebo os seguintes tempos de resposta:
Ainda mais hediondo seria fazer a mutação direta no local da mais curta das duas matrizes, o que obviamente corromperia o conteúdo de uma das matrizes e só seria feita se a matriz original novamente não fosse necessária:
Mas, obviamente, a mutação direta dos elementos da matriz não está no espírito de Scala.
fonte
Array.tabulate(minSize)(i => arr(i) + arr1(i))
para criar sua matrizArray.tabulate
deve ser muito mais rápido do que qualquer umzip
ouzipped
aqui (e está nos meus benchmarks).for
é desejado para uma chamada de função de ordem superior (foreach
). O lambda será instanciado apenas uma vez nos dois casos.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, azip
versão envolve um conjunto intermediário, enquanto que azipped
versão não, mas alocar uma matriz para 10.000 elementos não é o que faz com que azip
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:E algo
build.sbt
assim (estou usando o 2.11.8, já que você mencionou que é isso que está usando):Em seguida, você pode escrever sua referência como esta:
E execute-o com
sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 zipped_bench.ZippedBench"
: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
:… Onde
gc.alloc.rate.norm
é provavelmente a parte mais interessante, mostrando que ozip
versão está alocando mais de três vezes o valorzipped
.Implementações imperativas
Se eu soubesse que esse método seria chamado em contextos extremamente sensíveis ao desempenho, provavelmente o implementaria assim:
Observe que, diferentemente da versão otimizada em uma das outras respostas, isso usa, em
while
vez de um,for
pois ofor
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: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:É muito mais rápido que as
zip
versões, embora ainda muito mais lento que as imperativas: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.
fonte
Considerar
lazyZip
ao invés de
zip
Scala 2.13 adicionado a
lazyZip
favor de.zipped
zipped
(e, portantolazyZip
) é mais rápido do quezip
porque, conforme explicado por Tim e Mike Allen ,zip
seguido pormap
resultará em duas transformações separadas devido ao rigor, enquantozipped
seguido pormap
resultará em uma única transformação executada de uma só vez devido à preguiça.zipped
dáTuple2Zipped
, e analisandoTuple2Zipped.map
,vemos as duas coleções
coll1
ecoll2
somos iterados repetidamente e a cada iteração à qual a funçãof
transmitidamap
é aplicada ao longo do caminhosem ter que alocar e transformar estruturas intermediárias.
Aplicando o método de benchmarking de Travis, aqui está uma comparação entre novo
lazyZip
e obsoleto,zipped
ondedá
lazyZip
parece ter um desempenho um pouco melhor do quezipped
emArraySeq
. Curiosamente, notar um desempenho significativamente degradada pelo usolazyZip
onArray
.fonte
Você deve sempre ter cuidado com a medição de desempenho por causa da compilação JIT, mas um motivo provável é que isso
zipped
é preguiçoso e extrai elementos dosArray
vaules originais durante amap
chamada, enquantozip
cria um novoArray
objeto e depois chamamap
o novo objeto.fonte