Existem 3 funções dadas abaixo, que encontram o último, mas o segundo, elemento de uma lista. Aquele usando last . init
parece muito mais rápido que o resto. Não consigo entender o porquê.
Para o teste, usei uma lista de entrada de [1..100000000]
(100 milhões). O último roda quase instantaneamente, enquanto os outros levam alguns segundos.
-- slow
myButLast :: [a] -> a
myButLast [x, y] = x
myButLast (x : xs) = myButLast xs
myButLast _ = error "List too short"
-- decent
myButLast' :: [a] -> a
myButLast' = (!! 1) . reverse
-- fast
myButLast'' :: [a] -> a
myButLast'' = last . init
init
foi otimizado para evitar "descompactar" a lista várias vezes.myButLast
muito mais lento ?. Parece que não é desembalar qualquer lista, mas apenas atravessá-lo comoinit
função faz ...[x, y]
uma abreviação de(x:(y:[]))
, portanto, descompacta os contras externos, um segundo contras e verifica se a cauda do segundocons
é[]
. Além disso, a segunda cláusula descompactará a lista novamente em(x:xs)
. Sim, a descompactação é razoavelmente eficiente, mas é claro que, se acontecer com muita frequência, isso atrasará o processo.init
não verifica repetidamente se seu argumento é uma lista única ou uma lista vazia. Uma vez iniciada a recursão, assume-se que o primeiro elemento será alinhado ao resultado da chamada recursiva.myButLast
automaticamente. Eu acho que é mais provável a fusão de listas que é responsável pelo aumento de velocidade.Respostas:
Ao estudar velocidade e otimização, é muito fácil obter resultados totalmente errados . Em particular, você não pode realmente dizer que uma variante é mais rápida que outra sem mencionar a versão do compilador e o modo de otimização da sua configuração de benchmarking. Mesmo assim, os processadores modernos são tão sofisticados que apresentam preditores de ramificação baseados em redes neurais, sem mencionar todos os tipos de caches; portanto, mesmo com uma configuração cuidadosa, os resultados do benchmarking serão embaçados.
Dito isto ...
Benchmarking é nosso amigo.
criterion
é um pacote que fornece ferramentas avançadas de benchmarking. Elaborei rapidamente uma referência como esta:Como você vê, eu adicionei a variante que corresponde explicitamente a dois elementos ao mesmo tempo, mas, caso contrário, é o mesmo código literalmente. Eu também corro os benchmarks ao contrário, para estar ciente do viés devido ao cache. Então, vamos correr e ver!
Parece que a nossa versão "lenta" não é lenta! E os meandros da correspondência de padrões não adicionam nada. (Uma ligeira velocidade que vemos entre duas execuções consecutivas de
match2
eu atribuo aos efeitos do cache.)Existe uma maneira de obter mais dados "científicos" : podemos
-ddump-simpl
ver a maneira como o compilador vê nosso código.A inspeção de estruturas intermediárias é nossa amiga.
"Core" é uma linguagem interna do GHC. Todo arquivo de origem Haskell é simplificado para o Core antes de ser transformado no gráfico funcional final para a execução do sistema de tempo de execução. Se olharmos para este estágio intermediário, ele nos dirá isso
myButLast
ebutLast2
é equivalente. É preciso procurar, pois, no estágio de renomeação, todos os nossos bons identificadores são mutilados aleatoriamente.Parece que
A1
eA4
são os mais semelhantes. Uma inspeção completa mostrará que, de fato, o código estruturaA1
eA4
é idêntico. IssoA2
eA3
similares também é razoável, pois ambos são definidos como uma composição de duas funções.Se você for examinar a
core
saída extensivamente, faz sentido também fornecer sinalizadores como-dsuppress-module-prefixes
e-dsuppress-uniques
. Eles tornam muito mais fácil a leitura.Uma pequena lista de nossos inimigos também.
Então, o que pode dar errado com o benchmarking e a otimização?
ghci
, sendo projetado para reprodução interativa e iteração rápida, compila a fonte Haskell com um certo sabor de código de bytes, em vez de executável final, e evita otimizações caras em favor de uma recarga mais rápida.Isso pode parecer triste. Mas, na verdade, não é isso que deve preocupar um programador Haskell, na maioria das vezes. História real: tenho um amigo que recentemente começou a aprender Haskell. Eles haviam escrito um programa para integração numérica, e a tartaruga era lenta. Então nos sentamos juntos e escrevemos uma descrição categorizada do algoritmo, com diagramas e outras coisas. Quando eles reescreveram o código para se alinhar com a descrição abstrata, ele magicamente se tornou, tipo, rápido, e também magro na memória. Calculamos π em nenhum momento. Moral da história? Estrutura abstrata perfeita, e seu código se otimizará.
fonte
ghci
parece dar resultados diferentes (em termos de velocidade) em comparação com fazer um exe primeiro, como você disse.